Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions packages/zarf-agent/chart/templates/clusterrole.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@ rules:
verbs:
- get
- list
- apiGroups:
- ""
resources:
- namespaces
verbs:
- get
- list
32 changes: 32 additions & 0 deletions site/src/content/docs/ref/init-package.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,38 @@ Additionally, when adopting resources, ensure that the namespaces specified are

The Agent does not need to create any secrets in the cluster. Instead, during `zarf init` and `zarf package deploy`, secrets are automatically created in a [Helm Postrender Hook](https://helm.sh/docs/topics/advanced/#post-rendering) for any namespaces Zarf sees. If you have resources managed by [Flux](https://fluxcd.io/) that are not in a namespace managed by Zarf, you can either create the secrets manually or include a manifest to create the namespace in your package and let Zarf create the secrets for you.

#### Conditional Image Mutation with `mutate-if-exists`

By default, the `zarf-agent` rewrites all container image references to point to the Zarf registry. However, in some scenarios you may want to conditionally mutate images—only rewriting images that already exist in the Zarf registry while leaving others pointing to their original locations.

To enable this behavior, label a namespace with `zarf.dev/agent: mutate-if-exists`:

```yaml
apiVersion: v1
kind: Namespace
metadata:
name: my-namespace
labels:
zarf.dev/agent: mutate-if-exists
```

When this label is applied:

- **Images found in the Zarf registry**: Image references are rewritten to point to the Zarf registry (e.g., `docker.io/nginx:latest` → `127.0.0.1:31999/library/nginx:latest-zarf-...`)
- **Images not found in the Zarf registry**: Image references remain unchanged, pointing to their original registry

This is useful for:

- **Hybrid deployments**: Running workloads that use a mix of air-gapped images (in Zarf registry) and external images (from internet-accessible registries)
- **Gradual migration**: Testing incremental migration to fully air-gapped deployments
- **Development environments**: Allowing developers to pull some images from external registries while using Zarf-managed images for core components

:::note

The registry existence check is performed via HTTP HEAD request to the Zarf registry when pods are created. If the registry is temporarily unavailable, pod creation will fail. Ensure your Zarf registry is healthy before deploying workloads to namespaces using `mutate-if-exists`.

:::

## Optional Components

The Zarf team maintains some optional components in the default 'init' package.
Expand Down
56 changes: 56 additions & 0 deletions src/internal/agent/hooks/pods.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ func mutatePod(ctx context.Context, r *v1.AdmissionRequest, cluster *cluster.Clu
// Pods do not have a metadata.name at the time of admission if from a deployment so we don't log the name
l.Info("using the Zarf registry URL to mutate the Pod", "registry", registryURL)

// Check if namespace wants mutate-if-exists behavior
useMutateIfExists, skipResult := checkNamespaceMutationBehavior(ctx, cluster, r.Namespace)
if skipResult != nil {
return skipResult, nil
}

var patches []operations.PatchOperation

// Add the zarf secret to the podspec
Expand All @@ -113,6 +119,17 @@ func mutatePod(ctx context.Context, r *v1.AdmissionRequest, cluster *cluster.Clu
if err != nil {
return nil, err
}
// Check if image exists if mutate-if-exists is enabled
if useMutateIfExists {
exists, checkErr := imageExistsInRegistry(ctx, cluster, replacement, &state.RegistryInfo)
if checkErr != nil {
return nil, fmt.Errorf("registry check failed for %s: %w", replacement, checkErr)
}
if !exists {
l.Info("image not found in registry, skipping mutation", "original", container.Image, "transformed", replacement)
continue
}
}
updatedAnnotations[getImageAnnotationKey(ctx, container.Name)] = container.Image
patches = append(patches, operations.ReplacePatchOperation(path, replacement))
}
Expand All @@ -124,6 +141,17 @@ func mutatePod(ctx context.Context, r *v1.AdmissionRequest, cluster *cluster.Clu
if err != nil {
return nil, err
}
// Check if image exists if mutate-if-exists is enabled
if useMutateIfExists {
exists, checkErr := imageExistsInRegistry(ctx, cluster, replacement, &state.RegistryInfo)
if checkErr != nil {
return nil, fmt.Errorf("registry check failed for %s: %w", replacement, checkErr)
}
if !exists {
l.Info("image not found in registry, skipping mutation", "original", container.Image, "transformed", replacement)
continue
}
}
updatedAnnotations[getImageAnnotationKey(ctx, container.Name)] = container.Image
patches = append(patches, operations.ReplacePatchOperation(path, replacement))
}
Expand All @@ -139,6 +167,17 @@ func mutatePod(ctx context.Context, r *v1.AdmissionRequest, cluster *cluster.Clu
if err != nil {
return nil, fmt.Errorf("failed to transform volume %q (index %d) image reference %q: %w", volume.Name, idx, volume.Image.Reference, err)
}
// Check if image exists if mutate-if-exists is enabled
if useMutateIfExists {
exists, checkErr := imageExistsInRegistry(ctx, cluster, replacement, &state.RegistryInfo)
if checkErr != nil {
return nil, fmt.Errorf("registry check failed for volume %q image %s: %w", volume.Name, replacement, checkErr)
}
if !exists {
l.Info("volume image not found in registry, skipping mutation", "volume", volume.Name, "original", volume.Image.Reference, "transformed", replacement)
continue
}
}
updatedAnnotations[getVolumeAnnotationKey(ctx, volume.Name)] = volume.Image.Reference
patches = append(patches, operations.ReplacePatchOperation(path, replacement))
}
Expand Down Expand Up @@ -183,6 +222,12 @@ func mutateEphemeralContainers(ctx context.Context, r *v1.AdmissionRequest, clus
// Pods do not have a metadata.name at the time of admission if from a deployment so we don't log the name
l.Info("using the Zarf registry URL to mutate the Pod", "registry", registryURL)

// Check if namespace wants mutate-if-exists behavior
useMutateIfExists, skipResult := checkNamespaceMutationBehavior(ctx, cluster, r.Namespace)
if skipResult != nil {
return skipResult, nil
}

updatedAnnotations := pod.Annotations
if updatedAnnotations == nil {
updatedAnnotations = make(map[string]string)
Expand All @@ -197,6 +242,17 @@ func mutateEphemeralContainers(ctx context.Context, r *v1.AdmissionRequest, clus
if err != nil {
return nil, err
}
// Check if image exists if mutate-if-exists is enabled
if useMutateIfExists {
exists, checkErr := imageExistsInRegistry(ctx, cluster, replacement, &state.RegistryInfo)
if checkErr != nil {
return nil, fmt.Errorf("registry check failed for %s: %w", replacement, checkErr)
}
if !exists {
l.Info("ephemeral container image not found in registry, skipping mutation", "original", container.Image, "transformed", replacement)
continue
}
}
updatedAnnotations[getImageAnnotationKey(ctx, container.Name)] = container.Image
patches = append(patches, operations.ReplacePatchOperation(path, replacement))
}
Expand Down
Loading