dynamic-rp Architecture

September 18, 2026 ยท View on GitHub

dynamic-rp is the generic resource provider for resource types that do not have their own dedicated provider implementation in this repository.

This process owns generic resource lifecycle handling for dynamic types. It is the main place for authoring and handling Radius resource types through a generic provider model.

Entry Points

Quick Reference

TopicStart Here
Startupcmd/dynamic-rp/cmd/root.go
Host compositionpkg/dynamicrp/server/server.go
API servicepkg/dynamicrp/frontend/service.go
Route wiringpkg/dynamicrp/frontend/routes.go
Async backendpkg/dynamicrp/backend/service.go
Test FocusPackages
Frontend/read-path behavior./pkg/dynamicrp/frontend/...
Backend/processor behavior./pkg/dynamicrp/backend/...
Integration coverage./pkg/dynamicrp/integrationtest/...
Broad safety check./pkg/dynamicrp/...

Core Packages

PackageResponsibility
pkg/dynamicrp/frontendrequest handling and API surface
pkg/dynamicrp/backendbackend processing and async work
pkg/dynamicrp/datamodeldynamic resource persistence model
pkg/dynamicrp/apiversioned API types
pkg/dynamicrp/serverprocess bootstrap and hosting

How It Works

The process starts in cmd/dynamic-rp/cmd/root.go, which reads config, constructs runtime options, and builds a host through pkg/dynamicrp/server/server.go.

Dynamic RP exists so Radius resource types can be authored without a bespoke RP implementation for each type, so its route and backend layers need to stay type-agnostic.

Change dynamic-rp when the behavior applies across dynamic resource types, such as generic lifecycle orchestration, shared persistence behavior, generic async handling, common validation and metadata behavior, or new Radius resource type authoring patterns.

Invariants And Constraints

  • Keep the implementation generic and type-agnostic where possible.
  • Avoid leaking dedicated provider behavior into the dynamic provider.
  • Treat persistence, queueing, and secrets as shared runtime dependencies rather than business logic destinations.

Change This Safely

Scoped containerImages Bicep Hook

The Bicep driver has a hook used only by Radius.Compute/containerImages. When a recipe returns an imageBuild object, the driver loads the script embedded in the recipe, adds the operator-configured registry, passes the object's fields as command-line flags, and waits for the script to push the image. The script and imageBuild output can change together without a Radius driver change. Other resource types ignore this output, and recipe parameters cannot provide the script.

ARM/Bicep cannot call BuildKit directly, so dynamic-rp runs the script where the in-Pod BuildKit endpoint is available. The registry and credential Secret name come from the registered Recipe, which prevents developer overrides from redirecting credentials. Radius reads the Secret from the recipe runtime namespace on the selected cluster, including RADIUS_TARGET_KUBECONFIG, and does not fall back when that target is invalid. The build runs on every recipe execution and stores no state.

Shared BuildKit Capacity

All containerImages operations in one dynamic-rp Pod share its BuildKit sidecar and memory cgroup. The chart configures BuildKit's native OCI worker scheduler through dynamicrp.buildkit.maxParallelism, which defaults to one concurrent build step across all active solves. A generated buildkitd.toml carries the setting, and its Pod-template checksum restarts dynamic-rp when the value changes because BuildKit reads daemon configuration only at startup.

Git build sources need no local storage. For local sources, dynamicrp.buildkit.localContexts.existingClaim mounts an operator-managed PVC read-only into dynamic-rp at /var/radius/build-contexts; the chart does not create or populate the claim.

Keep this limit independent from workerServer.maxOperationConcurrency. The worker setting bounds all Dynamic RP operations, while BuildKit's scheduler bounds the memory-intensive execution steps within and across image builds. Increasing BuildKit parallelism requires profiling representative cold builds and sizing the sidecar's memory request and limit with sufficient headroom. This is a concurrency boundary, not per-build isolation: one build can still exceed the configured memory limit.

Packages That Usually Move Together

  • pkg/dynamicrp/frontend and pkg/dynamicrp/backend when request handling and async behavior are linked
  • pkg/dynamicrp/api and pkg/dynamicrp/datamodel when resource shape changes
  • pkg/dynamicrp/server, config, and options code when process bootstrap changes

Suggested Test Scope

  • go test ./pkg/dynamicrp/...
  • Pay particular attention to frontend, backend/controller, backend/processor, and integration tests under pkg/dynamicrp/integrationtest/...

Package Dependency View

graph TD
  Root[cmd/dynamic-rp/cmd]
  Options["pkg/dynamicrp<br/>config and options"]
  Host["pkg/dynamicrp/server<br/>plus hosting/components"]
  Frontend[pkg/dynamicrp/frontend]
  Backend[pkg/dynamicrp/backend]
  BackendController[pkg/dynamicrp/backend/controller]
  API[pkg/dynamicrp/api]
  DataModel["pkg/dynamicrp/datamodel<br/>and converter"]
  Shared[pkg/armrpc + pkg/components + middleware]
  UCPClients[pkg/ucp/api/v20231001preview + pkg/sdk]
  Crypto[pkg/crypto/encryption]
  Recipes[pkg/recipes/engine]
  Kube[Kubernetes provider and runtime client]

  Root --> Options
  Root --> Host
  Host --> Frontend
  Host --> Backend
  Host --> Shared
  Frontend --> API
  Frontend --> DataModel
  Frontend --> Shared
  Frontend --> UCPClients
  Frontend --> Crypto
  Frontend --> Kube
  Backend --> BackendController
  Backend --> DataModel
  Backend --> Shared
  Backend --> Recipes
  Backend --> UCPClients
  Backend --> Kube
  BackendController --> DataModel
  BackendController --> Recipes
  BackendController --> UCPClients
  BackendController --> Shared
  Crypto --> Kube

The important static seam is root -> host -> frontend generic routes versus backend default controller registration. Dynamic RP is organized around a generic request model rather than a large set of resource-specific setup packages.

Representative Flow

sequenceDiagram
  participant UCP
  participant API as dynamic-rp frontend
  participant Route as frontend/routes.go
  participant DefaultAsync as default async PUT/DELETE controller
  participant Store as resource database
  participant Status as status manager
  participant Queue
  participant Worker as backend worker
  participant DefaultCtrl as default dynamic controller
  participant Processor as dynamic processor / recipes

  UCP->>API: PUT or DELETE dynamic resource request
  API->>Route: match generic route
  Route->>DefaultAsync: default async handler
  opt PUT request
    DefaultAsync->>DefaultAsync: apply defaults and validate plaintext properties
    break Schema-invalid PUT
      DefaultAsync-->>API: 400 InvalidRequestContent
      API-->>UCP: synchronous error (resource unchanged)
    end
    DefaultAsync->>DefaultAsync: encrypt sensitive fields
  end
  DefaultAsync->>Store: persist resource state
  DefaultAsync->>Status: create status + queue message
  Status->>Queue: enqueue request
  API-->>UCP: ARM async response
  Queue->>Worker: dequeue request
  Worker->>DefaultCtrl: default controller from registry
  DefaultCtrl->>Processor: validate/process resource

The representative Dynamic RP flow is generic request-to-default-controller handoff. The frontend builds generic routes and default async handlers, then the backend worker resolves the operation through a default controller factory instead of a resource-specific registration table.

PUT update filters run in the order defaults, plaintext schema validation, encryption, before the shared controller saves the resource or queues an operation. A schema-invalid create returns HTTP 400 with InvalidRequestContent and leaves no resource. A rejected update leaves the existing properties, provisioning state, metadata, and ETag unchanged. Valid PUTs and DELETEs retain their asynchronous behavior; the backend also retains validation of stored, encrypted resource data.

Plaintext validation enforces the declared constraints on sensitive fields before encryption can replace their values with encrypted objects. Redacted values returned by GET are not instructions to retain old secrets: supplied null values follow the schema's nullability rules. Validation errors identify schema-declared top-level fields without exposing submitted sensitive values or nested object keys. This ordering prevents new invalid writes; it does not repair invalid properties persisted by earlier versions.