From 9338141786cd7d86f65ec77a0cdc749fd479efd3 Mon Sep 17 00:00:00 2001 From: Diego Braga Date: Sun, 21 Jun 2026 15:38:16 +0200 Subject: [PATCH 1/2] docs: document status projection + apiRef .api source Cover the declarative status projection (statusDataTemplate over the self/spec/status/helm/api source root, observedGeneration, degrade-only and paused behavior) and the apiRef .api source resolved via the authn-JWT -> snowplow RESTAction chain, with the env-driven config. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.md b/README.md index 34a7ea0..6d6d3d3 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,45 @@ The **Composition Dynamic Controller (CDC)** is the execution engine of Krateo. - **Lifecycle Orchestration**: Manages the end-to-end deployment, updates, and deletion of services based on Helm charts. - **Dynamic Reconciliation**: Automatically reconciles resource states, ensuring the live cluster matches the desired state defined in the `Composition`. - **Chart Inspector Integration**: Leverages the Krateo Chart Inspector for secure dry-runs, ensuring chart validity and resource safety before application. +- **Declarative Status Projection**: Projects author-defined fields onto each `Composition`'s `.status` on every reconcile, and stamps `status.observedGeneration`. + +## Status Projection + +On each reconcile the CDC evaluates a set of declarative `${ jq }` mappings (the `statusDataTemplate`, shipped from core-provider via `COMPOSITION_CONTROLLER_STATUS_DATA_TEMPLATE` as a JSON array of `{forPath, expression}` items) and writes their results onto the `Composition`'s `.status`. Each expression runs over a combined source root: + +- `self` / `spec` / `status` — the composition object itself (with `spec`/`status` as sugar for `self.spec`/`self.status`); +- `helm` — the managed Helm release's `url`, `version`, `status`, `revision`, and `name`; +- `api` — the resolved `apiRef` RESTAction (see below), present only when an `apiRef` is configured and resolution succeeds. + +The CDC also stamps `status.observedGeneration` from `metadata.generation` every reconcile. + +Projection is **degrade-only**: a failing or invalid mapping affects only its own field and never the baseline status, and an invalid `statusDataTemplate` simply disables projection. It is **skipped while a composition is gracefully paused**. + +### `apiRef` `.api` source + +When an `apiRef` is declared, the CDC resolves a single RESTAction through **snowplow**, under its **own** identity, and feeds the result in as the projection's `api` source. The resolution chain is: + +1. **authn** (`internal/authn`) — the CDC reads its projected (audience `authn`) ServiceAccount token from disk and exchanges it at authn's `POST /serviceaccount/login` (TokenReview-validated) for a service JWT, caching the JWT until shortly before its expiry. +2. **snowplow** (`internal/snowplow`) — the CDC GETs snowplow's `/call?resource=restactions&...&extras=` with `Authorization: Bearer `, receiving the resolved RESTAction's `.status` (the keyed `.api.` map). +3. **resolver** (`internal/composition/apiresolver.go`) — per-instance composition context (`compositionName`, `compositionNamespace`, `compositionId`) is layered over the author-declared static `apiRef.extras` (request-wins) before resolution. + +`.api` resolution is also degrade-only: if it fails, the `.api` source is simply absent (api-dependent mappings skip) while built-in and `helm`-sourced fields still project. + +### Configuration + +All status-projection and `apiRef` settings are env-driven and optional; an empty `apiRef` name disables `.api` resolution entirely: + +| Env var | Description | +| --- | --- | +| `COMPOSITION_CONTROLLER_STATUS_DATA_TEMPLATE` | JSON-encoded `statusDataTemplate` (`[{forPath, expression}]`). | +| `COMPOSITION_CONTROLLER_API_REF_NAME` | Name of the RESTAction resolved for the `.api` source; empty disables `apiRef` resolution. | +| `COMPOSITION_CONTROLLER_API_REF_NAMESPACE` | Namespace of the `apiRef` RESTAction. | +| `COMPOSITION_CONTROLLER_API_REF_EXTRAS` | JSON object of static extras merged into the `apiRef` resolution. | +| `URL_SNOWPLOW` | Snowplow base URL for resolving RESTActions. | +| `URL_AUTHN` | Authn base URL for exchanging the SA token for a service JWT. | +| `COMPOSITION_CONTROLLER_SERVICEACCOUNT_TOKEN_PATH` | Path to the projected (authn-audience) SA token (default `/var/run/secrets/krateo.io/serviceaccount/token`). | + +A real end-to-end test of the `apiRef` chain (real authn + real snowplow on kind) lives under [`hack/apiref-e2e/`](hack/apiref-e2e/README.md). ## Security & Operational Design From a8ad31cb40cd0673bc2ab73221f8d082dd8702b0 Mon Sep 17 00:00:00 2001 From: Diego Braga Date: Mon, 22 Jun 2026 12:34:35 +0200 Subject: [PATCH 2/2] log: use shared NewOTelJSONHandler from unstructured-runtime v1.2.0 (KOS-1 Phase 1) Bumped the unstructured-runtime replace v1.1.2 -> v1.2.0 and replaced the inline JSON handler with logging.NewOTelJSONHandler -- so the cdc and every composition controller emit identical OTel-structured logs (RFC3339Nano timestamp, SeverityText + SeverityNumber, trace_id/span_id when a span is in context). Keeps both "service" and "service.name" during the logs-ingester transition. Folded the stray slog.Warn into the structured logger. Build + vet green. Co-Authored-By: Claude Opus 4.8 --- go.mod | 8 ++++---- go.sum | 4 ++-- main.go | 25 ++++++++++--------------- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/go.mod b/go.mod index 96b666c..e82a972 100644 --- a/go.mod +++ b/go.mod @@ -153,11 +153,11 @@ require ( sigs.k8s.io/yaml v1.6.0 // indirect ) -// Use the braghettos fork of unstructured-runtime, which carries the create-pending -// Observe-before-refuse recovery (PR braghettos/unstructured-runtime#2) AND the jq-based -// statusprojection engine (PR braghettos/unstructured-runtime#3), unified in tag v1.1.2. +// Use the braghettos fork of unstructured-runtime: create-pending Observe-before-refuse +// recovery (#2), jq-based statusprojection (#3), AND the shared OTel JSON log handler +// NewOTelJSONHandler (#4) -- unified in tag v1.2.0. // The fork keeps the upstream module path, so pin it via replace. -replace github.com/krateoplatformops/unstructured-runtime => github.com/braghettos/unstructured-runtime v1.1.2 +replace github.com/krateoplatformops/unstructured-runtime => github.com/braghettos/unstructured-runtime v1.2.0 // Source plumbing from the braghettos fork (v1.7.6): carries the jqutil int64/int32 // gojq-panic fix the statusprojection engine relies on, plus crdgen array-default markers. diff --git a/go.sum b/go.sum index 2a3f0bc..995d9b1 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/braghettos/plumbing v1.7.6 h1:aOsvowAf5JXVU05zhwpWSCsy76XnrcE1Rsngbn732g8= github.com/braghettos/plumbing v1.7.6/go.mod h1:UrYvAvnfacOiOLlQbIg6J08zYC+XJ9ZMKbecQq+4jV4= -github.com/braghettos/unstructured-runtime v1.1.2 h1:/hDVcTDTkKBAbeNOVaNRkJ6d7sFWx4MNW3Nja6IySEI= -github.com/braghettos/unstructured-runtime v1.1.2/go.mod h1:vF9FuH0lDjBJbnI+iYcpAItvUkyTRm+RoA1yIFi52wg= +github.com/braghettos/unstructured-runtime v1.2.0 h1:ekY7WtHaR9npOPt81CGIyrOiON1gLvNlFtPAc9/Vw18= +github.com/braghettos/unstructured-runtime v1.2.0/go.mod h1:VX7QPKKyvKdpn7zRvU7zfccvPtrCwvSIgoktHFmfCAk= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= diff --git a/main.go b/main.go index 0482691..f601600 100644 --- a/main.go +++ b/main.go @@ -127,20 +127,15 @@ func main() { logLevel = slog.LevelDebug } - // JSON logs on stderr, compatible with logs-ingester: each line is a single JSON - // object with a canonical "timestamp" field in RFC3339Nano UTC. See - // docs/logs-ingester-compatibility.md. - lh := slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ - Level: logLevel, - AddSource: false, - ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { - if a.Key == slog.TimeKey { - return slog.String("timestamp", a.Value.Time().UTC().Format(time.RFC3339Nano)) - } - return a - }, - }) - sl := slog.New(lh).With(slog.String("service", serviceName)) + // JSON logs on stderr in the OTel log model (RFC3339Nano "timestamp", SeverityText + + // SeverityNumber, trace_id/span_id when a span is in context), compatible with logs-ingester. + // The shared handler lives in unstructured-runtime (pkg/logging) so every composition + // controller is consistent; "service" is kept alongside the OTel "service.name" during the + // transition. See docs/logs-ingester-compatibility.md. + sl := slog.New(logging.NewOTelJSONHandler(logLevel, os.Stderr, + slog.String("service.name", serviceName), + slog.String("service", serviceName), + )) log := logging.NewLogrLogger(logr.FromSlogHandler(sl.Handler())) @@ -254,7 +249,7 @@ func main() { // Initialize CDC-specific metrics if err := metrics.InitMetrics(context.Background(), log, telemetryEnabled, *otelServiceName, telemetryExportInterval, *deploymentName); err != nil { - slog.Warn("Cannot initialize CDC metrics, continuing without metrics", "error", err) + log.Warn("Cannot initialize CDC metrics, continuing without metrics", "error", err) } metrics.DebugStatus()