From 2683b8d410405fe3b804af0b2a5d306bbe22f3a1 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sat, 6 Jun 2026 18:51:21 +0200 Subject: [PATCH 1/6] :wrench: build: wire all validating webhooks in Helm VWC Register sixteen admission paths matching config/webhook/manifests.yaml so Helm installs validate every Kollect CRD kind, not only Profile. --- .../validating-webhook-configuration.yaml | 30 +++++++++++--- ...validating_webhook_configuration_test.yaml | 40 +++++++++++++++++++ 2 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 charts/kollect/tests/validating_webhook_configuration_test.yaml diff --git a/charts/kollect/templates/validating-webhook-configuration.yaml b/charts/kollect/templates/validating-webhook-configuration.yaml index fe78b6f6..209a17e5 100644 --- a/charts/kollect/templates/validating-webhook-configuration.yaml +++ b/charts/kollect/templates/validating-webhook-configuration.yaml @@ -1,4 +1,22 @@ {{- if .Values.webhooks.enabled }} +{{- $hooks := list + (dict "name" "vkollectclusterdatabasesink.kb.io" "kind" "kollectclusterdatabasesink" "resource" "kollectclusterdatabasesinks") + (dict "name" "vkollectclustereventsink.kb.io" "kind" "kollectclustereventsink" "resource" "kollectclustereventsinks") + (dict "name" "vkollectclusterinventory.kb.io" "kind" "kollectclusterinventory" "resource" "kollectclusterinventories") + (dict "name" "vkollectclusterprofile.kb.io" "kind" "kollectclusterprofile" "resource" "kollectclusterprofiles") + (dict "name" "vkollectclusterscope.kb.io" "kind" "kollectclusterscope" "resource" "kollectclusterscopes") + (dict "name" "vkollectclustersnapshotsink.kb.io" "kind" "kollectclustersnapshotsink" "resource" "kollectclustersnapshotsinks") + (dict "name" "vkollectclustertarget.kb.io" "kind" "kollectclustertarget" "resource" "kollectclustertargets") + (dict "name" "vkollectconnectiontest.kb.io" "kind" "kollectconnectiontest" "resource" "kollectconnectiontests") + (dict "name" "vkollectdatabasesink.kb.io" "kind" "kollectdatabasesink" "resource" "kollectdatabasesinks") + (dict "name" "vkollecteventsink.kb.io" "kind" "kollecteventsink" "resource" "kollecteventsinks") + (dict "name" "vkollectinventory.kb.io" "kind" "kollectinventory" "resource" "kollectinventories") + (dict "name" "vkollectprofile.kb.io" "kind" "kollectprofile" "resource" "kollectprofiles") + (dict "name" "vkollectremotecluster.kb.io" "kind" "kollectremotecluster" "resource" "kollectremoteclusters") + (dict "name" "vkollectscope.kb.io" "kind" "kollectscope" "resource" "kollectscopes") + (dict "name" "vkollectsnapshotsink.kb.io" "kind" "kollectsnapshotsink" "resource" "kollectsnapshotsinks") + (dict "name" "vkollecttarget.kb.io" "kind" "kollecttarget" "resource" "kollecttargets") +-}} apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: @@ -10,18 +28,20 @@ metadata: cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "kollect.fullname" . }}-serving-cert {{- end }} webhooks: +{{- range $hooks }} - admissionReviewVersions: [v1] clientConfig: service: - name: {{ include "kollect.webhookServiceName" . }} - namespace: {{ .Release.Namespace }} - path: /validate-kollect-dev-v1alpha1-kollectprofile + name: {{ include "kollect.webhookServiceName" $ }} + namespace: {{ $.Release.Namespace }} + path: /validate-kollect-dev-v1alpha1-{{ .kind }} failurePolicy: Fail - name: vkollectprofile.kb.io + name: {{ .name }} rules: - apiGroups: [kollect.dev] apiVersions: [v1alpha1] operations: [CREATE, UPDATE] - resources: [kollectprofiles] + resources: [{{ .resource }}] sideEffects: None {{- end }} +{{- end }} diff --git a/charts/kollect/tests/validating_webhook_configuration_test.yaml b/charts/kollect/tests/validating_webhook_configuration_test.yaml new file mode 100644 index 00000000..4d02bbfe --- /dev/null +++ b/charts/kollect/tests/validating_webhook_configuration_test.yaml @@ -0,0 +1,40 @@ +suite: validating webhook configuration +templates: + - validating-webhook-configuration.yaml +tests: + - it: renders no VWC when webhooks.enabled is false + set: + webhooks: + enabled: false + asserts: + - hasDocuments: + count: 0 + - it: registers all sixteen validating webhooks when enabled + set: + webhooks: + enabled: true + certManager: + create: true + asserts: + - hasDocuments: + count: 1 + - isKind: + of: ValidatingWebhookConfiguration + - equal: + path: metadata.annotations["cert-manager.io/inject-ca-from"] + value: NAMESPACE/RELEASE-NAME-kollect-serving-cert + - lengthEqual: + path: webhooks + count: 16 + - equal: + path: webhooks[0].name + value: vkollectclusterdatabasesink.kb.io + - equal: + path: webhooks[0].clientConfig.service.path + value: /validate-kollect-dev-v1alpha1-kollectclusterdatabasesink + - equal: + path: webhooks[15].name + value: vkollecttarget.kb.io + - equal: + path: webhooks[15].rules[0].resources[0] + value: kollecttargets From 47d1b7ac24df4596885bb8d4086dd34dd68f965b Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sat, 6 Jun 2026 18:55:48 +0200 Subject: [PATCH 2/6] :page_facing_up: docs: refresh family sink refs and probe status Update QUICKSTART, examples, PLATFORM-DECISIONS, and ADR-0403 for family sink CRDs; mark GCS/NATS connection probes as shipped. --- docs/PLATFORM-DECISIONS.md | 23 +++--- docs/QUICKSTART.md | 34 +++++---- docs/adr/0403-connection-test.md | 40 ++++++----- docs/crds/kollecttarget.md | 4 +- docs/examples/connection-test.md | 95 ++++++++++++++++++++----- docs/examples/deployment-inventory.md | 70 ++++++++++-------- docs/examples/kollect-inventory-demo.md | 6 +- docs/index.md | 33 +++++---- docs/roadmap/planned-features.md | 7 +- 9 files changed, 207 insertions(+), 105 deletions(-) diff --git a/docs/PLATFORM-DECISIONS.md b/docs/PLATFORM-DECISIONS.md index ff2b898c..ae66216e 100644 --- a/docs/PLATFORM-DECISIONS.md +++ b/docs/PLATFORM-DECISIONS.md @@ -52,7 +52,7 @@ backward compatibility. Breaking changes are batched deliberately before a futur 6. Argo **`Application`** sample + **contract test** (contract test **first**; then samples) 7. Hub `mode: hub|spoke` + merge lib (`inprocess`); no hub CRD 8. **`KollectClusterTarget`** — API + webhook only until namespaced MVP proven; controller later -9. **Secondary watches** — Profile → Targets, Sink → Inventories (beta requirement) +9. **Secondary watches** — Profile → Targets, family Sink → Inventories (beta requirement) 10. **Generic CRD sample** — `cert-manager.io/Certificate` + contract test 11. **GitLab sink** — Phase 2 (custom CA via `tls.caSecretRef`; first enterprise presentation path) 12. **Hub ingest** — SAR **`create`** on `kollectremoteclusters` @@ -75,7 +75,7 @@ Sink/transport reframe — [ADR-0401](adr/0401-sink-taxonomy-state-vs-stream.md) | Topic | Decision | | --- | --- | -| Secondary watches | **Ship** — `KollectProfile` change enqueues referring Targets; `KollectSink` change enqueues Inventories with `sinkRefs` | +| Secondary watches | **Ship** — `KollectProfile` change enqueues referring Targets; family sink change enqueues Inventories with matching `snapshotSinkRefs`, `databaseSinkRefs`, or `eventSinkRefs` | | Generic CRD sample | **`cert-manager.io/Certificate`** — contract test first, then profile + target + walkthrough | | `KollectClusterTarget` controller | **Defer** — API + webhook + sample only until namespaced e2e solid | | `profileRef` (cluster target) | Resolves **`KollectProfile` in platform namespace** (Helm `platformNamespace`); `KollectClusterProfile` later | @@ -122,7 +122,7 @@ the corresponding code merges. | Inventory read SAR | **`get`** on `kollectinventories` in caller namespace; **`list`** for index | 1 | | Hub ingest SAR | **`create`** on **`kollectremoteclusters`** in hub namespace | 2 | | GitLab sink | **`type: gitlab`** backend + custom CA TLS; Phase 2 after Git path proven | 2 | -| Secondary watches | Profile → Targets; Sink → Inventories | 1 (beta) | +| Secondary watches | Profile → Targets; family Sink → Inventories | 1 (beta) | | TokenReview/SAR cache | **30s TTL** in-memory per `(token hash, verb, resource)`; flag + `disabled` for dev | 1 | | `maxExportBytes` | Global manager default (~**1.5 MiB**) + optional **`KollectInventory.spec.maxExportBytes`**; webhook rejects override > global cap | 1 | @@ -188,7 +188,9 @@ the corresponding code merges. flowchart TD subgraph teamNs [Team namespace] Prof[KollectProfile] - Sink[KollectSink] + Snap[KollectSnapshotSink] + Db[KollectDatabaseSink] + Ev[KollectEventSink] Scope[KollectScope] Tgt[KollectTarget] Inv[KollectInventory] @@ -199,15 +201,20 @@ flowchart TD Scope -.-> Tgt Scope -.-> Inv Tgt --> Inv - Inv --> Sink - ConnTest -.-> Sink - ConnTest -.-> Prof + Inv --> Snap + Inv --> Db + Inv --> Ev + ConnTest -.-> Snap + ConnTest -.-> Db + ConnTest -.-> Ev ``` | Kind | Scope | Notes | | --- | --- | --- | | `KollectProfile` | Namespace | Same-ns `profileRef` on Target | -| `KollectSink` | **Namespace** | Same-ns `sinkRefs` on Inventory | +| `KollectSnapshotSink` | **Namespace** | Same-ns `snapshotSinkRefs` on Inventory | +| `KollectDatabaseSink` | **Namespace** | Same-ns `databaseSinkRefs` on Inventory | +| `KollectEventSink` | **Namespace** | Same-ns `eventSinkRefs` on Inventory | | `KollectTarget` | Namespace | Default for `tenantMode`; same-ns `profileRef` | | `KollectClusterTarget` | **Cluster** | Platform operator; `namespaceSelector` + `KollectClusterProfile` ref | | `KollectInventory` | Namespace | Aggregates namespaced targets in namespace | diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 9741468f..d6abe860 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -103,9 +103,10 @@ live API access. | File | Kind | Role | | --- | --- | --- | | `kollect_v1alpha1_kollectprofile.yaml` | `KollectProfile` | Extract Deployment image + labels | -| `kollect_v1alpha1_kollectsink.yaml` | `KollectSink` | Git sink (example repo URL) | +| `kollect_v1alpha1_kollectdatabasesink.yaml` | `KollectDatabaseSink` | Postgres sink (portal SoR) | +| `kollect_v1alpha1_kollectsnapshotsink.yaml` | `KollectSnapshotSink` | Git snapshot sink (audit) | | `kollect_v1alpha1_kollecttarget.yaml` | `KollectTarget` | Collect Deployments using the profile | -| `kollect_v1alpha1_kollectinventory.yaml` | `KollectInventory` | Aggregate and export to the sink | +| `kollect_v1alpha1_kollectinventory.yaml` | `KollectInventory` | Aggregate and export to family sinks | Narrative walkthrough with expected behavior notes: [examples/deployment-inventory.md](examples/deployment-inventory.md). @@ -135,28 +136,37 @@ Set `watchMode: OptIn` to collect only explicitly `enabled` namespaces/resources Sample opt-in target: `config/samples/kollect_v1alpha1_kollecttarget_opt-in.yaml`. -### Connection test (sink) +### Connection test (family sinks) -Samples set `spec.connectionTest: true` on `KollectSink`. The operator probes Git/Postgres/Kafka -and sets **`ConnectionVerified`** ([ADR-0403](adr/0403-connection-test.md)). +Samples set `spec.connectionTest: true` on **`KollectDatabaseSink`** and **`KollectSnapshotSink`**. +The operator probes Postgres/Git (and other wired backends) and sets **`ConnectionVerified`** +([ADR-0403](adr/0403-connection-test.md), [ADR-0414](adr/0414-sink-family-crds.md)). ```sh -kubectl wait --for=condition=ConnectionVerified kollectsink/git-inventory \ +kubectl wait --for=condition=ConnectionVerified kollectdatabasesink/postgres-inventory-demo \ + -n default --timeout=60s +kubectl describe kollectdatabasesink postgres-inventory-demo -n default +``` + +Git snapshot sink: + +```sh +kubectl wait --for=condition=ConnectionVerified kollectsnapshotsink/git-inventory-demo \ -n default --timeout=60s -kubectl describe kollectsink git-inventory -n default ``` Re-test without editing spec: ```sh -kubectl annotate kollectsink git-inventory -n default kollect.dev/test-connection=true --overwrite +kubectl annotate kollectsnapshotsink git-inventory-demo -n default \ + kollect.dev/test-connection=true --overwrite ``` ### Optional: Git credentials -The sample Git sink references a placeholder repository. For real exports, create a Secret with -credentials and point `spec.secretRef` at it. Connection tests can pass without a writable remote; -export requires valid credentials and endpoint reachability. +The sample Git snapshot sink references a placeholder repository. For real exports, create a Secret +with credentials and point `spec.secretRef` at it. Connection tests can pass without a writable +remote; export requires valid credentials and endpoint reachability. ## Verify @@ -175,7 +185,7 @@ Postgres row counts, etc. — not full payloads; see [ADR-0103](adr/0103-etcd-li ### CR status ```sh -kubectl get kollectprofiles,kollectsinks,kollectinventories +kubectl get kprof,ksnap,kdb,kinv -A kubectl get kollecttargets -A kubectl describe kollectinventory -n default team-inventory ``` diff --git a/docs/adr/0403-connection-test.md b/docs/adr/0403-connection-test.md index c7675d12..c7bad272 100644 --- a/docs/adr/0403-connection-test.md +++ b/docs/adr/0403-connection-test.md @@ -12,11 +12,12 @@ mechanisms remain valid. Product requirements call for a **first-class connection test** with clear, discoverable feedback when sinks are misconfigured ([REQUIREMENTS.md](../REQUIREMENTS.md), [engineering guidelines](../development/guidelines.md)). -[ADR-0202](0202-static-vs-reconciled.md) originally assumed static `KollectSink` objects with **no +[ADR-0202](0202-static-vs-reconciled.md) originally assumed static sink config objects with **no controller**, probing via annotations and surfacing `SinkReachable` on reconciled -`KollectTarget` / `KollectInventory`. Implementation added a **minimal `KollectSink` reconciler** -that runs connectivity checks and writes `ConnectionVerified` on the sink -(`internal/controller/kollectsink_controller.go`). +`KollectTarget` / `KollectInventory`. Implementation added **minimal family sink reconcilers** +(`KollectSnapshotSink`, `KollectDatabaseSink`, `KollectEventSink`) that run connectivity checks +and write `ConnectionVerified` on the sink +(`internal/controller/family_sink_connection.go`). An alternative is a dedicated **`KollectConnectionTest`** CR (apply a test object, wait on status, garbage-collect). That pattern helps composite or cross-cluster probes but adds API surface, RBAC, @@ -27,7 +28,7 @@ webhooks, and orphan CR lifecycle. ### ~~Reject `KollectConnectionTest` CR~~ → **Accepted in ADR-0703** Add namespaced **`KollectConnectionTest`** for audited/CI/composite probes. Keep **declarative -spec** + **imperative annotation** on `KollectSink` for quick sink-only retests, plus pipeline +spec** + **imperative annotation** on family sink CRDs for quick sink-only retests, plus pipeline conditions on reconciled objects. ### Sink connectivity (implemented) @@ -39,16 +40,20 @@ conditions on reconciled objects. | **Annotation `kollect.dev/test-connection: "true"`** | One-shot re-test without editing spec | Probe uses the same TLS trust and secret resolution as export (`caBundle` / `caSecretRef`, -`secretRef`). Supported types today: +`secretRef`). Supported types today (per family CRD): -| `spec.type` | Probe wired | -| --- | --- | -| `git`, `gitlab`, `postgres`, `kafka`, `s3` | ✅ | -| `nats`, `gcs` | ⬜ not wired ([kollectsink.md](../crds/kollectsink.md)) | +| Family CRD | `spec.type` | Probe wired | +| --- | --- | --- | +| `KollectSnapshotSink` | `git`, `gitlab`, `s3`, `gcs` | ✅ | +| `KollectDatabaseSink` | `postgres` | ✅ | +| `KollectEventSink` | `kafka`, `nats` | ✅ | + +Stub backends (`azureblob`, `http`, `bigquery`) register in the sink registry but return *not +implemented* at probe/export time until shipped. Extend per sink as backends mature. -**Status on `KollectSink`:** +**Status on family sink CRDs:** | Condition | Meaning | | --- | --- | @@ -62,14 +67,14 @@ Extend per sink as backends mature. **`kubectl` example:** ```sh -kubectl wait --for=condition=ConnectionVerified kollectsink/git-inventory \ +kubectl wait --for=condition=ConnectionVerified kollectsnapshotsink/git-inventory-demo \ -n default --timeout=60s ``` Re-run without spec change: ```sh -kubectl annotate kollectsink git-inventory -n kollect-system \ +kubectl annotate kollectsnapshotsink git-inventory-demo -n default \ kollect.dev/test-connection=true --overwrite ``` @@ -82,8 +87,8 @@ End-to-end export health belongs on **reconciled** objects, not only the sink: | **`SinkReachable`** | `KollectInventory`, `KollectTarget` | Sink resolution (`ConnectionVerified` / sink found) before export; **`ExportSucceeded`** / **`ExportFailed`** after inventory export attempts. `Synced` remains the primary export condition per [ADR-0602](0602-error-taxonomy.md). | `KollectTarget` derives sink refs from **`KollectInventory` in the same namespace** (targets have no -direct `sinkRefs`). Inventory reconciler watches **`KollectSink`** status changes to requeue affected -inventories. +direct family sink refs). Inventory reconciler watches **family sink** status changes to requeue +affected inventories. Sink-only `ConnectionVerified` proves **credentials and network to the backend**; it does not prove the full collect → aggregate → export path. @@ -92,8 +97,9 @@ prove the full collect → aggregate → export path. `KollectProfile` and `KollectScope` remain **webhook-validated static config** (no controller). -`KollectSink` has a **narrow reconciler** whose sole job is connection test status — not -collection or export. This is an intentional exception to full static-config purity, documented in +Family sink CRDs (`KollectSnapshotSink`, `KollectDatabaseSink`, `KollectEventSink`) each have a +**narrow reconciler** whose sole job is connection test status — not collection or export. This is +an intentional exception to full static-config purity, documented in [ADR-0202](0202-static-vs-reconciled.md). ### `KollectConnectionTest` CR (ADR-0703) diff --git a/docs/crds/kollecttarget.md b/docs/crds/kollecttarget.md index 3915ab57..928fe82d 100644 --- a/docs/crds/kollecttarget.md +++ b/docs/crds/kollecttarget.md @@ -101,7 +101,7 @@ kubectl apply -f config/samples/kollect_v1alpha1_kollecttarget_helm-releases.yam | `Ready=True` | Collecting | Profile resolved; informer registered | None | | `Synced=True` | Healthy | `reason`: `Collecting` with item count | Monitor `status` message | | `Degraded=True` | Blocked | See `reason` below | Fix root cause; generation bump re-reconciles | -| `SinkReachable=True/False` | Export path | Namespace inventory sinks reachable | Fix [KollectSink](kollectsink.md) connection | +| `SinkReachable=True/False` | Export path | Namespace inventory family sinks reachable | Fix [KollectSnapshotSink](kollectsnapshotsink.md), [KollectDatabaseSink](kollectdatabasesink.md), or [KollectEventSink](kollecteventsink.md) connection | ### Common `Degraded` reasons @@ -122,7 +122,7 @@ kubectl apply -f config/samples/kollect_v1alpha1_kollecttarget_helm-releases.yam | Actor | Verbs | Resource | Notes | | --- | --- | --- | --- | | Team engineers | `get`, `list`, `watch`, `create`, `update`, `patch`, `delete` | `kollecttargets` | Tenant namespace | -| Operator | `get`, `list`, `watch` | `kollecttargets`, `kollectprofiles`, `kollectscopes`, `kollectsinks` | Reconcile | +| Operator | `get`, `list`, `watch` | `kollecttargets`, `kollectprofiles`, `kollectscopes`, `kollectsnapshotsinks`, `kollectdatabasesinks`, `kollecteventsinks` | Reconcile | | Operator | `get`, `list`, `watch` | Target GVK resources | Dynamic — per profile (e.g. `deployments`) | | Operator | `update`, `patch` | `kollecttargets/status` | Write conditions | diff --git a/docs/examples/connection-test.md b/docs/examples/connection-test.md index 1c3ce63d..1f5094f0 100644 --- a/docs/examples/connection-test.md +++ b/docs/examples/connection-test.md @@ -1,67 +1,128 @@ # Example: Connection test !!! tip "Opt out in production" - `spec.connectionTest` defaults to **true**. Set `connectionTest: false` when automatic probes on - every spec change are undesirable. Use the `kollect.dev/test-connection` annotation or a - `KollectConnectionTest` CR for on-demand probes. + `spec.connectionTest` defaults to **true** on family sink CRDs. Set `connectionTest: false` when + automatic probes on every spec change are undesirable. Use the `kollect.dev/test-connection` + annotation or a `KollectConnectionTest` CR for on-demand probes. Kollect offers **three** ways to verify sink connectivity before relying on export -([ADR-0403](../adr/0403-connection-test.md), [ADR-0703](../adr/0703-platform-architecture-pivot.md)). +([ADR-0403](../adr/0403-connection-test.md), [ADR-0414](../adr/0414-sink-family-crds.md), +[ADR-0703](../adr/0703-platform-architecture-pivot.md)). ## Overview | Mechanism | Best for | Writes to | | --- | --- | --- | -| `spec.connectionTest` (default **true**) | Automatic probe on create/update | `KollectSink.status` | -| `kollect.dev/test-connection` annotation | On-demand prod re-test | `KollectSink.status` | +| `spec.connectionTest` (default **true**) | Automatic probe on create/update | Family sink `status` | +| `kollect.dev/test-connection` annotation | On-demand prod re-test | Family sink `status` | | `KollectConnectionTest` CR | Audited / CI pipelines | `KollectConnectionTest.status` | **Default:** `spec.connectionTest: true` (CRD OpenAPI default). Set `false` to opt out. +Family sink CRDs ([ADR-0414](../adr/0414-sink-family-crds.md)): + +| CRD | Role | Wired probe types | +| --- | --- | --- | +| `KollectSnapshotSink` | Snapshot store | `git`, `gitlab`, `s3`, `gcs` | +| `KollectDatabaseSink` | Relational SoR | `postgres` | +| `KollectEventSink` | Event emitter | `kafka`, `nats` | + +Stub backends (`azureblob`, `http`, `bigquery`) pass admission but return *not implemented* at probe +time until shipped. + ## Sink probe — `spec.connectionTest` -`config/samples/kollect_v1alpha1_kollectsink_postgres.yaml` sets `connectionTest: true`. +`config/samples/kollect_v1alpha1_kollectdatabasesink.yaml` sets `connectionTest: true`. + +```sh +kubectl apply -f config/samples/kollect_v1alpha1_kollectdatabasesink.yaml +kubectl wait --for=condition=ConnectionVerified kollectdatabasesink/postgres-inventory-demo \ + -n default --timeout=60s +kubectl describe kdb postgres-inventory-demo -n default +``` + +Git snapshot sink sample (`config/samples/kollect_v1alpha1_kollectsnapshotsink.yaml`): ```sh -kubectl wait --for=condition=ConnectionVerified kollectsink/postgres-inventory-demo \ +kubectl wait --for=condition=ConnectionVerified kollectsnapshotsink/git-inventory-demo \ -n default --timeout=60s ``` -Supported probe types: `git`, `gitlab`, `postgres`, `kafka`, `s3`. +Each family has a **narrow reconciler** whose sole job is connection-test status — not collection or +export ([ADR-0403](../adr/0403-connection-test.md)). ## Annotation re-test +Trigger a one-shot probe without editing `spec`: + +```sh +kubectl annotate kollectdatabasesink postgres-inventory-demo -n default \ + kollect.dev/test-connection=true --overwrite +``` + ```sh -kubectl annotate kollectsink postgres-inventory-demo -n default \ +kubectl annotate kollectsnapshotsink git-inventory-demo -n default \ kollect.dev/test-connection=true --overwrite ``` +When `spec.connectionTest: false`, the annotation is the supported way to re-probe in production. +After a successful annotation-only probe, the reconciler clears the annotation (kept when +`spec.connectionTest: true`). + ## KollectConnectionTest CR -`config/samples/kollect_v1alpha1_kollectconnectiontest.yaml`: +`config/samples/kollect_v1alpha1_kollectconnectiontest.yaml` — `spec.sinkRef` names exactly one +family sink: ```yaml spec: - sinkRef: postgres-inventory-demo + sinkRef: + databaseSinkRef: postgres-inventory-demo ownerSink: true ``` +Snapshot or event probes use `snapshotSinkRef` or `eventSinkRef` instead. + ```sh kubectl apply -f config/samples/kollect_v1alpha1_kollectconnectiontest.yaml kubectl wait --for=condition=ConnectionVerified kollectconnectiontest/postgres-sink-probe \ -n default --timeout=120s +kubectl get kconntest postgres-sink-probe -n default -o wide ``` -Default `spec.ttlSecondsAfterFinished`: **300** (CR auto-deleted after probe). +Default `spec.ttlSecondsAfterFinished`: **300** (CR auto-deleted after probe completes + TTL). + +Re-run after fixing credentials: delete and re-apply the CR, or patch `spec` to bump generation. + +## Status conditions + +On family sink CRDs: + +| Condition | Meaning | +| --- | --- | +| **`ConnectionVerified`** `True` | Last probe succeeded | +| **`ConnectionVerified`** `False` | Probe failed (`ConnectionTestFailed`, `SecretResolveFailed`, …) | +| **`Degraded`** `True` | Set alongside failed probe | +| **`TLSInsecure`** `True` | `insecureSkipVerify` enabled (dev warning) | + +Operator metric: `kollect_sink_connection_test_total{type,result}`. ## Pipeline reachability -`ConnectionVerified` proves backend credentials/network. `SinkReachable` and `Synced` on -`KollectInventory` prove export path health — see -[Deployment inventory](deployment-inventory.md). +`ConnectionVerified` on a family sink proves **credentials and network to the backend**. End-to-end +export health belongs on reconciled objects: + +| Condition | Object | Meaning | +| --- | --- | --- | +| **`SinkReachable`** | `KollectInventory`, `KollectTarget` | Family sink resolution before export | +| **`Synced`** | `KollectInventory` | Export succeeded for a sink ref | + +See [Deployment inventory](deployment-inventory.md) for the full Profile → Target → Inventory → +family sink path. ## Related - [KollectConnectionTest](../crds/kollectconnectiontest.md) +- [KollectSnapshotSink](../crds/kollectsnapshotsink.md) · [KollectDatabaseSink](../crds/kollectdatabasesink.md) · [KollectEventSink](../crds/kollecteventsink.md) - [Postgres state store](postgres-state-store.md) -- [ADR-0403](../adr/0403-connection-test.md) +- [ADR-0403](../adr/0403-connection-test.md) · [ADR-0414](../adr/0414-sink-family-crds.md) diff --git a/docs/examples/deployment-inventory.md b/docs/examples/deployment-inventory.md index 794676f7..36d40f8b 100644 --- a/docs/examples/deployment-inventory.md +++ b/docs/examples/deployment-inventory.md @@ -5,10 +5,11 @@ [Kind local lab](kind-local-lab.md)) and can apply manifests from `config/samples/`. The default path uses a Postgres sink; swap to Git for audit-only workflows. -This walkthrough connects the four core **namespaced** CRDs into a minimal pipeline: define **what** -to extract (`KollectProfile`), **where** to send it (`KollectSink`), **which** resources to watch -(`KollectTarget`), and **when** to aggregate and export (`KollectInventory`). There is **no -`KollectHub` CRD** — hub aggregation uses Helm `mode: hub` ([ADR-0703](../adr/0703-platform-architecture-pivot.md)). +This walkthrough connects the core **namespaced** CRDs into a minimal pipeline: define **what** +to extract (`KollectProfile`), **where** to send it (family sinks — `KollectDatabaseSink`, +`KollectSnapshotSink`), **which** resources to watch (`KollectTarget`), and **when** to aggregate +and export (`KollectInventory`). There is **no `KollectHub` CRD** — hub aggregation uses Helm +`mode: hub` ([ADR-0703](../adr/0703-platform-architecture-pivot.md)). **Default sample path:** Postgres state store (`postgres-inventory-demo`). Swap to `git-inventory-demo` for Git audit/CI. See [Postgres state store](postgres-state-store.md) and @@ -23,14 +24,17 @@ flowchart LR Profile[KollectProfile
Deployment schema] Target[KollectTarget
select Deployments] Inv[KollectInventory
aggregate + export] - Sink[KollectSink
Postgres or Git] + Db[KollectDatabaseSink
Postgres] + Snap[KollectSnapshotSink
Git] K8s[(Kubernetes API)] Profile --> Target Target --> K8s Target --> Inv - Inv --> Sink - Sink --> Store[(Postgres / Git)] + Inv --> Db + Inv --> Snap + Db --> PG[(Postgres)] + Snap --> Git[(Git repo)] ``` ## Scale @@ -95,20 +99,21 @@ Set `type: list` on multi-value attributes. CEL equivalent: See [DATA-FLOWS.md](../DATA-FLOWS.md#3-attribute-extraction-jsonpath-arrays) and [ADR-0302](../adr/0302-cel-jsonpath-extraction.md). -## Step 2 — KollectSink +## Step 2 — Family sinks !!! warning "Same-namespace sink refs" - `KollectSink` is **namespaced** — create sinks in the same namespace as `KollectInventory` - `sinkRefs`. Cross-namespace references fail admission with `SinkNotFound` - ([ADR-0703](../adr/0703-platform-architecture-pivot.md)). + Family sink CRDs are **namespaced** — create sinks in the same namespace as + `KollectInventory` family ref lists (`snapshotSinkRefs`, `databaseSinkRefs`, `eventSinkRefs`). + Cross-namespace references fail admission with `SinkNotFound` + ([ADR-0414](../adr/0414-sink-family-crds.md)). -### Postgres (default sample) +### KollectDatabaseSink (default sample — Postgres) -`config/samples/kollect_v1alpha1_kollectsink_postgres.yaml` +`config/samples/kollect_v1alpha1_kollectdatabasesink.yaml` ```yaml apiVersion: kollect.dev/v1alpha1 -kind: KollectSink +kind: KollectDatabaseSink metadata: name: postgres-inventory-demo namespace: default @@ -126,13 +131,13 @@ spec: Create the DSN secret before export — see [Postgres state store](postgres-state-store.md). -### Git (audit / CI) +### KollectSnapshotSink (Git audit / CI) -`config/samples/kollect_v1alpha1_kollectsink.yaml` +`config/samples/kollect_v1alpha1_kollectsnapshotsink.yaml` ```yaml apiVersion: kollect.dev/v1alpha1 -kind: KollectSink +kind: KollectSnapshotSink metadata: name: git-inventory-demo namespace: default @@ -140,16 +145,19 @@ spec: type: git endpoint: https://github.com/konih/kollect-inventory-demo.git connectionTest: true + git: + branch: main + pushPolicy: Commit + commitMessage: "chore(inventory): export {namespace}/{name}" # secretRef: # name: git-push-credentials # namespace: kollect-system ``` -**Behavior:** the inventory controller resolves sinks via the registry (`git`, `postgres`, `kafka`, -`gitlab`, `s3`, `gcs`). With `connectionTest: true`, the operator probes on create/update and sets -`ConnectionVerified` on the sink. Export commits deterministic JSON snapshots to Git or upserts rows -to Postgres; status stores summary refs (commit SHA), not the full payload -([ADR-0103](../adr/0103-etcd-limit.md)). +**Behavior:** the inventory controller resolves family sinks via the registry. With +`connectionTest: true`, family sink reconcilers probe on create/update and set `ConnectionVerified`. +Export commits deterministic JSON snapshots to Git or upserts rows to Postgres; status stores +summary refs (commit SHA), not the full payload ([ADR-0103](../adr/0103-etcd-limit.md)). Production installs should set `connectionTest: false` (chart default) and re-probe on demand — see [Connection test](connection-test.md). @@ -207,7 +215,7 @@ kubectl label deployment nginx app.kubernetes.io/name=nginx --overwrite ## Step 4 — KollectInventory -Namespaced aggregator (same namespace as targets) referencing one or more sinks. +Namespaced aggregator (same namespace as targets) referencing family sinks. `config/samples/kollect_v1alpha1_kollectinventory.yaml` @@ -219,8 +227,9 @@ metadata: namespace: default spec: exportMinInterval: 30s - sinkRefs: + databaseSinkRefs: - postgres-inventory-demo + snapshotSinkRefs: - name: git-inventory-demo exportMinInterval: 1h suspend: false @@ -232,7 +241,7 @@ spec: ref → sink default → inventory default → scope floor — see [ADR-0413](../adr/0413-export-interval-scheduling.md). -For Git-only audit, use a single string ref: `sinkRefs: [git-inventory-demo]`. +For Git-only audit, use a single snapshot ref: `snapshotSinkRefs: [git-inventory-demo]`. **Behavior:** @@ -246,13 +255,13 @@ For Git-only audit, use a single string ref: `sinkRefs: [git-inventory-demo]`. ```sh kubectl apply -k config/samples/ -kubectl get kprof,ksink,ktgt,kinv -A +kubectl get kinv,ktgt,ksnap,kdb -A ``` Verify sink connectivity before relying on export: ```sh -kubectl wait --for=condition=ConnectionVerified kollectsink/postgres-inventory-demo \ +kubectl wait --for=condition=ConnectionVerified kollectdatabasesink/postgres-inventory-demo \ -n default --timeout=60s kubectl describe kollectinventory team-inventory -n default ``` @@ -268,8 +277,8 @@ kubectl describe kollectinventory team-inventory -n default | --- | --- | | Target not found | `KollectTarget` is namespaced — ensure namespace matches | | Profile not found | `profileRef` must name a `KollectProfile` in the **same namespace** as the Target | -| Sink not found | `sinkRefs` must name a `KollectSink` in the **same namespace** as the Inventory | -| No export | Missing DSN/`secretRef`, `ConnectionVerified=False`, or `SinkReachable=False` with reason `SinkNotFound` / `SinkUnreachable` — see `kubectl describe kollectsink` and inventory `status.conditions` | +| Sink not found | Family ref must name a sink in the **same namespace** as the Inventory (`snapshotSinkRefs`, `databaseSinkRefs`, or `eventSinkRefs`) | +| No export | Missing DSN/`secretRef`, `ConnectionVerified=False`, or `SinkReachable=False` with reason `SinkNotFound` / `SinkUnreachable` — see `kubectl describe kollectdatabasesink` / `kollectsnapshotsink` and inventory `status.conditions` | | Empty item count | No Deployments match selector, or target suspended / scope denied | | Namespace skipped | `kollect.dev/namespace-watch: disabled` or `watchMode: OptIn` without `enabled` label | @@ -281,7 +290,8 @@ See [Kind local lab](kind-local-lab.md), [QUICKSTART.md](../QUICKSTART.md), and - [Spoke cluster inventory](spoke-cluster-inventory.md) — Helm `mode: single` install narrative - [Postgres state store](postgres-state-store.md) — DSN secret and delete reconciliation - [Connection test](connection-test.md) — `ConnectionVerified` and `KollectConnectionTest` -- [KollectProfile](../crds/kollectprofile.md) · [KollectSink](../crds/kollectsink.md) · +- [KollectProfile](../crds/kollectprofile.md) · [KollectSnapshotSink](../crds/kollectsnapshotsink.md) · + [KollectDatabaseSink](../crds/kollectdatabasesink.md) · [KollectEventSink](../crds/kollecteventsink.md) · [KollectTarget](../crds/kollecttarget.md) · [KollectInventory](../crds/kollectinventory.md) - [CR reference](../CR-REFERENCE.md) - [ADR-0703: Platform architecture pivot](../adr/0703-platform-architecture-pivot.md) diff --git a/docs/examples/kollect-inventory-demo.md b/docs/examples/kollect-inventory-demo.md index 77e4345a..83f62c97 100644 --- a/docs/examples/kollect-inventory-demo.md +++ b/docs/examples/kollect-inventory-demo.md @@ -46,11 +46,11 @@ kubectl apply -f config/samples/kollect_v1alpha1_kollectprofile.yaml kubectl apply -f config/samples/kollect_v1alpha1_kollecttarget.yaml ``` -Git sink + inventory (same namespace as `sinkRefs`): +Git snapshot sink + inventory (same namespace as family sink refs): ```yaml apiVersion: kollect.dev/v1alpha1 -kind: KollectSink +kind: KollectSnapshotSink metadata: name: git-inventory-demo namespace: default @@ -68,7 +68,7 @@ metadata: name: team-inventory namespace: default spec: - sinkRefs: + snapshotSinkRefs: - git-inventory-demo suspend: false ``` diff --git a/docs/index.md b/docs/index.md index daf81321..625e4565 100644 --- a/docs/index.md +++ b/docs/index.md @@ -70,8 +70,9 @@ documented in [ADR-0401](adr/0401-sink-taxonomy-state-vs-stream.md), not repeate ## The resource model A pipeline is just a handful of Kubernetes resources: **config you declare** (`KollectProfile`, -`KollectSink`, `KollectScope`) and **objects the operator reconciles** (`KollectTarget`, -`KollectInventory`). Cluster-scoped `KollectCluster*` variants add cross-namespace rollup. +family sinks — `KollectSnapshotSink`, `KollectDatabaseSink`, `KollectEventSink`, `KollectScope`) +and **objects the operator reconciles** (`KollectTarget`, `KollectInventory`). Cluster-scoped +`KollectCluster*` variants add cross-namespace rollup. ```mermaid flowchart LR @@ -81,7 +82,9 @@ flowchart LR direction TB Profile["KollectProfile
what to extract"] Scope["KollectScope
guardrails"] - Sink["KollectSink
where to send"] + Snap["KollectSnapshotSink
snapshot store"] + Db["KollectDatabaseSink
relational SoR"] + Ev["KollectEventSink
event emitter"] end subgraph run["Operator reconciles"] @@ -92,9 +95,9 @@ flowchart LR subgraph out["Sink projections — choose any"] direction TB - Snap["Git · GitLab · S3 · GCS
snapshot store"] + SnapOut["Git · GitLab · S3 · GCS
snapshot store"] Rel["Postgres
relational SoR"] - Evt["NATS · Kafka
event emitter"] + EvtOut["NATS · Kafka
event emitter"] end K8s -- "informer per GVK" --> Target @@ -102,27 +105,31 @@ flowchart LR Target --> Inv Scope -. gates .-> Target Scope -. gates .-> Inv - Inv --> Sink - Sink --> Snap - Sink --> Rel - Sink --> Evt + Inv --> Snap + Inv --> Db + Inv --> Ev + Snap --> SnapOut + Db --> Rel + Ev --> EvtOut classDef api fill:#1F2937,stroke:#6B7280,color:#fff; classDef config fill:#326CE5,stroke:#1b3a8c,color:#fff; classDef work fill:#18B6A3,stroke:#0e6f63,color:#fff; classDef proj fill:#7FB3FF,stroke:#326CE5,color:#081A4B; - class Profile,Scope,Sink config; + class Profile,Scope,Snap,Db,Ev config; class Target,Inv work; - class Snap,Rel,Evt proj; + class SnapOut,Rel,EvtOut proj; ``` | Kind | You set | Role | | --- | --- | --- | | `KollectProfile` | GVK + CEL / JSONPath attributes | **What to extract** from each object | | `KollectTarget` | selectors + `profileRef` | **What to watch** and collect | -| `KollectInventory` | `sinkRefs` + cadence | **Aggregate, debounce, and export** | -| `KollectSink` | type + endpoint + `secretRef` | **Where to send** (Git, Postgres, Kafka, …) | +| `KollectInventory` | family sink refs + cadence | **Aggregate, debounce, and export** | +| `KollectSnapshotSink` | type + endpoint + `secretRef` | **Snapshot store** (Git, S3, GCS, …) | +| `KollectDatabaseSink` | type + credentials | **Relational SoR** (Postgres, …) | +| `KollectEventSink` | type + brokers | **Event emitter** (NATS, Kafka) | | `KollectScope` | allowed GVKs / namespaces / sinks | **Guardrails** for the team namespace | Full fields: [CR reference](CR-REFERENCE.md) · model rationale: [ADR-0201](adr/0201-crd-model.md). diff --git a/docs/roadmap/planned-features.md b/docs/roadmap/planned-features.md index 0eeb4525..b6a52880 100644 --- a/docs/roadmap/planned-features.md +++ b/docs/roadmap/planned-features.md @@ -106,9 +106,10 @@ phased ⬜ backlog, and items that need a design pass before implementation. For | | | | --- | --- | -| **Status** | Planned | -| **Summary** | Wire **`KollectConnectionTest`** and sink annotation probes for **`gcs`** and **`nats`** sink types (Git, Postgres, Kafka, S3, GitLab probes exist; GCS/NATS marked ⬜ in CR reference). | -| **Related ADRs** | [ADR-0403](../adr/0403-connection-test.md) · [kollectsink CR reference](../crds/kollectsink.md) | +| **Status** | Shipped (Phase 1) | +| **Roadmap** | Phase 1 ✅ | +| **Summary** | **`KollectConnectionTest`** and family sink annotation probes for **`gcs`** (`KollectSnapshotSink`) and **`nats`** (`KollectEventSink`) — alongside Git, Postgres, Kafka, S3, and GitLab probes. | +| **Related ADRs** | [ADR-0403](../adr/0403-connection-test.md) · [ADR-0414](../adr/0414-sink-family-crds.md) · [KollectSnapshotSink](../crds/kollectsnapshotsink.md) · [KollectEventSink](../crds/kollecteventsink.md) | --- From 9ada848d4c3e6cb76434541bd383a07e4ee15d51 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sat, 6 Jun 2026 18:56:46 +0200 Subject: [PATCH 3/6] :wrench: build(docker): add git and openssh to runtime image Switch runtime from distroless/static to Debian bookworm-slim with git and openssh-client so spec.git.engine: cli and git ls-remote probes work in the default image. go-git (default engine) is unchanged. --- .github/release-notes-install.md | 2 +- Dockerfile | 15 +++++++++++---- SECURITY.md | 2 +- charts/kollect/README.md | 7 +++++++ docs/ASSURANCE-CASE.md | 2 +- docs/OPERATOR-MANUAL.md | 13 +++++++++++++ docs/adr/0101-kubebuilder-v4.md | 2 +- docs/adr/0705-release-supply-chain.md | 2 +- docs/crds/kollectsnapshotsink.md | 1 + docs/development/coding-standards.md | 6 ++++-- docs/development/guidelines.md | 2 +- 11 files changed, 42 insertions(+), 12 deletions(-) diff --git a/.github/release-notes-install.md b/.github/release-notes-install.md index 0e23b697..b8245233 100644 --- a/.github/release-notes-install.md +++ b/.github/release-notes-install.md @@ -4,7 +4,7 @@ ${IMAGE_REPO}:${VERSION} ``` -Multi-arch (`linux/amd64`, `linux/arm64`), distroless nonroot base. +Multi-arch (`linux/amd64`, `linux/arm64`), Debian bookworm-slim nonroot base (includes `git` and `openssh-client` for `spec.git.engine: cli`). OCI attestations (SBOM + SLSA provenance) are attached in GHCR and on the repository [Attestations](https://github.com/${GITHUB_REPOSITORY}/attestations) page. Verify the signature: diff --git a/Dockerfile b/Dockerfile index 9c21bfc5..da4ff7df 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,11 +21,18 @@ COPY . . # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -ldflags="-w -s" -a -o manager cmd/main.go -# Use distroless as minimal base image to package the manager binary -# Refer to https://github.com/GoogleContainerTools/distroless for more details -FROM gcr.io/distroless/static:nonroot@sha256:963fa6c544fe5ce420f1f54fb88b6fb01479f054c8056d0f74cc2c6000df5240 +# Runtime image: Debian slim with git + openssh-client for spec.git.engine=cli and git ls-remote probes. +# go-git export (default engine) does not require the git binary; the CLI path and connection probes do. +FROM debian:bookworm-slim@sha256:0104b334637a5f19aa9c983a91b54c89887c0984081f2068983107a6f6c21eeb + +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates git openssh-client && \ + rm -rf /var/lib/apt/lists/* && \ + groupadd --gid 65532 nonroot && \ + useradd --uid 65532 --gid 65532 --home-dir /home/nonroot --shell /usr/sbin/nologin --no-create-home nonroot + WORKDIR / -COPY --from=builder /workspace/manager . +COPY --from=builder /workspace/manager /manager USER 65532:65532 ENTRYPOINT ["/manager"] diff --git a/SECURITY.md b/SECURITY.md index d407f6ad..fe647b15 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -35,7 +35,7 @@ Risks to consider when deploying: - Sink endpoints must use verified TLS; credentials must not appear in CR specs or logs. - Restrict egress with `NetworkPolicy` in production. -See [engineering guidelines](docs/development/guidelines.md) for hardening baselines (distroless image, non-root, secret +See [engineering guidelines](docs/development/guidelines.md) for hardening baselines (non-root runtime image, secret handling, supply-chain checks in CI). ## Supply chain (releases) diff --git a/charts/kollect/README.md b/charts/kollect/README.md index f88de26f..c8a84fb5 100644 --- a/charts/kollect/README.md +++ b/charts/kollect/README.md @@ -10,6 +10,13 @@ Installs the [Kollect](https://github.com/konih/kollect) operator and CRDs. helm install kollect ./charts/kollect -n kollect-system --create-namespace ``` +### Container image + +The default image is **Debian bookworm-slim** (UID/GID 65532) with `git` and `openssh-client` so +`spec.git.engine: cli` and `git ls-remote` connection probes work out of the box. Default +`spec.git.engine: go-git` does not require those binaries. Pod `securityContext` defaults are +unchanged (`readOnlyRootFilesystem: true`, capabilities dropped, `/tmp` `emptyDir` for git workdirs). + ## Values | Key | Type | Default | Description | diff --git a/docs/ASSURANCE-CASE.md b/docs/ASSURANCE-CASE.md index a45f6aa8..38689a3a 100644 --- a/docs/ASSURANCE-CASE.md +++ b/docs/ASSURANCE-CASE.md @@ -21,7 +21,7 @@ Security requirements are tracked as NFR-SEC in [REQUIREMENTS.md](REQUIREMENTS.m | NFR-SEC-2 | Default verify TLS; `insecureSkipVerify` opt-in and surfaced | ADR-0104, sink validators | | NFR-SEC-3 | Tenancy via `KollectScope` + SAR; least-privilege RBAC | ADR-0203, ADR-0704, `task audit:rbac` | | NFR-SEC-4 | Sensitive-key redaction before export | ADR-0303, ADR-0405 | -| NFR-SEC-5 | Distroless nonroot image; minimal attack surface | Dockerfile, ADR-0705 | +| NFR-SEC-5 | Nonroot image (UID 65532); minimal runtime deps (`git`/`openssh-client` for git CLI engine) | Dockerfile, ADR-0705 | ## Trust boundaries diff --git a/docs/OPERATOR-MANUAL.md b/docs/OPERATOR-MANUAL.md index 1294c894..5ee2ce84 100644 --- a/docs/OPERATOR-MANUAL.md +++ b/docs/OPERATOR-MANUAL.md @@ -107,6 +107,19 @@ Walkthrough: [Postgres state store](examples/postgres-state-store.md). TLS trust for sinks uses `caBundle` or `caSecretRef` on the sink spec — same resolution as export and connection probes ([ADR-0403](adr/0403-connection-test.md)). +### Git snapshot sinks (`spec.git.engine`) + +| `spec.git.engine` | Runtime needs | Notes | +| --- | --- | --- | +| `go-git` (default) | None beyond the manager binary | Pure Go transport; works on minimal images | +| `cli` | `git` and `openssh-client` in `PATH` | Native clone/commit/push; SSH uses `GIT_SSH_COMMAND` with `openssh-client` | + +The published operator image (`ghcr.io/konih/kollect`) ships **Debian bookworm-slim** with +`git`, `openssh-client`, and `ca-certificates` on UID/GID **65532**. Default `go-git` export is +unchanged; the image change enables `engine: cli` and full `git ls-remote` connection probes. +Custom images built from an older distroless base must install `git` (and `openssh-client` for SSH) +when using `engine: cli`. + ### Webhook serving certificate Validating webhooks require a TLS serving cert mounted on every manager replica diff --git a/docs/adr/0101-kubebuilder-v4.md b/docs/adr/0101-kubebuilder-v4.md index adcf3a48..185169dd 100644 --- a/docs/adr/0101-kubebuilder-v4.md +++ b/docs/adr/0101-kubebuilder-v4.md @@ -23,7 +23,7 @@ patterns — thin reconcilers, workqueues, conditions, and leader election. - **Layout:** standard Kubebuilder v4 project structure (`api/v1alpha1`, `internal/{controller,collect,sink}`, `cmd/main.go`, `config/`, `charts/kollect/`). - **API version:** `kollect.dev/v1alpha1` (alpha until export/doc flows stabilize). -- **Image:** distroless nonroot; evaluate `ko` for reproducible builds (see PLAN Phase 0 tooling). +- **Image:** Debian bookworm-slim nonroot (UID 65532) with `git`/`openssh-client` for git CLI engine; evaluate `ko` for reproducible builds (see PLAN Phase 0 tooling). ## Consequences diff --git a/docs/adr/0705-release-supply-chain.md b/docs/adr/0705-release-supply-chain.md index 0876a841..00b25d13 100644 --- a/docs/adr/0705-release-supply-chain.md +++ b/docs/adr/0705-release-supply-chain.md @@ -73,7 +73,7 @@ are **documented and deferred** — not enabled — so one maintainer can ship w | --- | ---: | --- | --- | | Dangerous-Workflow | 0 critical | **Done** | No `pull_request_target`; workflow inputs passed via env vars; actions SHA-pinned | | Token-Permissions | 0 high | **Done** | Default `contents: read`; `security-events: write` scoped to CodeQL/Scorecard analyze jobs only; release job documents why `contents: write` is required | -| Pinned-Dependencies | 0 medium | **Done** | Actions pinned to commit SHA; distroless base image by digest; Helm tarball SHA256-verified; pip docs deps hash-locked (`--require-hashes`) | +| Pinned-Dependencies | 0 medium | **Done** | Actions pinned to commit SHA; runtime base image by digest (Debian bookworm-slim); Helm tarball SHA256-verified; pip docs deps hash-locked (`--require-hashes`) | | SAST | 0 medium | **Done** | `golangci-lint` + `govulncheck` in CI; **CodeQL** for Go (`.github/workflows/codeql.yaml`) | | Vulnerabilities | 0 | **Done** | `govulncheck` on every PR; grpc ≥1.79.3 and otel/sdk ≥1.43.0; Trivy gates release images; Dependabot alerts enabled | | Security-Policy | 10 | **Done** | `SECURITY.md` | diff --git a/docs/crds/kollectsnapshotsink.md b/docs/crds/kollectsnapshotsink.md index 0a60a3d3..86a642b4 100644 --- a/docs/crds/kollectsnapshotsink.md +++ b/docs/crds/kollectsnapshotsink.md @@ -17,6 +17,7 @@ snapshot sinks via `KollectInventory.spec.snapshotSinkRefs`. | `spec.type` | Backend: `git`, `gitlab`, `s3`, `gcs`, `azureblob`, `http` | | `spec.endpoint` | Repository URL, bucket URI, or webhook URL | | `spec.git` / `spec.gitlab` / `spec.objectStore` / `spec.http` | Type-specific settings | +| `spec.git.engine` | Git export backend: `go-git` (default, pure Go) or `cli` (native `git` binary). `cli` is required for some SSH/KEX edge cases; shipped operator image includes `git` and `openssh-client` | | `spec.exportMinInterval` | Default per-ref debounce when inventory ref omits override | | `spec.connectionTest` | Automatic probe on create/update (default `true`) | diff --git a/docs/development/coding-standards.md b/docs/development/coding-standards.md index 4acea607..56d7af45 100644 --- a/docs/development/coding-standards.md +++ b/docs/development/coding-standards.md @@ -74,8 +74,10 @@ Short, actionable rules for Go code in this repo. Operator reconcile semantics a ### Container builds -- **MUST** build the operator manager with `CGO_ENABLED=0` for distroless images - ([`Dockerfile`](../../Dockerfile)); enable CGO locally only for `task coverage:race`. +- **MUST** build the operator manager with `CGO_ENABLED=0` for the shipped image + ([`Dockerfile`](../../Dockerfile)); enable CGO locally only for `task coverage:race`. The runtime + stage is Debian bookworm-slim (nonroot UID 65532) with `git` and `openssh-client` for + `spec.git.engine: cli` and `git ls-remote` probes; `go-git` (default) does not use those binaries. ## Go style and lint diff --git a/docs/development/guidelines.md b/docs/development/guidelines.md index 255f5d1d..1dc1163d 100644 --- a/docs/development/guidelines.md +++ b/docs/development/guidelines.md @@ -49,7 +49,7 @@ Operator-specific error taxonomy drives reconcile behavior. For Go wrapping conv - **Least-privilege RBAC** — minimal generated roles; SAR pre-check before list/watch. - **Tenancy** — optional `KollectScope` (future) for allowed GVKs, namespaces, sinks. - **Secrets** — credentials only via `secretRef`; never in spec/status or logs. -- **Container hardening** — distroless non-root, read-only rootfs, dropped capabilities, seccomp. +- **Container hardening** — non-root runtime image (UID 65532), read-only rootfs, dropped capabilities, seccomp. - **Network** — restrictive `NetworkPolicy` for production egress. - **Transport** — TLS verification required for sink and doc endpoints; support org **custom CA** (no disable-verify in prod). - **Input validation** — CEL in CRD OpenAPI + **validating webhooks before reconcile workarounds**. From 48af9c4924c150bea36694dac1f691f3b7a88d82 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sun, 7 Jun 2026 16:08:45 +0200 Subject: [PATCH 4/6] :bug: fix(e2e): drop removed inventory sinkRefs field Multitenant fixture still set spec.sinkRefs on KollectInventory after the family-sink API split; strict decoding failed in nightly e2e. :page_facing_up: docs(changelog): sync commit links after rebase :page_facing_up: docs(helm): document runtime image in README gotmpl --- CHANGELOG.md | 6 +++--- charts/kollect/README.md.gotmpl | 7 +++++++ .../fixtures/multitenant/tenant-inventory.yaml.template | 1 - 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e46533b..ae82635e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,9 +27,9 @@ anchor on an older commit. ### Bug Fixes -- **e2e:** Validate collection via inventory HTTP [cd77870](https://github.com/konih/kollect/commit/cd778701acdd95cb4d2bf74e677499b856bbc702) +- **e2e:** Validate collection via inventory HTTP [ca816a0](https://github.com/konih/kollect/commit/ca816a072b89326d0bc6c6a6dae5f4d2f0ec8159) -- **e2e,test:** Stabilize smoke bootstrap and debounce IT [99d72bb](https://github.com/konih/kollect/commit/99d72bb35ba1cffe4a8c7dbd6777bc5ae37665e2) +- **e2e,test:** Stabilize smoke bootstrap and debounce IT [3677bb5](https://github.com/konih/kollect/commit/3677bb5dc2b1685def8a7aec05f066604e18985c) - **samples:** Drop legacy sinkRefs from team-a scope [1648ab3](https://github.com/konih/kollect/commit/1648ab3ce0b67e58d8129da909487db91bf3c23b) @@ -40,7 +40,7 @@ anchor on an older commit. ### Features -- **git:** Port transport retry and SSH host keys [b8bab57](https://github.com/konih/kollect/commit/b8bab57eba8ed6f554edad951ee761dc8c39a5a4) +- **git:** Port transport retry and SSH host keys [9f5b17f](https://github.com/konih/kollect/commit/9f5b17f40d0eda7b8e482e2c03cb69aac66f370a) - **controller:** Wire family sink reconcilers and export [8162345](https://github.com/konih/kollect/commit/8162345b399d75ae315d05e06bb25a43c7eb4e0f) diff --git a/charts/kollect/README.md.gotmpl b/charts/kollect/README.md.gotmpl index 9b54d7d3..ec7940d1 100644 --- a/charts/kollect/README.md.gotmpl +++ b/charts/kollect/README.md.gotmpl @@ -10,6 +10,13 @@ Installs the [Kollect](https://github.com/konih/kollect) operator and CRDs. helm install kollect ./charts/kollect -n kollect-system --create-namespace ``` +### Container image + +The default image is **Debian bookworm-slim** (UID/GID 65532) with `git` and `openssh-client` so +`spec.git.engine: cli` and `git ls-remote` connection probes work out of the box. Default +`spec.git.engine: go-git` does not require those binaries. Pod `securityContext` defaults are +unchanged (`readOnlyRootFilesystem: true`, capabilities dropped, `/tmp` `emptyDir` for git workdirs). + {{ template "chart.valuesSection" . }} Export debouncing is configured per **`KollectInventory.spec.exportMinInterval`** (CRD default diff --git a/test/e2e/fixtures/multitenant/tenant-inventory.yaml.template b/test/e2e/fixtures/multitenant/tenant-inventory.yaml.template index 57ccb639..274a4ddf 100644 --- a/test/e2e/fixtures/multitenant/tenant-inventory.yaml.template +++ b/test/e2e/fixtures/multitenant/tenant-inventory.yaml.template @@ -4,5 +4,4 @@ metadata: name: tenant-inventory namespace: ${TENANT_NS} spec: - sinkRefs: [] suspend: false From 144a9ff6f4a0630c95acac079d9768b68c02bb46 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sun, 7 Jun 2026 16:08:50 +0200 Subject: [PATCH 5/6] :page_facing_up: docs(changelog): note multitenant e2e fixture fix --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae82635e..a6351a5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ anchor on an older commit. ### Bug Fixes +- **e2e:** Drop removed inventory sinkRefs field [48af9c4](https://github.com/konih/kollect/commit/48af9c4924c150bea36694dac1f691f3b7a88d82) + - **e2e:** Validate collection via inventory HTTP [ca816a0](https://github.com/konih/kollect/commit/ca816a072b89326d0bc6c6a6dae5f4d2f0ec8159) - **e2e,test:** Stabilize smoke bootstrap and debounce IT [3677bb5](https://github.com/konih/kollect/commit/3677bb5dc2b1685def8a7aec05f066604e18985c) From 02c6c0994cd4b5c6436e9783729009b5d0375f0b Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sun, 7 Jun 2026 16:15:55 +0200 Subject: [PATCH 6/6] :bug: fix(e2e): drop legacy sinkRefs from multitenant scope Multitenant governance fixture still used spec.sinkRefs on KollectScope after the ADR-0414 family-sink split; align docs that referenced sinkRefs: [] in e2e. --- CHANGELOG.md | 2 ++ docs/examples/kind-local-lab.md | 2 +- docs/examples/spoke-cluster-inventory.md | 2 +- test/e2e/fixtures/multitenant/tenant-scope.yaml | 2 -- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6351a5d..95edf7a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ anchor on an older commit. ### Bug Fixes +- **e2e:** Drop legacy sinkRefs from multitenant scope fixture + - **e2e:** Drop removed inventory sinkRefs field [48af9c4](https://github.com/konih/kollect/commit/48af9c4924c150bea36694dac1f691f3b7a88d82) - **e2e:** Validate collection via inventory HTTP [ca816a0](https://github.com/konih/kollect/commit/ca816a072b89326d0bc6c6a6dae5f4d2f0ec8159) diff --git a/docs/examples/kind-local-lab.md b/docs/examples/kind-local-lab.md index 20b3ef2c..03824860 100644 --- a/docs/examples/kind-local-lab.md +++ b/docs/examples/kind-local-lab.md @@ -38,7 +38,7 @@ Production values and upgrades: [Operator manual](../OPERATOR-MANUAL.md). !!! note "No Postgres required" - For a minimal e2e without a database, apply `config/samples/e2e/team-inventory.yaml` (`sinkRefs: []`). + For a minimal e2e without a database, apply `config/samples/e2e/team-inventory.yaml` (no family sink refs). See [QUICKSTART.md](../QUICKSTART.md) and the [Examples index](README.md). !!! tip "Full showcase demo" diff --git a/docs/examples/spoke-cluster-inventory.md b/docs/examples/spoke-cluster-inventory.md index 3a33451a..d596f4a3 100644 --- a/docs/examples/spoke-cluster-inventory.md +++ b/docs/examples/spoke-cluster-inventory.md @@ -52,7 +52,7 @@ Set `spec.cluster` on the sink for shared-database fan-in across spokes. Full walkthrough: [Deployment inventory](deployment-inventory.md). -E2e without live backends: `config/samples/e2e/team-inventory.yaml` (`sinkRefs: []`). +E2e without live backends: `config/samples/e2e/team-inventory.yaml` (no family sink refs). ## Hub-and-spoke upgrade diff --git a/test/e2e/fixtures/multitenant/tenant-scope.yaml b/test/e2e/fixtures/multitenant/tenant-scope.yaml index 80cb568b..bdd6f53f 100644 --- a/test/e2e/fixtures/multitenant/tenant-scope.yaml +++ b/test/e2e/fixtures/multitenant/tenant-scope.yaml @@ -10,5 +10,3 @@ spec: kind: Deployment allowedNamespaces: - kollect-tenant-a - sinkRefs: - - git-inventory-demo