Skip to content
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
133 changes: 133 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,136 @@
## v0.7.16 — Per-Target OperatorBox and MuxReconciler

### Per-target operatorBox

Each `serve.target.entries` entry can now declare its own `operatorBox` — resources, lifecycle hooks, and preReconcile gates (`enqueueGate` / `reconcileGate`). The reconciler selects the active box from `serve-alias` / `serve-target` annotations on the CR at reconcile time.

```yaml
serve:
target:
entries:
v2-enabled:
operatorBox:
enqueueGate: "{{ isBusinessHours }}"
reconciler:
hooks: true
v2-ctor:
operatorBox:
reconciler:
default: false # use a dedicated constructor from ReconcilerRegistry
```

`reconciler.default: false` wires the target's constructor from `ReconcilerRegistry` at load time. A missing registry entry is a load-time error — the Runtime refuses to start rather than falling back silently.

Surface switches are detected via `orkestra.orkspace.io/last-surface` and cleaned up with a label-selector sweep (`SweepOwnedNamespaced` / `SweepOwnedClusterScoped`) rather than template expansion, which is immune to spec fields being cleared before cleanup runs. `keepPreviousSurface: true` skips the sweep when set.

`EffectiveOwnerKey` stamps and checks ownership as `<name>.<alias>` for target-mode CRs, allowing per-surface resource isolation.

---

### MuxReconciler and `pkg/intent/target/`

`MuxReconciler` holds one `domain.Reconciler` per registered target. At reconcile time the kordinator reads the target annotation and dispatches to the matching reconciler, falling back to the CRD-level reconciler when no target-specific one is registered. The kordinator sees one reconciler; the routing is internal.

Target resolution and CR construction move into a dedicated `pkg/intent/target/` package, separating the intent layer from the gateway API package. `target.go` and its tests follow.

---

### `ork serve apply --override`

Routing conflict detection (`409`) is still enforced when a target switch is attempted without an explicit override. Pass `--override` (or `?override=true` on the API) to route to the new target and trigger surface cleanup of the old one.

---

### Fixture: `03-hooks-targets`

`pkg/kubeclient/fixture/03-hooks-targets` — three targets on one CRD proving each dispatch path end-to-end:

- `v2-enabled` — hooks with an `enqueueGate` (business hours), `featureEnabled: true`
- `v2-disabled` — same hook binary, `featureEnabled: false`, no gate
- `v2-ctor` — `reconciler.default: false`, dedicated constructor

---

### Documentation

`documentation/concepts/reusability/` — new section covering Reusability and Composition in Orkestra.

---

### `lifecycle:` block — maturity, deprecation, compatibility, and platform policy

A new top-level `lifecycle:` block on every Katalog and Komposer file replaces the old `metadata.deprecation:` approach with a first-class lifecycle model.

```yaml
lifecycle:
maturity: beta # alpha | beta | stable | deprecated

deprecation:
message: "Replaced by task-runner"
migratedTo: task-runner:v1.0.0
timeline:
from: "2026-01-01"
to: "2027-01-01"

compatibility:
orkestra: ">= 0.7.0"
kubernetes: ">= 1.28"
```

**Maturity** — four levels: `alpha`, `beta`, `stable`, `deprecated`. The presence of a `deprecation:` block is the primary signal; `maturity: deprecated` without a block emits a warning rather than an error. `maturity: deprecated` with a block is always valid.

**Deprecation** — a deprecated Katalog always blocks startup when run directly. Consumers acknowledge it via `lifecycle.accept.patterns` on their Komposer, not by a field on the Katalog itself.

**Compatibility** — declares minimum `orkestra` and `kubernetes` semver constraints. `ork validate` rejects patterns that declare versions below the installed runtime.

**Kind boundary** — `lifecycle.accept.patterns` belongs on a Komposer. Declaring `lifecycle.accept` on a Katalog is a validation error.

---

### Komposer-level lifecycle acceptance — `lifecycle.accept.patterns`

Komposers accept deprecated Katalogs at the point of composition:

```yaml
lifecycle:
accept:
patterns:
- name: webapp-operator
version: ">= 1.0.0, < 2.0.0" # optional semver range
- name: cache-operator
```

`version:` scopes acceptance to a semver range. Acceptance without a range applies to any version of that pattern.

---

### Platform policy — `policy.lifecycle.minMaturity`

Operators can declare a minimum maturity floor for all imported patterns:

```yaml
policy:
lifecycle:
minMaturity: beta # alpha | beta | stable; deprecated is rejected
```

`ork validate` rejects any Katalog whose maturity is below the declared floor. `minMaturity: deprecated` is itself a validation error — the policy is a quality floor, not a filter.

`policy:` is structured as `policy.<area>.*` so security, registry, and user-defined policy categories can grow alongside `lifecycle:` without flattening.

---

### Registry guide examples 13–16

Four new self-contained steps extend the registry guide:

- **13-deprecation-accept** — accept a deprecated Katalog via Komposer `lifecycle.accept.patterns`; scoped version acceptance
- **14-lifecycle-maturity** — maturity progression from `alpha` through `stable`; `ork inspect` output at each stage
- **15-lifecycle-compatibility** — declaring `orkestra` and `kubernetes` version constraints; validation rejection behaviour
- **16-komposer-accept** — `lifecycle.accept.patterns` on a Komposer composing both a deprecated and an alpha Katalog; scoped `version:` range

---

## v0.7.15 — Gateway Webhook Intake + Artifact Signing

### Artifact signing — Cosign keyless, `publish:` block, local testing
Expand Down
30 changes: 19 additions & 11 deletions charts/orkestra/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ description: >-
namespace protection with admission webhooks, and observe everything in the
Control Center. Runtime and Gateway deploy independently.
type: application
version: 1.7.10
appVersion: "0.7.10"
version: 1.7.16
appVersion: "0.7.16"

keywords:
- orkestra
Expand Down Expand Up @@ -65,16 +65,24 @@ annotations:
url: https://raw.githubusercontent.com/orkspace/orkestra/main/documentation/assets/controlcenter/pv-watcher-workers.png
artifacthub.io/changes: |
- kind: added
description: "ork e2e: --report-file flag writes results as GFM markdown tables (passed + failed with error column)"
description: "Per-target operatorBox: each serve.target.entries entry can declare its own operatorBox with hooks, gates, and reconciler config"
- kind: added
description: "ork e2e: spec.onFailure — diagnostic kubectl and shell commands printed when any expectation fails"
description: "MuxReconciler: dispatches to the correct reconciler per target at reconcile time; kordinator sees one reconciler"
- kind: added
description: "ork e2e: per-expectation onFailure — diagnostics printed immediately when that specific checkpoint fails"
description: "pkg/intent/target/: target resolution and CR construction move into a dedicated package separate from gateway API"
- kind: added
description: "ork e2e: terminal output split — passed cases first, failed cases with indented error detail"
- kind: fixed
description: "forEach silently no-op on NetworkPolicy, ResourceQuota, LimitRange, ClusterRole, ClusterRoleBinding"
- kind: fixed
description: "NetworkPolicy, ResourceQuota, LimitRange, ClusterRole, ClusterRoleBinding not tracked as children in CR detail"
description: "ork serve apply --override: force-route to a new target and trigger surface cleanup of the old one"
- kind: added
description: "Fixture 03-hooks-targets: three targets on one CRD proving hooks, gate, and constructor dispatch paths"
- kind: added
description: "documentation/concepts/reusability/: new Reusability and Composition section"
- kind: fixed
description: "isFileMotif: bare relative motif paths not recognised as file paths"
description: "reconciler.default: false — constructor now wired from ReconcilerRegistry at load time; missing entry is a load-time error"
- kind: added
description: "lifecycle: block — maturity (alpha/beta/stable/deprecated), deprecation, compatibility, and platform policy on every Katalog"
- kind: added
description: "lifecycle.accept.patterns on Komposer: accept deprecated Katalogs at composition time with optional semver version range"
- kind: added
description: "policy.minMaturity: platform-level maturity floor; ork validate rejects patterns below the declared floor"
- kind: added
description: "Registry guide examples 13–16: deprecation-accept, lifecycle-maturity, lifecycle-compatibility, komposer-accept"
21 changes: 9 additions & 12 deletions cmd/cli/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,10 @@ func printTypedOperatorHint(err *katalog.TypedOperatorError, command string) {
// katalog. Reads timeline state from the KatalogDeprecation methods.
// Prints nothing if the block is nil or today is before timeline.from.
func printKatalogDeprecation(d *orktypes.KatalogDeprecation) {
printKatalogDeprecationWithHint(d, "")
}

func printKatalogDeprecationWithHint(d *orktypes.KatalogDeprecation, hint string) {
if d == nil {
return
}
Expand All @@ -734,17 +738,7 @@ func printKatalogDeprecation(d *orktypes.KatalogDeprecation) {
if state == "none" {
return
}
printDeprecationBlock(state, d.Message, d.MigratedTo, d.TimelineTo(), d.DaysUntilEOL(today))

// Stale accept flag warnings — shown after the block so they are not missed.
if d.Accept != nil {
if d.Accept.Eol && !d.Accept.BeforeEol {
fmt.Printf(" %s accept.eol is set without accept.beforeEol — eol: true alone is not sufficient\n", yellow("⚠"))
}
if d.Accept.BeforeEol && !d.Accept.Eol && state == "eol" {
fmt.Printf(" %s accept.beforeEol is set but the EOL date has passed — add accept.eol: true or ork run will refuse to start\n", yellow("⚠"))
}
}
printDeprecationBlock(state, d.Message, d.MigratedTo, d.TimelineTo(), hint, d.DaysUntilEOL(today))
}

// printPatternDeprecation prints the deprecation notice for a registry pattern
Expand All @@ -767,7 +761,7 @@ func printPatternDeprecation(dep *registry.PatternDeprecated) {
}

// printDeprecationBlock renders the deprecation block for a given state.
func printDeprecationBlock(state, message, migrateTo, eolDate string, daysLeft int) {
func printDeprecationBlock(state, message, migrateTo, eolDate, hint string, daysLeft int) {
switch state {
case "eol":
fmt.Printf("\n%s END OF LIFE\n", red("✗"))
Expand All @@ -788,6 +782,9 @@ func printDeprecationBlock(state, message, migrateTo, eolDate string, daysLeft i
if migrateTo != "" {
fmt.Printf(" Migrate to: %s\n", bold(migrateTo))
}
if hint != "" {
fmt.Printf(" %s\n", hint)
}
fmt.Println()
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/cli/play_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ import (
"strings"

"github.com/orkspace/orkestra/pkg/gateway/api"
orktarget "github.com/orkspace/orkestra/pkg/intent/target"
"github.com/orkspace/orkestra/pkg/katalog"
"github.com/orkspace/orkestra/pkg/merger"
"github.com/orkspace/orkestra/pkg/registry/simulate"
orktarget "github.com/orkspace/orkestra/pkg/intent/target"
orktmpl "github.com/orkspace/orkestra/pkg/resources/template"
orktypes "github.com/orkspace/orkestra/pkg/types"
"gopkg.in/yaml.v3"
Expand Down
6 changes: 5 additions & 1 deletion cmd/cli/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,11 @@ Examples:
builtIn := 0
custom := 0

printKatalogDeprecation(k.Deprecation())
var deprecationHint string
if konfig.IsKomposerKind(docKind) {
deprecationHint = "To acknowledge this import, add it to lifecycle.accept.patterns in your Komposer."
}
printKatalogDeprecationWithHint(k.Deprecation(), deprecationHint)

// Print each CRD entry with enrichment info
for _, entry := range sortedEntries {
Expand Down
139 changes: 139 additions & 0 deletions documentation/concepts/conversion/03-serve-translation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# serve.fields.values

`serve.fields.values` runs at the Gateway before the CR is written. Callers submit a simplified intent; the Katalog fans the fields out to the CRD's internal schema. The CRD never sees the caller's vocabulary — the translation is declared once in the serve block and runs transparently on every apply.

Use it when the caller should not be coupled to the CRD schema. The CRD is an implementation detail; the intent is the contract.

---

## How it works

Declare `serve.fields` in the Katalog. Each field entry maps one intent field to one or more CRD paths via `values:`. The expressions run against `.value` — the raw value the caller submitted.

```yaml
serve:
fields:
schedule:
values:
schedule.minute: '{{ cronMinute .value }}'
schedule.hour: '{{ cronHour .value }}'
schedule.dayOfMonth: '{{ cronDom .value }}'
schedule.month: '{{ cronMonth .value }}'
schedule.dayOfWeek: '{{ cronDow .value }}'
```

The caller submits:

```yaml
target: cronjob-tutorial
name: daily-backup
schedule: "0 2 * * 1-5"
image: "gcr.io/google-containers/busybox:latest"
```

What reaches the API server:

```yaml
spec:
schedule:
minute: "0"
hour: "2"
dayOfMonth: "*"
month: "*"
dayOfWeek: "1-5"
image: gcr.io/google-containers/busybox:latest
```

The structured schedule is reconstructed from the cron string entirely within the serve layer. Nothing downstream — not the CRD schema, not the reconciler, not etcd — ever sees the flat string.

---

## The pipeline order

```text
Intent submitted (ork serve apply)
serve.fields.values ← flat intent → CRD-shaped spec fields (at the Gateway)
validation ← intent gate fires on the raw request fields
CR written to API server ← structured spec only; caller's vocabulary gone
normalize / mutation / reconcile ← see the CRD shape throughout
```

`serve.fields.values` runs before the CR reaches the API server. Validation rules on `request.*` fields fire against the caller's raw input — before the fanout — so error messages speak the caller's vocabulary, not the CRD's.

```yaml
validation:
rules:
- field: request.schedule
operator: exists
message: "schedule is required — use a cron expression (e.g. \"*/5 * * * *\")"
action: deny
```

---

## One field to many CRD paths

A single intent field can fan out to any number of CRD paths. Each key under `values:` is a dot-notation path into `spec`:

```yaml
serve:
fields:
schedule:
values:
schedule.minute: '{{ cronMinute .value }}'
schedule.hour: '{{ cronHour .value }}'
schedule.dayOfMonth: '{{ cronDom .value }}'
schedule.month: '{{ cronMonth .value }}'
schedule.dayOfWeek: '{{ cronDow .value }}'
```

Five CRD fields from one intent field. The template functions (`cronMinute`, `cronHour`, `cronDom`, `cronMonth`, `cronDow`) each extract one component from the cron string.

---

## Comparing the three approaches

| | `normalize:` | `conversion.paths:` | `serve.fields.values` |
|---|---|---|---|
| **Translation point** | Reconciler | API server (`/convert`) | Gateway (before CR is written) |
| **CRD versions** | One | Two or more | One |
| **Caller submits via** | `kubectl apply` | `kubectl apply` | `ork serve apply` |
| **Caller sees CRD schema** | Yes | Yes | No |
| **Gateway required** | No | Yes | Yes |

`serve.fields.values` is the only approach where the CRD schema is entirely hidden from callers. The other two require the caller to know the CRD's field names, even if the format is flexible.

---

## Try it

```bash
ork init --pack use-cases/crd-conversion/with-serve-translation
cd with-serve-translation
```

Test the field fanout locally without a cluster:

```bash
ork serve play -i intent.yaml -t dev
```

Prints the built CR with `spec.schedule` as the structured object. Try an invalid cron string:

```bash
ork serve play -i intent-invalid.yaml -t dev
```

The intent gate fires on `request.schedule` and returns the error in the caller's vocabulary.

---

## Where to go next

- **[Schema Evolution](./index.md)** — pick-one table
- **[Self-service and target mode](../self-service/index.md)** — how `ork serve apply` and the Gateway delivery layer work
- **[serve.fields schema](../../reference/schema/02-katalog/22-serve-field-translation.md)** — full `serve.fields.values` reference and all field config options
Loading
Loading