Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions Compute/containerImages/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
## Overview

The `Radius.Compute/containerImages` resource type builds a container image from source and pushes it to a remote container registry (e.g. ghcr.io). It is always part of a Radius Application.

Builds run inside the cluster on a rootless [BuildKit](https://github.com/moby/buildkit) sidecar that ships with the dynamic-rp Pod. There is no host Docker socket, no privileged Pod, and no per-node host preparation. The recipe drives BuildKit by invoking the `buildctl` CLI, which is mounted into the dynamic-rp container by an init container; the in-Pod buildkitd sidecar exposes its gRPC API on Pod loopback TCP.

Developer documentation is embedded in the resource type definition YAML and is accessible via `rad resource-type show Radius.Compute/containerImages`.

## Architecture

```
┌──────────────────────────────────────────────────────────────────┐
│ dynamic-rp Pod │
│ │
│ ┌──────────────────────┐ ┌────────────────────────┐ │
│ │ dynamic-rp container │ ──────► │ buildkitd sidecar │ │
│ │ (runs Terraform │ unix │ (rootless, no privs) │ │
│ │ recipe) │ socket │ │ │
│ └──────────────────────┘ └───────────┬────────────┘ │
│ │ │
└────────────────────────────────────────────────┼─────────────────┘
│ HTTPS push
┌────────────────────────┐
│ user's container │
│ registry │
└────────────────────────┘
```

## Recipes

| Platform | IaC Language | Recipe Name | Stage |
|---|---|---|---|
| Kubernetes | Terraform | `recipes/kubernetes/terraform/main.tf` | Alpha |

## Recipe Parameters

The platform engineer registers the recipe once per environment with a default registry path. Registry credentials are delivered separately as a Kubernetes Secret mounted into dynamic-rp by the Helm chart (see [Prerequisites](#prerequisites)); they are not recipe parameters.

```bash
rad recipe register default \
--resource-type Radius.Compute/containerImages \
--template-kind terraform \
--template-path "git::https://github.com/radius-project/resource-types-contrib.git//Compute/containerImages/recipes/kubernetes/terraform" \
--parameters registry=ghcr.io/myorg
```

| Parameter | Required | Description |
|---|---|---|
| `registry` | yes | Registry path images are pushed under, e.g. `ghcr.io/myorg`. The recipe composes `<registry>/<resource-name>:<tag>` to form the full image reference. May be overridden per-resource via `properties.registry`. |

## Resource Properties

| Property | Required | Description |
|---|---|---|
| `environment` | yes | Radius Environment ID. |
| `application` | yes | Radius Application ID. |
| `tag` | no | Tag for the produced image. Defaults to a content-addressable digest (`sha256-<hash>`) computed from the build inputs. **Required** when `build.context` is a remote git URL. |
| `registry` | no | Per-resource override of the recipe's `registry` parameter. |
| `build.context` | yes | Source location. Either a git URL of the form `git::https://…` (BuildKit clones inside the cluster) or, for local-development workflows, a path that the rad CLI uploads as a tarball. |
| `build.dockerfile` | no | Path to the Dockerfile relative to the context. Defaults to `Dockerfile`. |
| `build.platforms` | no | Target platforms (e.g. `["linux/amd64", "linux/arm64"]`). When omitted, the build targets the BuildKit sidecar's native architecture. Multi-platform builds use cross-compilation. |

## Output Properties

| Output | Description |
|---|---|
| `properties.image` | The full resolved image reference, e.g. `ghcr.io/myorg/myimage:sha256-d4f2…`. Reference this from `Radius.Compute/containers` resources so they pick up new digests automatically. |

## Prerequisites

1. **dynamic-rp installed with the BuildKit sidecar enabled.** The default Helm install enables it. On Kubernetes < 1.30 (or any cluster without `UserNamespacesSupport`), install with `--set dynamicrp.buildkit.psaMode=baseline`.

2. **Registry credentials provisioned as a Kubernetes Secret.** The platform engineer creates a Secret in the dynamic-rp namespace whose `config.json` key holds a Docker config-format credentials document, then points the chart at it via `--set dynamicrp.buildkit.credentialsSecret=<secret-name>`.

```bash
kubectl create secret generic ghcr-credentials \
--from-file=config.json=$HOME/.docker/config.json \
--namespace radius-system
helm upgrade radius radius/radius \
--set dynamicrp.buildkit.credentialsSecret=ghcr-credentials
```

## Multi-architecture builds

Multi-platform builds use cross-compilation. The Dockerfile must use the standard `BUILDPLATFORM` / `TARGETPLATFORM` build args:

```dockerfile
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
COPY . .
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/app .

FROM alpine
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]
```

Dockerfiles whose `RUN` steps execute target-arch binaries (e.g. some `apt-get install` post-install scripts) will fail at build time with `exec format error`. The recipe does not silently fall back to a single arch, because that produces runtime crashes on foreign-arch nodes long after the deploy "succeeds." The remediation is to make the Dockerfile cross-compile-friendly or to drop the foreign architecture.

There is no QEMU/binfmt fallback in this design.

## Lifecycle and destroy semantics

`terraform destroy` (triggered by `rad app delete` or removing the resource from the Bicep file) removes the resource from Radius state but does **not** delete the pushed image from the registry. This is intentional:

- Image deletion is a registry-side concern. Some registries (e.g. Docker Hub) forbid tag deletion entirely; some (e.g. GHCR) require admin credentials beyond what the recipe is granted.
- Other Radius applications or external consumers may pull the same image reference.

Operators who need image cleanup should run a separate registry retention policy. Examples:

- **GHCR**: [`actions/delete-package-versions`](https://github.com/actions/delete-package-versions) on a schedule.
- **Azure Container Registry**: `az acr task` with `acr purge`.
- **Generic OCI**: `oras` plus a cron-style cleanup.

## Local testing

The contrib repo's generic `test-recipe.sh` harness cannot run this recipe in CI today: it does not pass per-recipe `--parameters`, and the kind cluster used for CI has no test registry. To test changes locally:

```bash
# 1. Stand up a kind cluster with the dynamic-rp + buildkit sidecar
rad install kubernetes --set dynamicrp.buildkit.enabled=true \
--set dynamicrp.buildkit.psaMode=baseline \
--set dynamicrp.buildkit.credentialsSecret=ghcr-credentials

# 2. Register this recipe pointing at your branch
rad recipe register default \
--resource-type Radius.Compute/containerImages \
--template-kind terraform \
--template-path "git::https://github.com/<you>/resource-types-contrib.git//Compute/containerImages/recipes/kubernetes/terraform?ref=<branch>" \
--parameters registry=ghcr.io/<your-org>

# 3. Deploy the test fixture
rad deploy Compute/containerImages/test/app.bicep
```

The follow-up issue tracking full CI integration (test registry, per-recipe parameter injection in the harness) is referenced from the resource-types-contrib backlog.

## Limitations

- **No `latest` for git contexts.** When `build.context` is a git URL, an explicit `properties.tag` is required. The recipe cannot compute a content-addressable tag from a remote tree, and defaulting to `latest` would defeat downstream reconciliation (the URL doesn't change between commits, so `properties.image` wouldn't change and Kubernetes wouldn't roll the Deployment).

- **Local context upload is deferred.** The first cut supports git URLs end-to-end. Local-path contexts will be uploaded by the rad CLI in a later iteration.
123 changes: 123 additions & 0 deletions Compute/containerImages/containerImages.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
namespace: Radius.Compute
types:
containerImages:
description: |
The Radius.Compute/containerImages Resource Type builds a container image from source and pushes it to a container registry. It is always part of a Radius Application.

Builds run inside the cluster on a rootless BuildKit sidecar that ships with the dynamic-rp Pod. There is no host Docker socket, no privileged Pod, and no per-node host preparation. The recipe drives BuildKit by invoking the `buildctl` CLI mounted into the dynamic-rp container; the in-cluster buildkitd sidecar exposes its gRPC API on Pod loopback TCP.

The full image reference is composed by the recipe as `<registry>/<resource-name>:<tag>` where:

- `<registry>` is the registry path configured by the platform engineer when registering the recipe (e.g. `ghcr.io/myorg`).
- `<resource-name>` is the name of the containerImages resource (e.g. `myImage` becomes `myimage`).
- `<tag>` defaults to a content-addressable digest (`sha256-<hash>`) derived from the build inputs so that downstream `Radius.Compute/containers` resources observe a property change on every code change. A user-provided `tag` overrides this. Note: when `build.context` is a remote git URL, an explicit `tag` is required because the recipe cannot hash the remote tree.

Registry credentials are a platform-engineer concern. The PE provisions a `Radius.Security/secrets` resource (whose recipe materializes a Kubernetes Secret of the same name in the environment namespace) and wires its name into the recipe pack as the `registrySecretName` recipe parameter — `parameters: { registry: 'ghcr.io/myorg', registrySecretName: ghcrCreds.name }`. The developer's Bicep never references the secret. The recipe reads the underlying Kubernetes Secret via `data "kubernetes_secret"` using the name supplied by the PE and the recipe's runtime namespace, assembles a Docker `config.json`, and runs BuildKit. Cluster-level image pull (kubelet → registry) is handled by the platform out-of-band (e.g. a cluster-wide pull secret on the default ServiceAccount, an OCI mirror, or kubelet credential providers).

extension radius
extension containerImages
extension containers

param environment string

resource myApp 'Radius.Core/applications@2025-08-01-preview' = {
name: 'myApp'
properties: {
environment: environment
}
}

resource myImage 'Radius.Compute/containerImages@2025-08-01-preview' = {
name: 'myImage'
properties: {
environment: environment
application: myApp.id
imageTag: 'v1.2.3'
build: {
context: 'git::https://github.com/myorg/myapp.git//frontend'
}
}
}

resource myContainer 'Radius.Compute/containers@2025-08-01-preview' = {
name: 'myContainer'
properties: {
environment: environment
application: myApp.id
containers: {
app: {
image: myImage.properties.image
ports: {
web: {
containerPort: 3000
}
}
}
}
}
}

Prerequisites:

1. dynamic-rp must be installed with the BuildKit sidecar enabled (default in the Helm chart). On Kubernetes < 1.30 the platform engineer must install with `--set dynamicrp.buildkit.psaMode=baseline`.

2. The platform engineer provisions a `Radius.Security/secrets` resource (e.g. `ghcr-creds`) with `username` and `password` data keys. Its recipe materializes a Kubernetes Secret of the same name in the environment namespace.

3. The platform engineer registers the recipe once per environment, supplying `registry` and `registrySecretName`. Easiest done via Bicep on a `Radius.Core/recipePacks` resource:

'Radius.Compute/containerImages': {
recipeKind: 'terraform'
recipeLocation: 'git::https://github.com/radius-project/resource-types-contrib.git//Compute/containerImages/recipes/kubernetes/terraform'
parameters: {
registry: 'ghcr.io/myorg'
registrySecretName: ghcrCreds.name
}
}

For an unauthenticated registry (e.g. a local kind registry) omit `registrySecretName`.

Multi-architecture builds (e.g. `platforms: ["linux/amd64", "linux/arm64"]`) require a Dockerfile that supports cross-compilation via `FROM --platform=$BUILDPLATFORM` and `TARGETARCH`. Dockerfiles that execute target-arch binaries during the build will fail with `exec format error`. There is no QEMU/binfmt fallback in this design.

apiVersions:
'2025-08-01-preview':
schema:
type: object
properties:
environment:
type: string
description: (Required) The Radius Environment ID. Typically set by the rad CLI. Typical value should be `environment`.
application:
type: string
description: (Required) The Radius Application ID. `myApplication.id` for example.
imageName:
type: string
description: (Optional) Override the image name. Defaults to the (lowercased) resource name. The recipe composes `<registry>/<imageName>:<imageTag>`.
imageTag:
type: string
description: (Optional) Tag for the produced image. When unset, the recipe computes a content-addressable digest (`sha256-<hash>`) from the build inputs. Required when `build.context` is a remote git URL.
image:
type: string
readOnly: true
description: (Read-only) The full image reference produced by the recipe in the form `<registry>/<imageName>:<imageTag>`. Reference this from `Radius.Compute/containers` to consume the built image.
build:
type: object
description: (Required) Build configuration for the container image.
properties:
context:
type: string
description: (Required) Source location for the build. Either a git URL of the form `git::https://...` (BuildKit clones the repo inside the cluster) or, for local development, a path that the rad CLI uploads as a tarball. `git::https://github.com/myorg/myapp.git//frontend` for example.
dockerfile:
type: string
description: (Optional) Path to the Dockerfile relative to the build context. Defaults to `Dockerfile`.
platforms:
type: array
items:
type: string
description: |
(Optional) Target platforms to build for (e.g. `["linux/amd64"]`, `["linux/amd64", "linux/arm64"]`). When omitted, defaults to `["linux/amd64", "linux/arm64"]`. Multi-platform builds use cross-compilation; the Dockerfile must use `FROM --platform=$BUILDPLATFORM` and `TARGETARCH`.
required:
- context
required:
- environment
- application
- build
Loading
Loading