Skip to content

CRD reference

This is the hand-written reference for projection.be0x74a.io/v1 Projection. It mirrors the validation markers in api/v1/projection_types.go exactly.

  • API group: projection.be0x74a.io
  • API version: v1 (storage version; alpha stability — see Limitations)
  • Kind: Projection
  • Scope: Namespaced

spec.source (required)

Identifies the object to mirror. All four fields are required.

Field Type Required Default Validation Description
apiVersion string yes MinLength=1, pattern ^([a-z0-9.-]+/)?v[0-9]+((alpha|beta)[0-9]+)?$ e.g. v1, apps/v1, cert-manager.io/v1.
kind string yes MinLength=1, pattern ^[A-Z][A-Za-z0-9]*$ (PascalCase) e.g. ConfigMap, Secret, Certificate.
name string yes MinLength=1, MaxLength=63, pattern ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ (DNS-1123) Name of the source object.
namespace string yes MinLength=1, MaxLength=63, pattern ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ (DNS-1123) Namespace of the source object.

spec.destination (optional)

Where the mirrored object is written. All fields are optional; leaving them unset produces sensible defaults.

Field Type Required Default Validation Description
namespace string no The Projection's own namespace MaxLength=63, pattern ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ Single destination namespace. Mutually exclusive with namespaceSelector.
namespaceSelector metav1.LabelSelector no Standard label selector (matchLabels/matchExpressions) Fan-out: project into every namespace matching the selector. Mutually exclusive with namespace.
name string no spec.source.name MaxLength=63, pattern ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ Name at each destination.

Setting both namespace and namespaceSelector is rejected by the reconciler with DestinationWritten=False reason=InvalidSpec. (This invariant is enforced controller-side rather than via CEL for cross-apiserver-version compatibility.)

When namespaceSelector is set, the controller projects into every matching namespace and cleans up destinations in namespaces that stop matching (e.g. a label is removed). Deletion of the Projection cleans up every owned destination across all namespaces.

Note: the destination Kind is always the same as the source Kind — there is no transformation.

spec.overlay (optional)

Metadata applied on top of the copied source. Overlay entries win on key conflict with source metadata. The overlay never touches spec/data/etc.; it is metadata only.

Field Type Required Default Description
labels map[string]string no {} Merged with the source's metadata.labels. Overlay wins on conflict.
annotations map[string]string no {} Merged with the source's metadata.annotations. Overlay wins on conflict.

The controller always stamps projection.be0x74a.io/owned-by: <projection-ns>/<projection-name> on the destination, regardless of overlay. Do not attempt to set this key via overlay — it will be overwritten by the controller on every reconcile.

status.conditions

The standard metav1.Condition array. The controller maintains three condition types:

Type True reason(s) False reason(s) Unknown reason(s)
SourceResolved Resolved SourceResolutionFailed, SourceFetchFailed
DestinationWritten Projected DestinationCreateFailed, DestinationUpdateFailed, DestinationFetchFailed, DestinationConflict, NamespaceResolutionFailed, DestinationWriteFailed, InvalidSpec SourceNotResolved
Ready Projected Mirrors whichever of SourceResolved or DestinationWritten failed, with the same reason and message

DestinationWritten=Unknown reason=SourceNotResolved specifically means the source-side step failed, so a destination write was never attempted.

For selector-based Projections, DestinationWritten is a rollup across all matched namespaces. If every matched namespace succeeds, the condition is True. If any fail, it's False with a reason from the failure set (or the generic DestinationWriteFailed when reasons differ across namespaces); the message lists the failed namespaces. Per-namespace detail is surfaced via Events.

DestinationWritten=False reason=InvalidSpec means both namespace and namespaceSelector were set — fix the spec. DestinationWritten=False reason=NamespaceResolutionFailed means the label selector was malformed.

Printer columns

kubectl get projections (and -A) surfaces:

Column JSONPath
Kind .spec.source.kind
Source-Namespace .spec.source.namespace
Source-Name .spec.source.name
Destination .spec.destination.name
Ready .status.conditions[?(@.type=='Ready')].status
Age .metadata.creationTimestamp

Finalizers and annotations the controller manages

Name Where Purpose
projection.be0x74a.io/finalizer Projection.metadata Blocks deletion until the controller has cleaned up the destination object (if it still owns it).
projection.be0x74a.io/owned-by Destination annotations Marks the destination as owned by <projection-ns>/<projection-name>. Used for conflict detection on every reconcile and before destination cleanup.

Fully-spelled-out example

apiVersion: projection.be0x74a.io/v1
kind: Projection
metadata:
  name: app-config-to-tenant-a
  namespace: platform
spec:
  source:
    apiVersion: v1
    kind: ConfigMap
    name: app-config
    namespace: platform
  destination:
    namespace: tenant-a
    name: shared-app-config      # optional; defaults to source.name
  overlay:
    labels:
      tenant: tenant-a
      projected-by: projection
    annotations:
      mirror.example.com/source: platform/app-config
status:
  conditions:
    - type: SourceResolved
      status: "True"
      reason: Resolved
      message: ""
      lastTransitionTime: "2026-04-13T10:00:00Z"
    - type: DestinationWritten
      status: "True"
      reason: Projected
      message: ""
      lastTransitionTime: "2026-04-13T10:00:00Z"
    - type: Ready
      status: "True"
      reason: Projected
      message: ""
      lastTransitionTime: "2026-04-13T10:00:00Z"

Fan-out example (one source → many destinations)

apiVersion: projection.be0x74a.io/v1
kind: Projection
metadata:
  name: shared-config-fanout
  namespace: platform
spec:
  source:
    apiVersion: v1
    kind: ConfigMap
    name: shared-config
    namespace: platform
  destination:
    # namespace is omitted — namespaceSelector picks the destinations
    namespaceSelector:
      matchLabels:
        projection.be0x74a.io/mirror: "true"
  overlay:
    labels:
      projected-by: projection

Every namespace carrying the label projection.be0x74a.io/mirror=true gets a copy. Adding or removing the label on a namespace triggers a reconcile (via the controller's namespace watch) and the destination set adjusts automatically.