Sinker is a Kubernetes controller that copies resources, or selected fields, from a source object to a target object.
Each ResourceSync defines a one-way synchronization within one cluster or between clusters. Sinker watches both
objects and reapplies the desired target when they change.
- Copy whole resources or select fields with JSONPath source expressions and dotted destination paths.
- Connect either end to another cluster using a kubeconfig stored in a Kubernetes Secret.
- Share kubeconfig Secrets across namespaces with an explicit namespace access annotation.
- Store arbitrary structured data in
SinkerContainerresources for use as sources or targets. - Manage target cleanup through a finalizer and report reconciliation results in
ResourceSyncstatus.
- A Kubernetes cluster for the controller and its CRDs, with permission to install CRDs, RBAC, and a Deployment. The bundled schema uses CEL validation, and target writes use server-side apply. The repository selects Kubernetes 1.33 API bindings at build time; this is not a tested minimum cluster version.
kubectlwith Kustomize support.- The Rust toolchain selected by rust-toolchain.toml, currently
1.85.0, when building locally. - An OCI image builder and a registry accessible to the cluster when building your own container image.
- For remote clusters, network access from the controller and a usable kubeconfig for each connection.
git clone https://github.com/influxdata/sinker.git
cd sinker
cargo build --locked --releaseThe binary is target/release/sinker. To build and publish your own image, replace the registry and tag below:
docker build -t <registry>/sinker:<tag> .
docker push <registry>/sinker:<tag>The Dockerfile uses cargo-chef for build caching and a distroless Debian 12 runtime running as UID/GID
65532:65532. CI also publishes images; see Development for the publication workflow.
The bundled manifests install both CRDs, a namespace, RBAC, a ServiceAccount, and a single-replica Deployment. Customize deployment.yml, directly or through an overlay:
- Replace the
sinker:replace_meimage with an image you can pull. - Configure the
gar-auth-sinkerimage pull Secret, or remove/changeimagePullSecretsfor your registry. - Adjust resource requests and limits: the defaults request 2 CPUs and 3G memory, with an 8 CPU limit.
- Extend clusterrole.yml if you need additional resource kinds; see Permissions.
If the registry requires a pull Secret, create the namespace first:
kubectl apply -f manifests/namespace.yaml
kubectl -n sinker create secret docker-registry gar-auth-sinker \
--docker-server=<registry> \
--docker-username=<username> \
--docker-password=<password>After customizing the manifests, render, apply, and check the Deployment:
kubectl kustomize manifests
kubectl apply -k manifests
kubectl -n sinker rollout status deployment/sinker
kubectl -n sinker logs deployment/sinkerSinker watches ResourceSync objects in all namespaces. Keep one active controller per cluster: the implementation
has no leader election. The admin server listens on port 8080; see Status and observability.
Install the CRDs in the cluster selected by your kubeconfig, then run:
kubectl --kubeconfig /path/to/kubeconfig --context my-context apply -f manifests/crd.yml
kubectl --kubeconfig /path/to/kubeconfig --context my-context wait --for=condition=Established \
--timeout=60s crd/resourcesyncs.sinker.influxdata.io crd/sinkercontainers.sinker.influxdata.io
cargo run --locked -- --kubeconfig /path/to/kubeconfig --context my-contextUse a cluster without another active Sinker controller. Local execution needs the same API permissions as a deployed
controller. Credentials are loaded from --kubeconfig, then KUBECONFIG or ~/.kube/config; without explicit client
selection, a failed local configuration falls back to in-cluster credentials.
Run cargo run --locked -- --help for the full CLI. These options configure the controller's local connection and runtime:
| Option | Purpose / default |
|---|---|
--log-level / SINKER_LOG |
Tracing filter; sinker=info,warn. |
--log-format |
plain or json; defaults to plain. |
--kubeconfig, --context, --cluster, --user |
Select the local kubeconfig and its entries. |
--as, --as-group |
Set kubeconfig user/group impersonation. |
--kube-api-response-headers-timeout |
Local Kubernetes client response-header timeout; 9s. |
--admin-addr |
Admin HTTP bind address; 0.0.0.0:8080. |
Remote clients use the kubeconfigs referenced in each ResourceSync; local client flags do not override those connections.
Both CRDs use apiVersion: sinker.influxdata.io/v1alpha1 and are namespaced.
For a simple in-cluster sync, create a source ConfigMap:
kubectl -n default create configmap sinker-source --from-literal=message=helloSave this as resource-sync.yaml, then run kubectl apply -f resource-sync.yaml:
apiVersion: sinker.influxdata.io/v1alpha1
kind: ResourceSync
metadata:
name: demo
namespace: default
spec:
source:
resourceRef:
apiVersion: v1
kind: ConfigMap
name: sinker-source
target:
resourceRef:
apiVersion: v1
kind: ConfigMap
name: sinker-targetInspect the result with kubectl -n default get configmap sinker-target -o yaml and
kubectl -n default get resourcesync demo -o yaml. Changes to the source are copied to the target; external changes to
fields Sinker manages on the target trigger another apply. Deleting the source causes reconciliation errors and leaves
an existing target in place. Deleting the ResourceSync initiates target cleanup.
| Field | Required | Meaning |
|---|---|---|
spec.source.resourceRef |
Yes | Source apiVersion, kind, and name. |
spec.target.resourceRef |
Yes | Target apiVersion, kind, and name. |
spec.source.cluster |
No | Source kubeconfig reference and optional resource namespace override. |
spec.target.cluster |
No | Target kubeconfig reference and optional resource namespace override. |
spec.mappings |
No | Ordered field mappings. Omitted or [] copies the source's content, labels, and annotations. |
The checked-in CRD makes the entire spec immutable, including mappings. Replace the ResourceSync to change its
spec, accounting for target deletion when removing the old sync. This restriction is enforced by the installed schema;
the generator currently omits the rule. See Generating CRDs before updating or packaging schemas.
Without cluster, a namespaced source or target is resolved in the ResourceSync's namespace using the controller's
local client. resourceRef has no namespace field. To use a kubeconfig, add this under either source or target:
cluster:
namespace: workloads
kubeConfig:
secretRef:
name: remote-kubeconfig
namespace: cluster-credentials
key: valueThe two namespace fields have different meanings:
| Field | Location | Default when omitted |
|---|---|---|
cluster.kubeConfig.secretRef.namespace |
Namespace of the Secret in the controller's cluster. | ResourceSync namespace. |
cluster.namespace |
Namespace of the source/target in the referenced cluster. | Selected kubeconfig context's namespace, or default. |
Discovery determines whether a kind is namespaced or cluster-scoped. Cluster-scoped API requests use no resource namespace; see the local target ownership limitation before using such a target.
Create the kubeconfig Secret in an existing namespace. This example authorizes syncs in default to use credentials
stored in cluster-credentials:
kubectl -n cluster-credentials create secret generic remote-kubeconfig \
--from-file=value=/path/to/remote-kubeconfig
kubectl -n cluster-credentials annotate secret remote-kubeconfig \
'sinker.influxdata.io/allowed-namespaces=^default$'The sinker.influxdata.io/allowed-namespaces annotation belongs to the kubeconfig Secret. Its value is a Rust regular
expression matched against the ResourceSync namespace. Use anchors for exact matches, for example
^(team-a|team-b)$; matching is otherwise not anchored. Cross-namespace access is denied when the annotation is absent,
invalid, or does not match. Same-namespace Secret references do not require this annotation. A failed cross-namespace
Secret read also reports the generic namespace-restriction error.
The Secret key must contain a UTF-8 kubeconfig with a usable current context. Any referenced files or credential helper executables must be available inside the controller container; the supplied distroless image includes no cloud CLI helpers. A self-contained kubeconfig avoids dependencies on workstation files.
example.yaml shows remote-to-local field mapping. It requires the Secret k3-test-27-kubeconfig in
default and a remote ConfigMap/default/remote-demo containing data.remote and data.foo.
Sinker uses the controller's credentials for local resources and kubeconfig Secret reads, and the referenced kubeconfig's
identity for remote resources. It does not impersonate the creator of a ResourceSync. Grant the ability to create syncs
and authorize shared credentials according to the resources those identities can access.
The bundled ClusterRole supplies access to ResourceSync, SinkerContainer, ConfigMaps, and Secrets, plus read access to
Namespaces. Dynamic resource discovery does not grant access to other kinds: add RBAC for them on the relevant cluster.
Source operations require get and watch; targets require get, watch, patch for server-side apply, and delete
for cleanup. API discovery and remote server-version requests must also be allowed. The controller additionally needs
cluster-wide list/watch access to ResourceSync, status updates, and finalizer management, as provided by the bundle.
With no mappings, Sinker copies the source's non-metadata fields, labels, and annotations into a target with the requested
name and discovered type. It drops kubectl.kubernetes.io/last-applied-configuration and does not copy source UIDs,
resource versions, owner references, or finalizers.
With mappings, Sinker starts with an empty target and processes entries in order:
| Mapping | Behavior |
|---|---|
Both fromFieldPath and toFieldPath |
Select a source value and place it at the destination. |
Only toFieldPath |
Place the entire source object at the destination, such as spec in a SinkerContainer. |
Only fromFieldPath |
Treat the selected subtree as a Kubernetes object and replace the target template with it. The subtree must contain apiVersion and kind matching the target reference. |
| Neither field | Reconciliation error; use an empty mappings list for a whole-resource copy. The CRD does not reject an empty mapping entry. |
Source paths are JSONPath expressions without a leading $ or $.: Sinker prepends $. to nonempty paths.
For example, data.foo, spec.items[0], or metadata.annotations['example.com/key']. An omitted or empty
fromFieldPath selects the whole source. No match produces JSON null; multiple matches cause a reconciliation error.
Destination paths are dot-separated object keys, such as data.bar or metadata.labels. They create missing objects
but cannot traverse a scalar, index an array, or escape a dot within a key. To copy keys containing dots, map their whole
parent object, such as data or metadata.annotations. An empty toFieldPath is not a root replacement; omit it for that.
For example, to copy only the demo ConfigMap's data.message to data.copiedMessage, add this to the sync's initial spec:
mappings:
- fromFieldPath: data.message
toFieldPath: data.copiedMessageAll target writes use forced server-side apply with field manager sinker.influxdata.io. Sinker can take ownership of
conflicting fields on an existing target. Other fields follow Kubernetes apply and schema rules; a sync is not an exact
byte-for-byte clone. Sinker writes to the main resource endpoint, so mapping status.* does not update a separate
/status subresource.
SinkerContainer holds an arbitrary JSON object under .spec, with unknown fields preserved by its manual schema. It
has no separate reconciliation loop. Use it to collect selected data or hold an embedded Kubernetes object:
apiVersion: sinker.influxdata.io/v1alpha1
kind: SinkerContainer
metadata:
name: config-template
namespace: default
spec:
apiVersion: v1
kind: ConfigMap
data:
message: helloTo materialize this embedded ConfigMap, reference config-template as the source, a v1 ConfigMap as the target, and
use mappings: [{fromFieldPath: spec}]. Conversely, mappings: [{toFieldPath: spec}] stores an entire source object in
a target SinkerContainer.
Sinker adds sinker.influxdata.io/target to the ResourceSync before applying the target. On deletion, it requests target
deletion and waits until the target is absent before stopping the watches and removing its finalizer. It uses background
deletion when the target already has finalizers, and foreground deletion otherwise. A target's own finalizers can keep
cleanup pending. Existing targets adopted by a sync are subject to the same cleanup.
These annotations belong to the ResourceSync and are enabled only by the string "true":
| Annotation | Effect |
|---|---|
sinker.influxdata.io/disable-target-deletion: "true" |
Skip Sinker issuing the target delete request, stop watches, and remove Sinker's finalizer. |
sinker.influxdata.io/force-delete: "true" |
During deletion, remove Sinker's finalizer if constructing either source or target API fails, for example because credentials or a remote API are unavailable. It does not bypass errors from subsequent target GET/DELETE requests or a target's finalizers. |
Both source and target APIs are resolved before normal cleanup, even with disable-target-deletion. An unavailable
source cluster can therefore block cleanup too. Force deletion can leave a target behind for manual cleanup.
Local targets (target.cluster omitted) also receive an owner reference to the namespaced ResourceSync.
Disabling target deletion alone does not retain a local target: Kubernetes garbage collection can still delete it.
Retention must also account for Kubernetes orphaning behavior.
The controller currently adds this owner reference even to a local cluster-scoped target, which conflicts with Kubernetes
owner scope rules.
Targets configured through a kubeconfig receive no Sinker owner reference, even if that kubeconfig points at the local cluster.
status.conditions contains ResourceSyncFailing: "True" with an error message after failure, or "False" with reason
ResourceSyncSucceeded after successful reconciliation. It records observedGeneration and preserves lastTransitionTime
while the condition value stays the same, using the live status rather than the watch cache. A successful finalizer-only
initialization also reports "False", so inspect the target when verifying the first sync. Successful deletion cleanup
does not write status.
kubectl get resourcesyncs -A
kubectl -n default get resourcesync demo -o yaml
kubectl -n sinker logs deployment/sinker --tail=100
kubectl -n sinker port-forward deployment/sinker 8080:8080
# In another terminal:
curl http://localhost:8080/live
curl http://localhost:8080/readyThe admin server provides /live and /ready. Readiness reflects runtime initialization and shutdown, not the health
of individual syncs or remote clusters. There is currently no registered /metrics endpoint; enabling the dependency's
Prometheus feature does not configure an exporter in main.rs.
Reconciliation errors retry after five seconds; object watches reconnect with backoff. A healthy sync waits for events
rather than polling on a fixed interval. Metadata-only edits to ResourceSync and changes to kubeconfig Secrets are not
explicit reconciliation triggers. Existing watches keep their clients until they reconnect. A retry or subsequent object
event can pick up updated settings; restart the controller when credential or access changes need to take effect promptly.
For failures, check the condition message, referenced object names, resource and Secret namespaces, the Secret's selected key and access annotation, RBAC, and connectivity to both API servers. If startup logs say the CRD is not queryable, check CRD installation and list permissions. Debug logging includes source objects and mapped values, which can include Secret contents.
flowchart TD
RS[ResourceSync watch] --> R[Reconcile]
K[Kubeconfig Secrets in controller cluster] --> C[Resolve clients and discover resource APIs]
R --> C
C --> S[Read source]
S --> M[Clone content or apply mappings]
M --> T[Server-side apply target]
T --> W[Watch source and target]
W -->|External changes, deletion, or watch errors| R
R --> ST[Write ResourceSync condition]
C -->|ResourceSync is deleting| D[Clean up target and remove finalizer]
The controller filters ResourceSync events by generation to avoid loops from its own status writes. The watcher manager
keeps a watcher per resource reference and owning sync, for local and remote endpoints. Object watches use
metadata.name selectors and managed-field timestamps to suppress changes last attributed to sinker.influxdata.io;
events with unknown ownership trigger reconciliation. Shutdown cancels and joins the watchers.
sinker manifests emits only the two CRDs; it needs no cluster connection. Compare generated output in a temporary file
before replacing the checked-in schema:
cargo run --locked -- manifests > /tmp/sinker-crds.yaml
diff -u manifests/crd.yml /tmp/sinker-crds.yamlKnown drift: manifests/crd.yml includes the self == oldSelf validation on ResourceSync.spec;
resources.rs does not generate it. The comparison currently reports that rule missing. Replacing the
checked-in file with generated output would remove spec immutability. CI regenerates manifests/crd.yml and checks for a
clean diff, so this mismatch also affects the generation check. Preserve the rule when packaging until its generation is
reconciled with the manifest.
Use kubectl kustomize manifests to render the complete deployment bundle.
Sinker is a single Rust package with a binary and library. The CI workflow builds, checks formatting and CRD drift, runs tests, and runs Clippy. Equivalent local checks, with formatting kept read-only, are:
cargo build --locked
cargo fmt --check
cargo test --locked
cargo clippy --locked --all-targets --all-featuresTests are inline unit tests for mappings, status transitions, annotations, namespace access checks, and event filtering. They do not establish end-to-end behavior against a live Kubernetes API. Use the CRD comparison when changing resource definitions.
| Source | Read when changing |
|---|---|
| main.rs, lib.rs | CLI, runtime wiring, public modules, and shared errors. |
| resources.rs | Serialized API fields, annotations, and the manual SinkerContainer schema. |
| controller.rs | Reconciliation, server-side apply, status, ownership, and cleanup. |
| resource_extensions.rs | Client creation, namespace resolution, discovery, and kubeconfig Secret authorization. |
| mapping.rs | Source selection, destination construction, and metadata handling. |
| remote_watcher.rs, remote_watcher_manager.rs, filters.rs | Watch lifecycle, retries, and filtering of Sinker-generated events. |
| manifests/, Dockerfile, rust.yml | Deployment defaults, RBAC, packaging, and publication. |
On successful pushes to main, CI builds linux/amd64 and linux/arm64 images at
us-docker.pkg.dev/influxdb2-artifacts/tubernetes/sinker:<commit-sha>, updates the manifest image for publication, and
publishes a Flux OCI artifact under sinker-manifests in the same registry. This workflow requires the configured
registry credentials and Depot project; building your own image does not require access to that infrastructure.
Sinker is licensed under the MIT License.