Skip to content

Add scoped containerImages support to Bicep recipes#12361

Open
AzureMike wants to merge 4 commits into
radius-project:mainfrom
AzureMike:feature/container-images-bicep
Open

Add scoped containerImages support to Bicep recipes#12361
AzureMike wants to merge 4 commits into
radius-project:mainfrom
AzureMike:feature/container-images-bicep

Conversation

@AzureMike

@AzureMike AzureMike commented Jul 9, 2026

Copy link
Copy Markdown

Add scoped containerImages support to Bicep recipes

Description

Adds a small hook to the Bicep recipe driver for Radius.Compute/containerImages. After the Bicep deployment finishes, dynamic-rp runs the build script included in the published recipe, waits for buildctl to push the image, and then returns imageReference.

The script must be included in the published recipe. Resource properties are passed to it as command arguments, so app developers can't supply shell code. For a private registry, Radius gets the username and password from a Kubernetes Secret in the cluster and namespace where the app runs.

The hook runs only for Radius.Compute/containerImages. Other Bicep recipes keep their current behavior.

Related to radius-project/resource-types-contrib#220

Paired with radius-project/resource-types-contrib#228

Why this needs a Radius change

Bicep can describe resources, but it can't call the BuildKit sidecar in the dynamic-rp Pod or return the result of that image push. Running the script in dynamic-rp uses the existing BuildKit setup and lets Radius wait for success before saving imageReference.

Testing

go test ./pkg/recipes/driver/bicep

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a type-scoped execution hook to the Bicep recipe driver so Radius.Compute/containerImages recipes can run a packaged build script inside dynamic-rp after deployment and return the pushed imageReference.

Changes:

  • Introduces a new container image build hook that extracts a reserved imageBuild output, validates it, runs a statically embedded script, and merges imageReference into the recipe output.
  • Adds Kubernetes Secret-based registry auth materialization (via the shared cluster-access resolver) into a temporary DOCKER_CONFIG for authenticated builds, and explicitly blanks DOCKER_CONFIG for unauthenticated builds.
  • Documents the new scoped hook behavior in the dynamic-rp architecture guide and adds unit tests covering parsing, execution, and result contract enforcement.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pkg/recipes/driver/bicep/containerimages.go Implements imageBuild parsing/validation, script execution, result reading, and docker auth config writing.
pkg/recipes/driver/bicep/containerimages_unix.go Unix process-group/cancel handling so child processes are killed on timeout/cancel.
pkg/recipes/driver/bicep/containerimages_windows.go Windows stub so the package cross-compiles.
pkg/recipes/driver/bicep/containerimages_test.go Unit tests for spec/script extraction, deterministic args, execution contract, and docker config writing.
pkg/recipes/driver/bicep/bicep.go Wires in the hook during Execute and initializes the cluster access resolver on the driver.
docs/architecture/dynamic-rp.md Documents the scoped containerImages Bicep hook and its constraints.

Comment thread pkg/recipes/driver/bicep/containerimages.go Outdated
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.01235% with 51 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.45%. Comparing base (882b2ef) to head (88e99a9).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pkg/recipes/driver/bicep/containerimages.go 81.38% 22 Missing and 21 partials ⚠️
pkg/recipes/driver/bicep/bicep.go 0.00% 8 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12361      +/-   ##
==========================================
+ Coverage   53.34%   53.45%   +0.11%     
==========================================
  Files         756      758       +2     
  Lines       49030    49269     +239     
==========================================
+ Hits        26153    26338     +185     
- Misses      20450    20480      +30     
- Partials     2427     2451      +24     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

// dynamic-rp. The build script is deliberately not part of this evaluated output: it is read
// from a static compiled-template variable instead, so developer-controlled parameters can
// only be data.
type imageBuildSpec struct {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks like it's tied to the schema for Radius.Compute/containerImages. if we change that type schema, then this will need to be updated too.

if err != nil {
return fmt.Errorf("failed to create a target-cluster client for registry Secret '%s/%s': %w", runtime.Namespace, spec.RegistrySecretName, err)
}
secret, err := clientset.CoreV1().Secrets(runtime.Namespace).Get(ctx, spec.RegistrySecretName, metav1.GetOptions{})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does the terraform version load the secrets? ideally we shouldn't be pulling the secrets from kubernetes because we might be deploying to ACI or some other compute platform

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Terraform also reads username and password from a Kubernetes Secret in the runtime namespace. The original design runs the build inside the dynamic-rp Pod using its BuildKit sidecar, and the Bicep path keeps the same behavior

// configureScriptProcessGroup runs the script in its own process group and kills the whole
// group on cancellation, so processes the script spawned (e.g. buildctl) do not outlive the
// shell when the operation times out or is canceled.
func configureScriptProcessGroup(cmd *exec.Cmd) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there anything we can do to remove these per-platform files?

@AzureMike AzureMike Jul 10, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There may be a way, but I'm not sure. configureScriptProcessGroup uses Unix-only process APIs to put the shell and its child processes into one group. If the operation is canceled, Radius kills the entire group, including buildctl. Windows does not provide the same APIs, so this implementation cannot live in the shared Go file without breaking Windows compilation. The small Windows stub keeps cross-platform builds working, even though the hook runs only on Linux.


func (d *bicepDriver) executeImageBuild(ctx context.Context, script string, spec *imageBuildSpec, opts driver.ExecuteOptions) (string, error) {
logger := logr.FromContextOrDiscard(ctx)
tempDir, err := os.MkdirTemp("", "radius-imagebuild-")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this similar behavior to the terraform version? or specific to bicep?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is Bicep-specific plumbing, but the build outcome matches Terraform. Terraform runs buildctl through local-exec in its module directory and uses Terraform state to skip unchanged builds. Bicep cannot run shell commands, so the driver creates a temporary directory, runs the embedded script, waits for buildctl to finish, reads the result, and cleans up. Bicep runs this on every recipe execution, while Terraform may skip unchanged builds.


// writeDockerConfig materializes registry credentials from the recipe runtime namespace. The
// Secret values are already decoded by client-go and are never logged.
func (d *bicepDriver) writeDockerConfig(ctx context.Context, spec *imageBuildSpec, dir string, opts driver.ExecuteOptions) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how do we guarantee parity with terraform?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parity is not automatic because Terraform and Bicep are separate implementations. This function matches Terraform by reading the same Secret, requiring username and password, and writing the same Docker authentication config for buildctl. The tests verify the target cluster, Secret path, generated JSON, and file permissions.

@willdavsmith willdavsmith left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looking good, left some comments

@AzureMike
AzureMike force-pushed the feature/container-images-bicep branch from 20e47de to 9f2ec0e Compare July 10, 2026 20:37
@AzureMike
AzureMike requested a deployment to external-contributor-approval July 10, 2026 20:38 — with GitHub Actions Waiting
@AzureMike
AzureMike requested a review from Copilot July 10, 2026 20:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment on lines +212 to +216
for _, value := range env {
name, _, _ := strings.Cut(value, "=")
if name == dockerConfigEnvName || name == execOutputEnvName {
continue
}
@@ -0,0 +1,367 @@
/*
@AzureMike
AzureMike requested a review from willdavsmith July 10, 2026 20:52
@AzureMike
AzureMike marked this pull request as draft July 13, 2026 18:17
Mike Azure and others added 4 commits July 13, 2026 17:26
Signed-off-by: Mike Azure <mikeazure+microsoft@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ebd094d9-0447-48e6-9ab2-07506f7755c2
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: aea4b23a-3bc7-4672-be88-6ed2103d8ea4
Signed-off-by: Mike Azure <mikeazure+microsoft@microsoft.com>
Signed-off-by: Mike Azure <mikeazure+microsoft@microsoft.com>
@AzureMike
AzureMike force-pushed the feature/container-images-bicep branch from 9f2ec0e to 88e99a9 Compare July 14, 2026 17:32
@AzureMike
AzureMike marked this pull request as ready for review July 14, 2026 17:33
@AzureMike
AzureMike requested a review from Copilot July 14, 2026 17:34
@radius-functional-tests

radius-functional-tests Bot commented Jul 14, 2026

Copy link
Copy Markdown

Radius functional test overview

🔍 Go to test action run

Click here to see the test run details
Name Value
Repository AzureMike/radius
Commit ref 88e99a9
Unique ID func1e5ef03151
Image tag pr-func1e5ef03151
  • Dapr: 1.14.4
  • Azure KeyVault CSI driver: 1.4.2
  • Azure Workload identity webhook: 1.3.0
  • Bicep recipe location ghcr.io/radius-project/dev/test/testrecipes/test-bicep-recipes/<name>:pr-func1e5ef03151
  • Terraform recipe location http://tf-module-server.radius-test-tf-module-server.svc.cluster.local/<name>.zip (in cluster)
  • applications-rp test image location: ghcr.io/radius-project/dev/applications-rp:pr-func1e5ef03151
  • dynamic-rp test image location: ghcr.io/radius-project/dev/dynamic-rp:pr-func1e5ef03151
  • controller test image location: ghcr.io/radius-project/dev/controller:pr-func1e5ef03151
  • ucp test image location: ghcr.io/radius-project/dev/ucpd:pr-func1e5ef03151
  • deployment-engine test image location: ghcr.io/radius-project/deployment-engine:latest

Test Status

⌛ Building Radius and pushing container images for functional tests...
✅ Container images build succeeded
⌛ Publishing Bicep Recipes for functional tests...
✅ Recipe publishing succeeded
⌛ Starting corerp-cloud functional tests...
⌛ Starting ucp-cloud functional tests...
✅ ucp-cloud functional tests succeeded
✅ corerp-cloud functional tests succeeded

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Comment on lines +277 to +283
if err != nil {
message := fmt.Sprintf("recipe %q script failed: %s", imageBuildOutputName, err.Error())
if stderrTail != "" {
message += "\nstderr (tail):\n" + stderrTail
}
return "", errors.New(message)
}
Comment on lines +394 to +398
text, err := reader.ReadString('\n')
if text != "" {
logger.Info(logPrefix + strings.TrimRight(text, "\r\n"))
if tail != nil {
tail.appendText(text)
Comment on lines +498 to +500
func Test_RunScript_LongOutputLineDoesNotHang(t *testing.T) {
ctx, cancel := context.WithTimeout(testcontext.New(t), 10*time.Second)
defer cancel()
Comment on lines +506 to +509
func Test_RunScript_CancelKillsSpawnedProcesses(t *testing.T) {
ctx, cancel := context.WithTimeout(testcontext.New(t), 500*time.Millisecond)
defer cancel()
start := time.Now()
@AzureMike AzureMike self-assigned this Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants