Skip to content
This repository was archived by the owner on Aug 5, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<json>` with `Authorization: Bearer <JWT>`, receiving the resolved RESTAction's `.status` (the keyed `.api.<callName>` 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

Expand Down
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
25 changes: 10 additions & 15 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()))

Expand Down Expand Up @@ -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()

Expand Down