projection¶
The Kubernetes CRDs for declarative resource mirroring across namespaces — any Kind, conflict-safe, watch-driven.
projection is a Kubernetes operator that mirrors any Kubernetes object — ConfigMap, Secret, Service, your custom resources — from a source location to a destination, declaratively, per resource. Each Projection (namespaced, single-target) or ClusterProjection (cluster-scoped, fan-out) is its own first-class object with status conditions, events, and Prometheus metrics you can alert on. Edits to the source propagate to the destination in roughly 100 milliseconds, and a manual kubectl delete of a destination self-heals on the next reconcile via destination watches.
It exists because every team eventually rebuilds this with a one-off controller or a Kyverno generate policy, and neither approach is the right shape. projection is meant to be the answer when somebody asks "how do you mirror a Secret across namespaces in this cluster?" — and to give namespace tenants a self-service path that doesn't need cluster-tier authority for the common case.
Why projection¶
| projection | emberstack/Reflector | Kyverno generate |
|
|---|---|---|---|
| Works on any Kind | yes | ConfigMap & Secret only | yes |
Source-of-truth in a CR you can kubectl get |
yes (Projection/ClusterProjection) |
no (annotations on the source) | no (cluster policy) |
| Per-resource status + Kubernetes Events | yes | partial | no |
| Conflict-safe (refuses to overwrite unowned objects) | yes | no | no |
| Watch-driven propagation (~100 ms) | yes | yes | yes |
Destination self-healing on manual kubectl delete |
yes | yes | yes |
| Admission-time validation of source fields | yes | n/a | yes |
| Cluster-scoped fan-out CR | yes (ClusterProjection) |
no | yes (cluster policy) |
Namespace-tier self-service via standard edit role |
yes (rbac.aggregate) |
no | no |
| Prometheus metrics per reconcile outcome | yes ({kind, result}) |
partial | yes |
| Footprint | two CRDs + Deployment | one CRD + Deployment | full policy engine |
See vs alternatives for the full comparison.
60-second demo¶
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: platform
annotations:
projection.sh/projectable: "true"
data:
log_level: info
---
apiVersion: projection.sh/v1
kind: Projection
metadata:
name: app-config-mirror
namespace: tenant-a
spec:
source:
kind: ConfigMap
namespace: platform
name: app-config
overlay:
labels:
projected-by: projection
$ kubectl get projections -n tenant-a
NAME KIND SOURCE-NAMESPACE SOURCE-NAME DESTINATION-NAME READY
app-config-mirror ConfigMap platform app-config app-config True
$ kubectl get configmap -n tenant-a app-config \
-o jsonpath='{.metadata.annotations.projection\.sh/owned-by-projection}'
tenant-a/app-config-mirror
- Edit the source — the destination updates within ~100 ms.
kubectl delete configmapthe destination —ensureDestWatchtriggers an immediate reconcile and recreates it.- Delete the
Projection— the destination is removed (only ifprojectionstill owns it). - Pre-existing object at the destination?
Ready=False reason=DestinationConflict. We don't overwrite strangers.
For fan-out across many namespaces (single source, multiple destinations), use ClusterProjection:
apiVersion: projection.sh/v1
kind: ClusterProjection
metadata:
name: app-config-fanout
spec:
source:
kind: ConfigMap
namespace: platform
name: app-config
destination:
namespaceSelector:
matchLabels:
projection.sh/mirror: "true"
Cluster admins bind the <release>-projection-cluster-admin ClusterRole explicitly to whoever should manage ClusterProjections; namespace tenants get Projection access automatically via the standard edit role aggregation.
Features at a glance¶
- Two CRDs, one operator —
Projection(namespaced, single-target) for tenant self-service;ClusterProjection(cluster-scoped, fan-out via list or selector) for cluster-tier mirroring. - Any Kind —
RESTMapper-driven GVR resolution. Sourceversionmay be omitted for any group so the projection follows version promotions automatically. - Watch-driven both ways — dynamic informer registration per source GVK, plus label-filtered watches on destinations so manual deletes self-heal.
- Conflict-safe —
projection.sh/owned-by-projection(or…-cluster-projection) annotations mark our destinations; the controller never overwrites a stranger-owned object. - Clean deletion — distinct finalizers per tier (
projection.sh/finalizerandprojection.sh/cluster-finalizer) clean up every owned destination across every namespace before the CR is removed. - Observable — three status conditions (
SourceResolved,DestinationWritten,Ready), Kubernetes Events for every state transition, andprojection_reconcile_total{kind,result}plusprojection_watched_gvks/projection_watched_dest_gvksgauges. - Validated at admission — source fields are pattern-validated and CEL-checked, so typos and shape errors fail at
kubectl apply, not at runtime. - Tenant-friendly RBAC — chart aggregates
ProjectionCRUD into the standardadmin/editroles by default (rbac.aggregate=true);ClusterProjectionaccess is gated separately and must be granted explicitly. - Smart copy — strips server-owned metadata, drops
.status, preserves apiserver-allocated fields likeService.spec.clusterIPon update. - Small — two CRDs, one Deployment, one container. Distroless image, multi-arch.
Next¶
Install it and create your first Projection: Getting started.