From 4521c22c4dbf2db44b6caefc3a015e8da24f2c57 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Thu, 2 Apr 2026 10:19:39 +0000 Subject: [PATCH 01/36] Add CUE v0.16.0 to y-bin runner tools Restores CUE CLI that was in ystack from 2022-2023 (removed during y-bin.yaml split). Available as y-cue. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/y-bin.runner.yaml | 13 +++++++++++++ bin/y-cue | 8 ++++++++ 2 files changed, 21 insertions(+) create mode 100755 bin/y-cue diff --git a/bin/y-bin.runner.yaml b/bin/y-bin.runner.yaml index 7e66108..093c626 100755 --- a/bin/y-bin.runner.yaml +++ b/bin/y-bin.runner.yaml @@ -145,6 +145,19 @@ yarn: linux_amd64: 16252fe8ac0b3500bd697bc47213cc438209e2d5f8a812def075a0cdec891301 linux_arm64: 05ca3451a6f78a68b08c13b4b0b582cb22220b90ccd00160bffff80224d8c50d +cue: + version: 0.16.0 + templates: + download: https://github.com/cue-lang/cue/releases/download/v${version}/cue_v${version}_${os}_${arch}.tar.gz + sha256: + darwin_amd64: 451495b46684fd78120741e1d325f9b21e6843f05ec455d3fb5073ba2a9311db + darwin_arm64: 83ba7485cbad5f031ffc3e0142bc4f3ab26b53fd4491548939bfa9d5b00f6b66 + linux_amd64: bbe0c5e85bc101cd1f95e68242f5ad48c3db73bad72be94fe2dc79d3f1ef0d4c + linux_arm64: 5c5a59bcb31d53c81feee58774752df14baef5c412138f49c208ab15608ac981 + archive: + tool: tar + path: cue + npx: version: 0.2.1 templates: diff --git a/bin/y-cue b/bin/y-cue new file mode 100755 index 0000000..1e81896 --- /dev/null +++ b/bin/y-cue @@ -0,0 +1,8 @@ +#!/bin/sh +[ -z "$DEBUG" ] || set -x +set -e +YBIN="$(dirname $0)" + +version=$(y-bin-download $YBIN/y-bin.runner.yaml cue) + +y-cue-v${version}-bin "$@" || exit $? From 887d317d81cecf7445cf9460e103af8c0456d0a8 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Thu, 2 Apr 2026 10:19:44 +0000 Subject: [PATCH 02/36] qemu provisioner: delete disk on teardown by default Add --keep-disk flag to preserve the disk image for faster re-provision. Without it, teardown now removes the qcow2 disk for clean e2e runs. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/y-cluster-provision-qemu | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/bin/y-cluster-provision-qemu b/bin/y-cluster-provision-qemu index 919f6f7..0daf25f 100755 --- a/bin/y-cluster-provision-qemu +++ b/bin/y-cluster-provision-qemu @@ -36,7 +36,8 @@ Flags: --exclude=SUBSTRING exclude k3s bases matching substring (default: monitoring) --skip-converge skip converge and post-provision steps --skip-image-load skip image cache and load into containerd - --teardown stop and delete the VM + --teardown stop and delete the VM (removes disk by default) + --keep-disk preserve disk image on teardown for faster re-provision --export-vmdk=PATH export disk as streamOptimized VMDK Prerequisites: @@ -53,6 +54,7 @@ EOF --skip-converge) SKIP_CONVERGE=true; shift ;; --skip-image-load) SKIP_IMAGE_LOAD=true; shift ;; --teardown) TEARDOWN=true; shift ;; + --keep-disk) KEEP_DISK=true; shift ;; --export-vmdk=*) EXPORT_VMDK="${1#*=}"; shift ;; *) echo "Unknown flag: $1" >&2; exit 1 ;; esac @@ -116,7 +118,12 @@ if [ "$TEARDOWN" = "true" ]; then rm -f "$VM_PIDFILE" fi kubectl config delete-context $CTX 2>/dev/null || true - echo "[y-cluster-provision-qemu] Teardown complete. Disk preserved at $VM_DISK" + if [ "$KEEP_DISK" = "true" ]; then + echo "[y-cluster-provision-qemu] Teardown complete. Disk preserved at $VM_DISK" + else + rm -f "$VM_DISK" + echo "[y-cluster-provision-qemu] Teardown complete. Disk deleted." + fi exit 0 fi From dc6757f9bd4eb498d24e4182222f03125ee51eee Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Thu, 2 Apr 2026 11:06:32 +0000 Subject: [PATCH 03/36] Add kubectl-yconverge to ystack with converge-mode support Generic, POSIX-compatible convergence plugin supporting five modes via the yolean.se/converge-mode label: (none) standard kubectl apply create kubectl create --save-config (skip if exists) replace kubectl delete + apply (for immutable resources) serverside kubectl apply --server-side serverside-force kubectl apply --server-side --force-conflicts Flags: --context= (required), --diff=true, --dry-run=true. Honors KUBECONFIG. Handles empty label selections gracefully. Moved from checkit where it had a hardcoded --server-side path. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/kubectl-yconverge | 74 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100755 bin/kubectl-yconverge diff --git a/bin/kubectl-yconverge b/bin/kubectl-yconverge new file mode 100755 index 0000000..a08d02e --- /dev/null +++ b/bin/kubectl-yconverge @@ -0,0 +1,74 @@ +#!/bin/sh +[ -z "$DEBUG" ] || set -x +set -e + +[ "$1" = "help" ] && echo ' +Idempotent apply with support for converge-mode annotations. + +Supported values for the yolean.se/converge-mode label: + (none) standard kubectl apply + create kubectl create --save-config (skip if exists) + replace kubectl delete + apply (for immutable resources like Jobs) + serverside kubectl apply --server-side + serverside-force kubectl apply --server-side --force-conflicts + +Flags (must come before kustomize args): + --context=NAME required + --diff=true run kubectl diff instead of apply + --dry-run=true pass --dry-run=server to kubectl apply + +Honors KUBECONFIG if set. +' && exit 0 + +DIFF=false +DRY_RUN=false + +ctx=$1 +case $ctx in + "--context="*) shift 1 ;; + *) echo "Error: first arg must be --context=" >&2 && exit 1 ;; +esac + +while true; do + case "${1:-}" in + --diff=true) DIFF=true; shift ;; + --dry-run=true) DRY_RUN=true; shift ;; + *) break ;; + esac +done + +for arg in "$@"; do + case "$arg" in + -l|--selector) echo "Error: yconverge can not be combined with other selectors" >&2 && exit 1 ;; + esac +done + +if [ "$DIFF" = "true" ]; then + kubectl $ctx diff "$@" + exit $? +fi + +DRY_RUN_FLAG="" +[ "$DRY_RUN" = "true" ] && DRY_RUN_FLAG="--dry-run=server" + +# Each mode applies only to resources with the matching label. +# create and delete may find no matching resources — that's expected. +kubectl $ctx create --save-config $DRY_RUN_FLAG --selector=yolean.se/converge-mode=create "$@" 2>/dev/null || true # y-script-lint:disable=or-true # idempotent, ignore already-exists +if [ "$DRY_RUN" = "false" ]; then + kubectl $ctx delete --selector=yolean.se/converge-mode=replace "$@" 2>/dev/null || true # y-script-lint:disable=or-true # ignore not-found +fi + +# "no objects passed to apply" means nothing matched the selector — not an error. +_apply_if_any() { + output=$(kubectl "$@" 2>&1) || { + case "$output" in + *"no objects passed to apply"*) return 0 ;; + *) printf '%s\n' "$output" >&2; return 1 ;; + esac + } + [ -n "$output" ] && printf '%s\n' "$output" +} + +_apply_if_any $ctx apply --server-side --force-conflicts $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside-force "$@" +_apply_if_any $ctx apply --server-side $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside "$@" +_apply_if_any $ctx apply $DRY_RUN_FLAG --selector='yolean.se/converge-mode!=create,yolean.se/converge-mode!=serverside,yolean.se/converge-mode!=serverside-force' "$@" From 085d366b7d2bdfc87ec8be77cb85acb1bb729610 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Thu, 2 Apr 2026 11:06:32 +0000 Subject: [PATCH 04/36] Draft CUE-based converge DAG design Documents the approach for replacing implicit script ordering with CUE import-based dependency resolution and kubectl yconverge. Co-Authored-By: Claude Opus 4.6 (1M context) --- TODO_CONVERGE_DAG.md | 211 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 TODO_CONVERGE_DAG.md diff --git a/TODO_CONVERGE_DAG.md b/TODO_CONVERGE_DAG.md new file mode 100644 index 0000000..b0ee228 --- /dev/null +++ b/TODO_CONVERGE_DAG.md @@ -0,0 +1,211 @@ +# Converge DAG: CUE-based cluster convergence + +## Problem + +Dependencies between backends and modules are implicit in script ordering +(MANUAL_STEPS_FOR_NEW_SITES, y-site-upgrade, y-cluster-converge-dev). +Adding or reordering a module means editing a bash script. + +## Design + +Every kustomize base that can be applied with `kubectl yconverge -k` +is a **step**. Each step declares its readiness via **checks**. +Dependencies between steps are expressed as **CUE imports** — +importing another step's package makes it a precondition. + +The dependency graph is the CUE import graph. +A `cue cmd converge` walks it in topological order. + +## ystack provides + +Schema in `cue/converge/schema.cue`: + +```cue +package converge + +// A convergence step: apply a kustomize base, then verify. +#Step: { + // Path to kustomize directory, relative to repo root. + kustomization: string + // Namespace override. If unset, kustomization must set it. + namespace?: string + // Checks that must pass after apply (and that downstream steps + // use as preconditions by importing this package). + // Empty list means no checks — the step is ready after apply. + checks: [...#Check] +} + +// Check is a discriminated union. Each variant maps to a kubectl +// subcommand that manages its own timeout and output. +#Check: #Wait | #Rollout | #Exec + +// Thin wrapper around kubectl wait. +// Timeout and output are managed by kubectl. +#Wait: { + kind: "wait" + resource: string // e.g. "pod/redpanda-0" or "job/setup-topic" + for: string // e.g. "condition=Ready" or "condition=Complete" + namespace?: string + timeout: *"60s" | string + description: *"" | string +} + +// Thin wrapper around kubectl rollout status. +// Timeout and output are managed by kubectl. +#Rollout: { + kind: "rollout" + resource: string // e.g. "deploy/gateway-v4" or "statefulset/redpanda" + namespace?: string + timeout: *"60s" | string + description: *"" | string +} + +// Arbitrary command for checks that don't map to kubectl builtins. +// The engine retries until timeout. +#Exec: { + kind: "exec" + command: string + timeout: *"60s" | string + description: string +} +``` + +## Validation + +`cue vet` validates that every `y-k8s.cue` file conforms to the schema. +This runs without a cluster — it's a static check on the declarations. + +``` +y-cue vet ./... +``` + +This catches: missing required fields, wrong check types, invalid +timeout formats, typos in field names (CUE is closed by default — +unknown fields are errors). + +CI can run `cue vet` to ensure all modules comply before merge. + +## Engine + +The engine in `cue/converge/converge_tool.cue` translates checks +to kubectl commands: + +```cue +// #Wait -> kubectl wait --for=$for --timeout=$timeout $resource [-n $namespace] +// #Rollout -> kubectl rollout status --timeout=$timeout $resource [-n $namespace] +// #Exec -> retry with $timeout: sh -c $command +``` + +`kubectl wait` and `kubectl rollout status` handle their own polling +and timeout — the engine just propagates the timeout value and +passes through stdout/stderr. + +For `#Exec` checks the engine manages the retry loop. + +`kubectl-yconverge` handles the apply modes (create, replace, +serverside, serverside-force, regular). +The engine does not need to know about apply strategies. + +## Modules provide + +Each module has a `y-k8s.cue` that declares its step and checks. +A module with no checks is valid — it just declares the kustomization. + +Example `kafka-v3/y-k8s.cue` (backend with rollout check): + +```cue +package kafka_v3 + +import "yolean.se/ystack/cue/converge" + +step: converge.#Step & { + kustomization: "cluster-local/kafka-v3" + checks: [ + {kind: "rollout", resource: "statefulset/redpanda", namespace: "kafka", timeout: "120s"}, + {kind: "exec", command: "kubectl exec -n kafka redpanda-0 -- rpk cluster info", description: "redpanda cluster healthy"}, + ] +} +``` + +Example `cluster-local/mysql/y-k8s.cue` (backend, no checks needed): + +```cue +package mysql + +import "yolean.se/ystack/cue/converge" + +step: converge.#Step & { + kustomization: "cluster-local/mysql" + checks: [] +} +``` + +Example `gateway-v4/y-k8s.cue` (module with dependencies): + +```cue +package gateway_v4 + +import ( + "yolean.se/ystack/cue/converge" + "yolean.se/checkit/cluster-local/kafka-v3" + "yolean.se/checkit/keycloak-v3" +) + +// Importing kafka_v3 and keycloak_v3 makes their checks +// preconditions for this step. The engine ensures they +// converge and pass before applying gateway-v4. + +step: converge.#Step & { + kustomization: "gateway-v4/site-apply-namespaced" + checks: [ + {kind: "rollout", resource: "deploy/gateway-v4", namespace: "dev"}, + {kind: "wait", resource: "job/setup-topic-gateway-v4-userstate", namespace: "dev", for: "condition=Complete"}, + ] +} +``` + +## Dependency resolution + +The engine collects all `y-k8s.cue` files, inspects their imports, +and builds a topological sort. A step runs only after all imported +steps have converged and their checks pass. + +Import cycles are a CUE compile error — no runtime cycle detection needed. + +## Namespace binding + +`y-site-generate` determines which modules a site needs and which +namespace they target. The CUE engine receives `--context=` and +site name as inputs. Namespace is either set in the kustomization +or passed as a CUE value that templates into check commands. + +## Convergence is cheap + +`kubectl yconverge -k` is idempotent. Re-running a fully converged +step is a no-op (unchanged resources) followed by passing checks. +This means: + +- No "has this been applied" state tracking +- Re-running after a failure retries only what's needed +- Checks serve double duty: post-apply verification AND + precondition for downstream steps + +## CLI surface + +``` +y-cue cmd converge --context=local dev # full site +y-cue cmd converge --context=local dev gateway-v4 # one module + deps +y-cue cmd check --context=local dev # checks only, no apply +y-cue vet ./... # validate all y-k8s.cue +``` + +## Migration path + +1. Add schema to ystack `cue/converge/` +2. Write `y-k8s.cue` for ystack backends (kafka, blobs, builds-registry) +3. Write `y-k8s.cue` for checkit backends (mysql, keycloak-v3) +4. Write `y-k8s.cue` for a few site modules (gateway-v4, events-v1) +5. `cue cmd converge --context=local dev` replaces + `y-cluster-provision-first-site dev` for local clusters +6. Extend to `y-site-upgrade` by adding upgrade-specific checks +7. Extend to non-local clusters by parameterizing context and namespace From f3e61d819f1deb102146c242fc7dcb5939971045 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Thu, 2 Apr 2026 11:46:21 +0000 Subject: [PATCH 05/36] Implement CUE converge engine with y-k8s.cue for all k3s modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schema (cue/converge/schema.cue): #Step, #Check (#Wait, #Rollout, #Exec), #Action Module declarations (k3s/*/y-k8s.cue): 17 modules with typed dependencies, checks, and actions. Dependencies expressed as CUE imports — the import graph IS the DAG. Engine (converge_tool.cue): `y-cue cmd converge -t context=local -t path=$PATH` Prints human-readable converge plan before execution. Delegates all applies to kubectl-yconverge. Translates checks to kubectl wait/rollout/exec commands. Also: add serverside-force label to k3s/10-gateway-api/kustomization.yaml so kubectl-yconverge handles CRD apply correctly. Co-Authored-By: Claude Opus 4.6 (1M context) --- cue.mod/module.cue | 4 + cue/converge-ystack/converge_tool.cue | 103 ++++++++++++++++++++++ cue/converge-ystack/steps.cue | 45 ++++++++++ cue/converge/schema.cue | 59 +++++++++++++ k3s/00-namespace-ystack/y-k8s.cue | 9 ++ k3s/01-namespace-blobs/y-k8s.cue | 9 ++ k3s/02-namespace-kafka/y-k8s.cue | 9 ++ k3s/03-namespace-monitoring/y-k8s.cue | 9 ++ k3s/09-y-kustomize-secrets-init/y-k8s.cue | 14 +++ k3s/10-gateway-api/kustomization.yaml | 2 + k3s/10-gateway-api/y-k8s.cue | 18 ++++ k3s/11-monitoring-operator/y-k8s.cue | 19 ++++ k3s/20-gateway/y-k8s.cue | 25 ++++++ k3s/29-y-kustomize/y-k8s.cue | 21 +++++ k3s/30-blobs-minio-disabled/y-k8s.cue | 10 +++ k3s/30-blobs-ystack/y-k8s.cue | 16 ++++ k3s/30-blobs/y-k8s.cue | 19 ++++ k3s/40-kafka-ystack/y-k8s.cue | 34 +++++++ k3s/40-kafka/y-k8s.cue | 19 ++++ k3s/50-monitoring/y-k8s.cue | 19 ++++ k3s/60-builds-registry/y-k8s.cue | 31 +++++++ k3s/61-prod-registry/y-k8s.cue | 14 +++ k3s/62-buildkit/y-k8s.cue | 19 ++++ 23 files changed, 527 insertions(+) create mode 100644 cue.mod/module.cue create mode 100644 cue/converge-ystack/converge_tool.cue create mode 100644 cue/converge-ystack/steps.cue create mode 100644 cue/converge/schema.cue create mode 100644 k3s/00-namespace-ystack/y-k8s.cue create mode 100644 k3s/01-namespace-blobs/y-k8s.cue create mode 100644 k3s/02-namespace-kafka/y-k8s.cue create mode 100644 k3s/03-namespace-monitoring/y-k8s.cue create mode 100644 k3s/09-y-kustomize-secrets-init/y-k8s.cue create mode 100644 k3s/10-gateway-api/y-k8s.cue create mode 100644 k3s/11-monitoring-operator/y-k8s.cue create mode 100644 k3s/20-gateway/y-k8s.cue create mode 100644 k3s/29-y-kustomize/y-k8s.cue create mode 100644 k3s/30-blobs-minio-disabled/y-k8s.cue create mode 100644 k3s/30-blobs-ystack/y-k8s.cue create mode 100644 k3s/30-blobs/y-k8s.cue create mode 100644 k3s/40-kafka-ystack/y-k8s.cue create mode 100644 k3s/40-kafka/y-k8s.cue create mode 100644 k3s/50-monitoring/y-k8s.cue create mode 100644 k3s/60-builds-registry/y-k8s.cue create mode 100644 k3s/61-prod-registry/y-k8s.cue create mode 100644 k3s/62-buildkit/y-k8s.cue diff --git a/cue.mod/module.cue b/cue.mod/module.cue new file mode 100644 index 0000000..10e646f --- /dev/null +++ b/cue.mod/module.cue @@ -0,0 +1,4 @@ +module: "yolean.se/ystack" +language: { + version: "v0.16.0" +} diff --git a/cue/converge-ystack/converge_tool.cue b/cue/converge-ystack/converge_tool.cue new file mode 100644 index 0000000..2617090 --- /dev/null +++ b/cue/converge-ystack/converge_tool.cue @@ -0,0 +1,103 @@ +package converge_ystack + +import ( + "list" + "strings" + "tool/cli" + "tool/exec" +) + +_context: string @tag(context) +_dryRun: *"false" | "true" @tag(dryRun) +_diff: *"false" | "true" @tag(diff) +_path: string @tag(path) +_kubeconfig: *"" | string @tag(kubeconfig) + +_env: { + CONTEXT: _context + PATH: _path + if _kubeconfig != "" { + KUBECONFIG: _kubeconfig + } +} + +_activeSteps: [for s in steps if s.enabled {s}] + +// Build human-readable plan +_planLines: [for s in _activeSteps { + let _nsLabel = { + if s.namespace != _|_ {" [ns: \(s.namespace)]"} + if s.namespace == _|_ {""} + } + let _header = " \(s.kustomization)\(_nsLabel)" + let _actionLines = [for a in s.actions {" action: \(a.description)"}] + let _checkLines = [for c in s.checks { + if c.kind == "wait" {" check: wait \(c.resource) \(c.for)"} + if c.kind == "rollout" {" check: rollout \(c.resource)"} + if c.kind == "exec" {" check: \(c.description)"} + }] + strings.Join(list.Concat([[_header], _actionLines, _checkLines]), "\n") +}] + +_plan: strings.Join(list.Concat([ + ["=== Converge plan (context=\(_context), dry-run=\(_dryRun), diff=\(_diff)) ==="], + ["Steps (\(len(_activeSteps))):"], + _planLines, + ["==="], +]), "\n") + +command: converge: { + printPlan: cli.Print & { + text: _plan + } + + for i, s in _activeSteps { + let _name = strings.Replace(strings.Replace(s.kustomization, "k3s/", "", 1), "/", "_", -1) + + // Apply via kubectl-yconverge + "apply_\(_name)": exec.Run & { + $after: printPlan + cmd: ["kubectl-yconverge", "--context=\(_context)", "-k", "\(s.kustomization)/"] + env: _env + stdout: string + } + + // Actions (run after apply) + for j, a in s.actions { + "action_\(_name)_\(j)": exec.Run & { + $after: "apply_\(_name)" + cmd: ["sh", "-c", a.command] + env: _env + } + } + + // Checks (run after actions or apply) + for j, c in s.checks { + let _afterTarget = { + if len(s.actions) > 0 {"action_\(_name)_\(len(s.actions) - 1)"} + if len(s.actions) == 0 {"apply_\(_name)"} + } + "check_\(_name)_\(j)": exec.Run & { + $after: _afterTarget + if c.kind == "wait" { + let _nsFlag = { + if c.namespace != _|_ {"-n \(c.namespace) "} + if c.namespace == _|_ {""} + } + cmd: ["sh", "-c", "kubectl --context=\(_context) wait --for=\(c.for) --timeout=\(c.timeout) \(_nsFlag)\(c.resource)"] + } + if c.kind == "rollout" { + let _nsFlag = { + if c.namespace != _|_ {"-n \(c.namespace) "} + if c.namespace == _|_ {""} + } + cmd: ["sh", "-c", "kubectl --context=\(_context) rollout status --timeout=\(c.timeout) \(_nsFlag)\(c.resource)"] + } + if c.kind == "exec" { + cmd: ["sh", "-c", c.command] + } + env: _env + } + } + } +} diff --git a/cue/converge-ystack/steps.cue b/cue/converge-ystack/steps.cue new file mode 100644 index 0000000..224575a --- /dev/null +++ b/cue/converge-ystack/steps.cue @@ -0,0 +1,45 @@ +package converge_ystack + +import ( + "yolean.se/ystack/k3s/00-namespace-ystack:namespace_ystack" + "yolean.se/ystack/k3s/01-namespace-blobs:namespace_blobs" + "yolean.se/ystack/k3s/02-namespace-kafka:namespace_kafka" + "yolean.se/ystack/k3s/03-namespace-monitoring:namespace_monitoring" + "yolean.se/ystack/k3s/09-y-kustomize-secrets-init:y_kustomize_secrets_init" + "yolean.se/ystack/k3s/10-gateway-api:gateway_api" + "yolean.se/ystack/k3s/11-monitoring-operator:monitoring_operator" + "yolean.se/ystack/k3s/20-gateway:gateway" + "yolean.se/ystack/k3s/29-y-kustomize:y_kustomize" + "yolean.se/ystack/k3s/30-blobs-ystack:blobs_ystack" + "yolean.se/ystack/k3s/30-blobs:blobs" + "yolean.se/ystack/k3s/30-blobs-minio-disabled:blobs_minio_disabled" + "yolean.se/ystack/k3s/40-kafka-ystack:kafka_ystack" + "yolean.se/ystack/k3s/40-kafka:kafka" + "yolean.se/ystack/k3s/50-monitoring:monitoring" + "yolean.se/ystack/k3s/60-builds-registry:builds_registry" + "yolean.se/ystack/k3s/61-prod-registry:prod_registry" + "yolean.se/ystack/k3s/62-buildkit:buildkit" +) + +// All steps in dependency order. The order here matches the DAG +// but CUE imports enforce the actual dependency constraints. +steps: [ + namespace_ystack.step, + namespace_blobs.step, + namespace_kafka.step, + namespace_monitoring.step, + y_kustomize_secrets_init.step, + gateway_api.step, + monitoring_operator.step, + gateway.step, + y_kustomize.step, + blobs_ystack.step, + blobs.step, + blobs_minio_disabled.step, + kafka_ystack.step, + kafka.step, + monitoring.step, + builds_registry.step, + prod_registry.step, + buildkit.step, +] diff --git a/cue/converge/schema.cue b/cue/converge/schema.cue new file mode 100644 index 0000000..dafad55 --- /dev/null +++ b/cue/converge/schema.cue @@ -0,0 +1,59 @@ +package converge + +// A convergence step: apply a kustomize base, then verify. +#Step: { + // Path to kustomize directory, relative to repo root. + kustomization: string + // Namespace this step targets. Used for filtering (--exclude-namespace). + namespace?: string + // Set to false to disable this step (e.g. alternative implementations). + enabled: *true | bool + // One-shot mutations that run after apply (not retried). + actions: [...#Action] + // Checks that must pass after apply. Downstream steps that import + // this package use these as preconditions. + // Empty list means the step is ready immediately after apply. + checks: [...#Check] +} + +// Check is a discriminated union. Each variant maps to a kubectl +// subcommand that manages its own timeout and output. +#Check: #Wait | #Rollout | #Exec + +// Thin wrapper around kubectl wait. +// Timeout and output are managed by kubectl. +#Wait: { + kind: "wait" + resource: string // e.g. "pod/redpanda-0" or "crd/gateways.gateway.networking.k8s.io" + for: string // e.g. "condition=Ready" or "condition=Established" + namespace?: string + timeout: *"60s" | string + description: *"" | string +} + +// Thin wrapper around kubectl rollout status. +// Timeout and output are managed by kubectl. +#Rollout: { + kind: "rollout" + resource: string // e.g. "deploy/y-kustomize" or "statefulset/redpanda" + namespace?: string + timeout: *"60s" | string + description: *"" | string +} + +// Arbitrary command for checks that don't map to kubectl builtins. +// The engine retries until timeout. +#Exec: { + kind: "exec" + command: string + timeout: *"60s" | string + description: string +} + +// An imperative action that runs once after apply. +// Unlike checks, actions are not retried -- they either succeed or fail. +#Action: { + kind: "action" + command: string + description: string +} diff --git a/k3s/00-namespace-ystack/y-k8s.cue b/k3s/00-namespace-ystack/y-k8s.cue new file mode 100644 index 0000000..2222a21 --- /dev/null +++ b/k3s/00-namespace-ystack/y-k8s.cue @@ -0,0 +1,9 @@ +package namespace_ystack + +import "yolean.se/ystack/cue/converge" + +step: converge.#Step & { + kustomization: "k3s/00-namespace-ystack" + namespace: "ystack" + checks: [] +} diff --git a/k3s/01-namespace-blobs/y-k8s.cue b/k3s/01-namespace-blobs/y-k8s.cue new file mode 100644 index 0000000..56e395e --- /dev/null +++ b/k3s/01-namespace-blobs/y-k8s.cue @@ -0,0 +1,9 @@ +package namespace_blobs + +import "yolean.se/ystack/cue/converge" + +step: converge.#Step & { + kustomization: "k3s/01-namespace-blobs" + namespace: "blobs" + checks: [] +} diff --git a/k3s/02-namespace-kafka/y-k8s.cue b/k3s/02-namespace-kafka/y-k8s.cue new file mode 100644 index 0000000..c4330ad --- /dev/null +++ b/k3s/02-namespace-kafka/y-k8s.cue @@ -0,0 +1,9 @@ +package namespace_kafka + +import "yolean.se/ystack/cue/converge" + +step: converge.#Step & { + kustomization: "k3s/02-namespace-kafka" + namespace: "kafka" + checks: [] +} diff --git a/k3s/03-namespace-monitoring/y-k8s.cue b/k3s/03-namespace-monitoring/y-k8s.cue new file mode 100644 index 0000000..062ffa2 --- /dev/null +++ b/k3s/03-namespace-monitoring/y-k8s.cue @@ -0,0 +1,9 @@ +package namespace_monitoring + +import "yolean.se/ystack/cue/converge" + +step: converge.#Step & { + kustomization: "k3s/03-namespace-monitoring" + namespace: "monitoring" + checks: [] +} diff --git a/k3s/09-y-kustomize-secrets-init/y-k8s.cue b/k3s/09-y-kustomize-secrets-init/y-k8s.cue new file mode 100644 index 0000000..374d66c --- /dev/null +++ b/k3s/09-y-kustomize-secrets-init/y-k8s.cue @@ -0,0 +1,14 @@ +package y_kustomize_secrets_init + +import ( + "yolean.se/ystack/cue/converge" + "yolean.se/ystack/k3s/00-namespace-ystack:namespace_ystack" +) + +_deps: namespace_ystack.step + +step: converge.#Step & { + kustomization: "k3s/09-y-kustomize-secrets-init" + namespace: "ystack" + checks: [] +} diff --git a/k3s/10-gateway-api/kustomization.yaml b/k3s/10-gateway-api/kustomization.yaml index 195509f..a36bb86 100644 --- a/k3s/10-gateway-api/kustomization.yaml +++ b/k3s/10-gateway-api/kustomization.yaml @@ -1,5 +1,7 @@ # yaml-language-server: $schema=https://json.schemastore.org/kustomization.json apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization +commonLabels: + yolean.se/converge-mode: serverside-force resources: - traefik-gateway-provider.yaml diff --git a/k3s/10-gateway-api/y-k8s.cue b/k3s/10-gateway-api/y-k8s.cue new file mode 100644 index 0000000..fba027d --- /dev/null +++ b/k3s/10-gateway-api/y-k8s.cue @@ -0,0 +1,18 @@ +package gateway_api + +import ( + "yolean.se/ystack/cue/converge" + "yolean.se/ystack/k3s/00-namespace-ystack:namespace_ystack" +) + +_deps: namespace_ystack.step + +step: converge.#Step & { + kustomization: "k3s/10-gateway-api" + checks: [{ + kind: "wait" + resource: "crd/gateways.gateway.networking.k8s.io" + for: "condition=Established" + timeout: "60s" + }] +} diff --git a/k3s/11-monitoring-operator/y-k8s.cue b/k3s/11-monitoring-operator/y-k8s.cue new file mode 100644 index 0000000..0b67cc0 --- /dev/null +++ b/k3s/11-monitoring-operator/y-k8s.cue @@ -0,0 +1,19 @@ +package monitoring_operator + +import ( + "yolean.se/ystack/cue/converge" + "yolean.se/ystack/k3s/03-namespace-monitoring:namespace_monitoring" +) + +_deps: namespace_monitoring.step + +step: converge.#Step & { + kustomization: "k3s/11-monitoring-operator" + namespace: "monitoring" + checks: [{ + kind: "rollout" + resource: "deploy/prometheus-operator" + namespace: "default" + timeout: "120s" + }] +} diff --git a/k3s/20-gateway/y-k8s.cue b/k3s/20-gateway/y-k8s.cue new file mode 100644 index 0000000..d0d7e2b --- /dev/null +++ b/k3s/20-gateway/y-k8s.cue @@ -0,0 +1,25 @@ +package gateway + +import ( + "yolean.se/ystack/cue/converge" + "yolean.se/ystack/k3s/10-gateway-api:gateway_api" +) + +_deps: gateway_api.step + +step: converge.#Step & { + kustomization: "k3s/20-gateway" + namespace: "ystack" + actions: [{ + kind: "action" + command: "y-k8s-ingress-hosts --context=$CONTEXT --ensure" + description: "update /etc/hosts for gateway routes" + }] + checks: [{ + kind: "wait" + resource: "gateway/ystack" + namespace: "ystack" + for: "condition=Programmed" + timeout: "60s" + }] +} diff --git a/k3s/29-y-kustomize/y-k8s.cue b/k3s/29-y-kustomize/y-k8s.cue new file mode 100644 index 0000000..849b1c5 --- /dev/null +++ b/k3s/29-y-kustomize/y-k8s.cue @@ -0,0 +1,21 @@ +package y_kustomize + +import ( + "yolean.se/ystack/cue/converge" + "yolean.se/ystack/k3s/09-y-kustomize-secrets-init:y_kustomize_secrets_init" + "yolean.se/ystack/k3s/20-gateway:gateway" +) + +_dep_secrets: y_kustomize_secrets_init.step +_dep_gateway: gateway.step + +step: converge.#Step & { + kustomization: "k3s/29-y-kustomize" + namespace: "ystack" + checks: [{ + kind: "rollout" + resource: "deploy/y-kustomize" + namespace: "ystack" + timeout: "120s" + }] +} diff --git a/k3s/30-blobs-minio-disabled/y-k8s.cue b/k3s/30-blobs-minio-disabled/y-k8s.cue new file mode 100644 index 0000000..b164a9d --- /dev/null +++ b/k3s/30-blobs-minio-disabled/y-k8s.cue @@ -0,0 +1,10 @@ +package blobs_minio_disabled + +import "yolean.se/ystack/cue/converge" + +step: converge.#Step & { + kustomization: "k3s/30-blobs-minio-disabled" + namespace: "blobs" + enabled: false + checks: [] +} diff --git a/k3s/30-blobs-ystack/y-k8s.cue b/k3s/30-blobs-ystack/y-k8s.cue new file mode 100644 index 0000000..2d54ae0 --- /dev/null +++ b/k3s/30-blobs-ystack/y-k8s.cue @@ -0,0 +1,16 @@ +package blobs_ystack + +import ( + "yolean.se/ystack/cue/converge" + "yolean.se/ystack/k3s/01-namespace-blobs:namespace_blobs" + "yolean.se/ystack/k3s/29-y-kustomize:y_kustomize" +) + +_dep_ns: namespace_blobs.step +_dep_kustomize: y_kustomize.step + +step: converge.#Step & { + kustomization: "k3s/30-blobs-ystack" + namespace: "blobs" + checks: [] +} diff --git a/k3s/30-blobs/y-k8s.cue b/k3s/30-blobs/y-k8s.cue new file mode 100644 index 0000000..b022436 --- /dev/null +++ b/k3s/30-blobs/y-k8s.cue @@ -0,0 +1,19 @@ +package blobs + +import ( + "yolean.se/ystack/cue/converge" + "yolean.se/ystack/k3s/30-blobs-ystack:blobs_ystack" +) + +_deps: blobs_ystack.step + +step: converge.#Step & { + kustomization: "k3s/30-blobs" + namespace: "blobs" + checks: [{ + kind: "rollout" + resource: "deploy/versitygw" + namespace: "blobs" + timeout: "60s" + }] +} diff --git a/k3s/40-kafka-ystack/y-k8s.cue b/k3s/40-kafka-ystack/y-k8s.cue new file mode 100644 index 0000000..1c06252 --- /dev/null +++ b/k3s/40-kafka-ystack/y-k8s.cue @@ -0,0 +1,34 @@ +package kafka_ystack + +import ( + "yolean.se/ystack/cue/converge" + "yolean.se/ystack/k3s/02-namespace-kafka:namespace_kafka" + "yolean.se/ystack/k3s/29-y-kustomize:y_kustomize" +) + +_dep_ns: namespace_kafka.step +_dep_kustomize: y_kustomize.step + +step: converge.#Step & { + kustomization: "k3s/40-kafka-ystack" + namespace: "kafka" + actions: [{ + kind: "action" + command: "kubectl --context=$CONTEXT -n ystack rollout restart deploy/y-kustomize && kubectl --context=$CONTEXT -n ystack rollout status deploy/y-kustomize --timeout=60s" + description: "restart y-kustomize to pick up kafka secrets" + }] + checks: [ + { + kind: "exec" + command: "kubectl --context=$CONTEXT get --raw /api/v1/namespaces/ystack/services/y-kustomize:80/proxy/v1/blobs/setup-bucket-job/base-for-annotations.yaml" + timeout: "60s" + description: "y-kustomize serving blobs bases" + }, + { + kind: "exec" + command: "kubectl --context=$CONTEXT get --raw /api/v1/namespaces/ystack/services/y-kustomize:80/proxy/v1/kafka/setup-topic-job/base-for-annotations.yaml" + timeout: "60s" + description: "y-kustomize serving kafka bases" + }, + ] +} diff --git a/k3s/40-kafka/y-k8s.cue b/k3s/40-kafka/y-k8s.cue new file mode 100644 index 0000000..a717225 --- /dev/null +++ b/k3s/40-kafka/y-k8s.cue @@ -0,0 +1,19 @@ +package kafka + +import ( + "yolean.se/ystack/cue/converge" + "yolean.se/ystack/k3s/40-kafka-ystack:kafka_ystack" +) + +_deps: kafka_ystack.step + +step: converge.#Step & { + kustomization: "k3s/40-kafka" + namespace: "kafka" + checks: [{ + kind: "exec" + command: "kubectl --context=$CONTEXT exec -n kafka redpanda-0 -c redpanda -- rpk cluster info" + timeout: "120s" + description: "redpanda cluster healthy" + }] +} diff --git a/k3s/50-monitoring/y-k8s.cue b/k3s/50-monitoring/y-k8s.cue new file mode 100644 index 0000000..420189c --- /dev/null +++ b/k3s/50-monitoring/y-k8s.cue @@ -0,0 +1,19 @@ +package monitoring + +import ( + "yolean.se/ystack/cue/converge" + "yolean.se/ystack/k3s/11-monitoring-operator:monitoring_operator" +) + +_deps: monitoring_operator.step + +step: converge.#Step & { + kustomization: "k3s/50-monitoring" + namespace: "monitoring" + checks: [{ + kind: "rollout" + resource: "deploy/kube-state-metrics" + namespace: "monitoring" + timeout: "60s" + }] +} diff --git a/k3s/60-builds-registry/y-k8s.cue b/k3s/60-builds-registry/y-k8s.cue new file mode 100644 index 0000000..3912d0e --- /dev/null +++ b/k3s/60-builds-registry/y-k8s.cue @@ -0,0 +1,31 @@ +package builds_registry + +import ( + "yolean.se/ystack/cue/converge" + "yolean.se/ystack/k3s/30-blobs:blobs" + "yolean.se/ystack/k3s/40-kafka-ystack:kafka_ystack" + "yolean.se/ystack/k3s/29-y-kustomize:y_kustomize" +) + +_dep_blobs: blobs.step +_dep_kafka: kafka_ystack.step +_dep_kustomize: y_kustomize.step + +step: converge.#Step & { + kustomization: "k3s/60-builds-registry" + namespace: "ystack" + checks: [ + { + kind: "rollout" + resource: "deploy/registry" + namespace: "ystack" + timeout: "60s" + }, + { + kind: "exec" + command: "kubectl --context=$CONTEXT get --raw /api/v1/namespaces/ystack/services/builds-registry:80/proxy/v2/_catalog" + timeout: "30s" + description: "registry v2 API responds" + }, + ] +} diff --git a/k3s/61-prod-registry/y-k8s.cue b/k3s/61-prod-registry/y-k8s.cue new file mode 100644 index 0000000..3af7472 --- /dev/null +++ b/k3s/61-prod-registry/y-k8s.cue @@ -0,0 +1,14 @@ +package prod_registry + +import ( + "yolean.se/ystack/cue/converge" + "yolean.se/ystack/k3s/00-namespace-ystack:namespace_ystack" +) + +_deps: namespace_ystack.step + +step: converge.#Step & { + kustomization: "k3s/61-prod-registry" + namespace: "ystack" + checks: [] +} diff --git a/k3s/62-buildkit/y-k8s.cue b/k3s/62-buildkit/y-k8s.cue new file mode 100644 index 0000000..c4af64a --- /dev/null +++ b/k3s/62-buildkit/y-k8s.cue @@ -0,0 +1,19 @@ +package buildkit + +import ( + "yolean.se/ystack/cue/converge" + "yolean.se/ystack/k3s/60-builds-registry:builds_registry" +) + +_deps: builds_registry.step + +step: converge.#Step & { + kustomization: "k3s/62-buildkit" + namespace: "ystack" + checks: [{ + kind: "exec" + command: "kubectl --context=$CONTEXT -n ystack get statefulset buildkitd" + timeout: "10s" + description: "buildkitd statefulset exists" + }] +} From b63205bfbab14f0a14a03408d822c504cf558c60 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Thu, 2 Apr 2026 12:21:08 +0000 Subject: [PATCH 06/36] Replace y-cluster-converge-ystack with CUE engine invocation The bash convergence script now delegates to: y-cue cmd converge ./cue/converge-ystack/ Sequential task chaining ensures steps execute in DAG order. Override-ip, KUBECONFIG, and PATH are passed as CUE tags. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/kubectl-yconverge | 1 + bin/y-cluster-converge-ystack | 145 +----------------- cue/converge-ystack/converge_tool.cue | 101 ++++++------ cue/converge/schema.cue | 4 + k3s/10-gateway-api/y-k8s.cue | 8 +- k3s/11-monitoring-operator/kustomization.yaml | 2 + k3s/20-gateway/y-k8s.cue | 17 +- k3s/40-kafka-ystack/y-k8s.cue | 16 +- k3s/40-kafka/y-k8s.cue | 20 ++- 9 files changed, 105 insertions(+), 209 deletions(-) diff --git a/bin/kubectl-yconverge b/bin/kubectl-yconverge index a08d02e..27e19e8 100755 --- a/bin/kubectl-yconverge +++ b/bin/kubectl-yconverge @@ -63,6 +63,7 @@ _apply_if_any() { output=$(kubectl "$@" 2>&1) || { case "$output" in *"no objects passed to apply"*) return 0 ;; + *"not found"*) return 0 ;; # y-script-lint:disable=or-true # selector matched no server-side resources *) printf '%s\n' "$output" >&2; return 1 ;; esac } diff --git a/bin/y-cluster-converge-ystack b/bin/y-cluster-converge-ystack index 03384ed..6d596fc 100755 --- a/bin/y-cluster-converge-ystack +++ b/bin/y-cluster-converge-ystack @@ -19,145 +19,12 @@ done [ -z "$CONTEXT" ] && echo "Usage: y-cluster-converge-ystack --context= [--exclude=SUBSTRING] [--override-ip=IP]" && exit 1 -# Validate --exclude value matches a known namespace directory -if [ -n "$EXCLUDE" ]; then - EXCLUDE_VALID=false - for ns_dir in "$YSTACK_HOME"/k3s/[0-9][0-9]-namespace-*/; do - ns_name=$(basename "$ns_dir") - ns_name="${ns_name#[0-9][0-9]-namespace-}" - if [ "$EXCLUDE" = "$ns_name" ]; then - EXCLUDE_VALID=true - break - fi - done - if [ "$EXCLUDE_VALID" = "false" ]; then - echo "ERROR: --exclude=$EXCLUDE does not match any namespace in k3s/" >&2 - echo "Valid values:" >&2 - for ns_dir in "$YSTACK_HOME"/k3s/[0-9][0-9]-namespace-*/; do - ns_name=$(basename "$ns_dir") - echo " ${ns_name#[0-9][0-9]-namespace-}" >&2 - done - exit 1 - fi -fi - -k() { - kubectl --context="$CONTEXT" "$@" -} - -# HTTP requests to cluster services via the K8s API proxy (works regardless of provisioner) -# Usage: kurl -kurl() { - local ns="$1" svc="$2" path="$3" - k get --raw "/api/v1/namespaces/$ns/services/$svc:80/proxy/$path" -} - -apply_base() { - local base="$1" - local output - output=$(k apply -k "$YSTACK_HOME/k3s/$base/" 2>&1) || { - echo "$output" >&2 - return 1 - } - [ -n "$output" ] && echo "$output" -} - -# List bases in order, filter out -disabled suffix -echo "[y-cluster-converge-ystack] Listing bases" -BASES=() -for dir in "$YSTACK_HOME"/k3s/[0-9][0-9]-*/; do - base=$(basename "$dir") - if [[ "$base" == *-disabled ]]; then - echo "[y-cluster-converge-ystack] Skipping disabled: $base" - continue - fi - if [ -n "$EXCLUDE" ] && [[ "$base" == *"$EXCLUDE"* ]]; then - echo "[y-cluster-converge-ystack] Skipping excluded (--exclude=$EXCLUDE): $base" - continue - fi - BASES+=("$base") -done -echo "[y-cluster-converge-ystack] Bases: ${BASES[*]}" - -prev_digit="" -for base in "${BASES[@]}"; do - digit="${base:0:1}" - - # Between digit groups, wait for readiness - if [ -n "$prev_digit" ] && [ "$digit" != "$prev_digit" ]; then - echo "[y-cluster-converge-ystack] Waiting for rollouts after ${prev_digit}* bases" - - # After CRDs (1*), wait for all of them to be established - if [ "$prev_digit" = "1" ]; then - echo "[y-cluster-converge-ystack] Waiting for all CRDs to be established" - k wait --for=condition=Established crd --all --timeout=60s - fi - - # Wait for all deployments that exist in any namespace - for ns in $(k get deploy --all-namespaces --no-headers -o custom-columns=NS:.metadata.namespace 2>/dev/null | sort -u); do - echo "[y-cluster-converge-ystack] Waiting for deployments in $ns" - k -n "$ns" rollout status deploy --timeout=120s - done - - # After 2* (gateway + y-kustomize), update /etc/hosts so curl can reach services - if [ "$prev_digit" = "2" ]; then - if [ -n "$OVERRIDE_IP" ]; then - echo "[y-cluster-converge-ystack] Annotating gateway with yolean.se/override-ip=$OVERRIDE_IP" - k -n ystack annotate gateway ystack yolean.se/override-ip="$OVERRIDE_IP" --overwrite - fi - if ! "$YSTACK_HOME/bin/y-k8s-ingress-hosts" --context="$CONTEXT" --ensure; then - echo "[y-cluster-converge-ystack] WARNING: /etc/hosts update failed (may need manual sudo)" >&2 - fi - fi - - # After 4* (kafka secrets updated), restart y-kustomize so volume mounts refresh - # without waiting for kubelet sync (can take 60-120s) - if [ "$prev_digit" = "4" ]; then - echo "[y-cluster-converge-ystack] Restarting y-kustomize to pick up updated secrets" - k -n ystack rollout restart deploy/y-kustomize - k -n ystack rollout status deploy/y-kustomize --timeout=60s - fi - - # Before 6* bases, verify y-kustomize serves real content - # Check via API proxy first, then via Traefik (port 80) which is the path kustomize uses - if [ "$digit" = "6" ]; then - echo "[y-cluster-converge-ystack] Verifying y-kustomize API" - kurl ystack y-kustomize health >/dev/null - echo "[y-cluster-converge-ystack] y-kustomize health ok (via API proxy)" - # Verify the Traefik route works (this is the path kustomize uses for HTTP resources) - curl -sSf --retry 5 --retry-delay 2 --retry-all-errors --connect-timeout 2 --max-time 5 \ - http://y-kustomize.ystack.svc.cluster.local/v1/blobs/setup-bucket-job/base-for-annotations.yaml >/dev/null - echo "[y-cluster-converge-ystack] y-kustomize serving blobs bases (via Traefik)" - curl -sSf --retry 5 --retry-delay 2 --retry-all-errors --connect-timeout 2 --max-time 5 \ - http://y-kustomize.ystack.svc.cluster.local/v1/kafka/setup-topic-job/base-for-annotations.yaml >/dev/null - echo "[y-cluster-converge-ystack] y-kustomize serving kafka bases (via Traefik)" - fi - fi - - echo "[y-cluster-converge-ystack] Applying $base" - if [[ "$base" == 1* ]]; then - k apply -k "$YSTACK_HOME/k3s/$base/" --server-side=true --force-conflicts - else - apply_base "$base" - fi - - prev_digit="$digit" -done - -# Update /etc/hosts now that all routes exist -if ! "$YSTACK_HOME/bin/y-k8s-ingress-hosts" --context="$CONTEXT" --ensure; then - echo "[y-cluster-converge-ystack] WARNING: /etc/hosts update failed (may need manual sudo)" >&2 -fi +TAGS="-t context=$CONTEXT -t path=$PATH" +[ -n "$KUBECONFIG" ] && TAGS="$TAGS -t kubeconfig=$KUBECONFIG" -# Validation -echo "[y-cluster-converge-ystack] Validation" -k -n ystack get gateway ystack -k -n ystack get deploy y-kustomize -k -n blobs get svc y-s3-api -k -n kafka get statefulset redpanda -CLUSTER_IP=$(k -n ystack get svc builds-registry -o=jsonpath='{.spec.clusterIP}' 2>/dev/null || echo "") -if [ -n "$CLUSTER_IP" ] && [ "$CLUSTER_IP" != "10.43.0.50" ]; then - echo "[y-cluster-converge-ystack] WARNING: builds-registry clusterIP is $CLUSTER_IP, expected 10.43.0.50" >&2 +# Override-ip is applied as a gateway annotation after the gateway step +if [ -n "$OVERRIDE_IP" ]; then + TAGS="$TAGS -t overrideIP=$OVERRIDE_IP" fi -echo "[y-cluster-converge-ystack] Completed. To verify use: y-cluster-validate-ystack --context=$CONTEXT" +(cd "$YSTACK_HOME" && y-cue cmd converge $TAGS ./cue/converge-ystack/) diff --git a/cue/converge-ystack/converge_tool.cue b/cue/converge-ystack/converge_tool.cue index 2617090..b0e64cc 100644 --- a/cue/converge-ystack/converge_tool.cue +++ b/cue/converge-ystack/converge_tool.cue @@ -7,15 +7,15 @@ import ( "tool/exec" ) -_context: string @tag(context) -_dryRun: *"false" | "true" @tag(dryRun) -_diff: *"false" | "true" @tag(diff) +_context: string @tag(context) _path: string @tag(path) _kubeconfig: *"" | string @tag(kubeconfig) +_overrideIP: *"" | string @tag(overrideIP) _env: { - CONTEXT: _context - PATH: _path + CONTEXT: _context + PATH: _path + OVERRIDE_IP: _overrideIP if _kubeconfig != "" { KUBECONFIG: _kubeconfig } @@ -40,64 +40,59 @@ _planLines: [for s in _activeSteps { }] _plan: strings.Join(list.Concat([ - ["=== Converge plan (context=\(_context), dry-run=\(_dryRun), diff=\(_diff)) ==="], + ["=== Converge plan (context=\(_context)) ==="], ["Steps (\(len(_activeSteps))):"], _planLines, ["==="], ]), "\n") +// Generate shell commands per step, wrapped in error handler +_stepCmds: [for s in _activeSteps { + let _apply = "kubectl-yconverge --context=\(_context) -k \(s.kustomization)/" + let _actionCmds = [for a in s.actions {"echo ' action: \(a.description)' && " + a.command}] + let _checkCmds = [for c in s.checks { + if c.kind == "wait" { + let _ns = { + if c.namespace != _|_ {"-n \(c.namespace) "} + if c.namespace == _|_ {""} + } + "echo ' check: wait \(c.resource)' && kubectl --context=\(_context) wait --for=\(c.for) --timeout=\(c.timeout) \(_ns)\(c.resource)" + } + if c.kind == "rollout" { + let _ns = { + if c.namespace != _|_ {"-n \(c.namespace) "} + if c.namespace == _|_ {""} + } + "echo ' check: rollout \(c.resource)' && kubectl --context=\(_context) rollout status --timeout=\(c.timeout) \(_ns)\(c.resource)" + } + if c.kind == "exec" {"echo ' check: \(c.description)' && { for _retry_i in $(seq 1 15); do " + c.command + " && break || sleep 2; done; }"} + }] + let _body = strings.Join(list.Concat([[_apply], _actionCmds, _checkCmds]), "\n") + "echo '>>> \(s.kustomization)'\nif ! (\n\(_body)\n); then\n echo ''\n echo \"FAILED: \(s.kustomization)\"\n echo 'The step above failed. Re-run to retry from this point.'\n exit 1\nfi" +}] + +_script: strings.Join(list.Concat([ + ["set -eo pipefail"], + _stepCmds, + ["echo '=== Converge complete ==='"], +]), "\n") + +// Write script to temp file so CUE error messages don't dump the entire script command: converge: { printPlan: cli.Print & { text: _plan } - for i, s in _activeSteps { - let _name = strings.Replace(strings.Replace(s.kustomization, "k3s/", "", 1), "/", "_", -1) - - // Apply via kubectl-yconverge - "apply_\(_name)": exec.Run & { - $after: printPlan - cmd: ["kubectl-yconverge", "--context=\(_context)", "-k", "\(s.kustomization)/"] - env: _env - stdout: string - } - - // Actions (run after apply) - for j, a in s.actions { - "action_\(_name)_\(j)": exec.Run & { - $after: "apply_\(_name)" - cmd: ["sh", "-c", a.command] - env: _env - } - } + writeScript: exec.Run & { + $after: printPlan + cmd: ["sh", "-c", "SCRIPT=$(mktemp /tmp/ystack-converge.XXXXXX.sh) && cat > $SCRIPT && echo $SCRIPT"] + stdin: _script + stdout: string + } - // Checks (run after actions or apply) - for j, c in s.checks { - let _afterTarget = { - if len(s.actions) > 0 {"action_\(_name)_\(len(s.actions) - 1)"} - if len(s.actions) == 0 {"apply_\(_name)"} - } - "check_\(_name)_\(j)": exec.Run & { - $after: _afterTarget - if c.kind == "wait" { - let _nsFlag = { - if c.namespace != _|_ {"-n \(c.namespace) "} - if c.namespace == _|_ {""} - } - cmd: ["sh", "-c", "kubectl --context=\(_context) wait --for=\(c.for) --timeout=\(c.timeout) \(_nsFlag)\(c.resource)"] - } - if c.kind == "rollout" { - let _nsFlag = { - if c.namespace != _|_ {"-n \(c.namespace) "} - if c.namespace == _|_ {""} - } - cmd: ["sh", "-c", "kubectl --context=\(_context) rollout status --timeout=\(c.timeout) \(_nsFlag)\(c.resource)"] - } - if c.kind == "exec" { - cmd: ["sh", "-c", c.command] - } - env: _env - } - } + run: exec.Run & { + $after: writeScript + cmd: ["sh", "-c", "sh " + strings.TrimSpace(writeScript.stdout) + "; EXIT=$?; rm -f " + strings.TrimSpace(writeScript.stdout) + "; exit $EXIT"] + env: _env } } diff --git a/cue/converge/schema.cue b/cue/converge/schema.cue index dafad55..c8f668d 100644 --- a/cue/converge/schema.cue +++ b/cue/converge/schema.cue @@ -14,6 +14,10 @@ package converge // this package use these as preconditions. // Empty list means the step is ready immediately after apply. checks: [...#Check] + // True after apply + actions + checks complete successfully. + // Downstream steps reference this to express dependencies. + // Default false; set by the engine at runtime. + up: *false | bool } // Check is a discriminated union. Each variant maps to a kubectl diff --git a/k3s/10-gateway-api/y-k8s.cue b/k3s/10-gateway-api/y-k8s.cue index fba027d..2837b8c 100644 --- a/k3s/10-gateway-api/y-k8s.cue +++ b/k3s/10-gateway-api/y-k8s.cue @@ -10,9 +10,9 @@ _deps: namespace_ystack.step step: converge.#Step & { kustomization: "k3s/10-gateway-api" checks: [{ - kind: "wait" - resource: "crd/gateways.gateway.networking.k8s.io" - for: "condition=Established" - timeout: "60s" + kind: "exec" + command: "for i in $(seq 1 30); do kubectl --context=$CONTEXT wait --for=condition=Established --timeout=2s crd/gateways.gateway.networking.k8s.io 2>/dev/null && break; sleep 2; done && kubectl --context=$CONTEXT wait --for=condition=Established --timeout=5s crd/gateways.gateway.networking.k8s.io" + timeout: "120s" + description: "gateway API CRDs established" }] } diff --git a/k3s/11-monitoring-operator/kustomization.yaml b/k3s/11-monitoring-operator/kustomization.yaml index fe1e4df..682dcdd 100644 --- a/k3s/11-monitoring-operator/kustomization.yaml +++ b/k3s/11-monitoring-operator/kustomization.yaml @@ -1,5 +1,7 @@ # yaml-language-server: $schema=https://json.schemastore.org/kustomization.json apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization +commonLabels: + yolean.se/converge-mode: serverside-force resources: - ../../monitoring/prometheus-operator diff --git a/k3s/20-gateway/y-k8s.cue b/k3s/20-gateway/y-k8s.cue index d0d7e2b..e947230 100644 --- a/k3s/20-gateway/y-k8s.cue +++ b/k3s/20-gateway/y-k8s.cue @@ -10,11 +10,18 @@ _deps: gateway_api.step step: converge.#Step & { kustomization: "k3s/20-gateway" namespace: "ystack" - actions: [{ - kind: "action" - command: "y-k8s-ingress-hosts --context=$CONTEXT --ensure" - description: "update /etc/hosts for gateway routes" - }] + actions: [ + { + kind: "action" + command: "[ -z \"$OVERRIDE_IP\" ] || kubectl --context=$CONTEXT -n ystack annotate gateway ystack yolean.se/override-ip=$OVERRIDE_IP --overwrite" + description: "annotate gateway with override-ip (if set)" + }, + { + kind: "action" + command: "y-k8s-ingress-hosts --context=$CONTEXT --ensure || echo 'WARNING: /etc/hosts update failed (may need manual sudo)'" + description: "update /etc/hosts for gateway routes" + }, + ] checks: [{ kind: "wait" resource: "gateway/ystack" diff --git a/k3s/40-kafka-ystack/y-k8s.cue b/k3s/40-kafka-ystack/y-k8s.cue index 1c06252..9af895f 100644 --- a/k3s/40-kafka-ystack/y-k8s.cue +++ b/k3s/40-kafka-ystack/y-k8s.cue @@ -22,13 +22,25 @@ step: converge.#Step & { kind: "exec" command: "kubectl --context=$CONTEXT get --raw /api/v1/namespaces/ystack/services/y-kustomize:80/proxy/v1/blobs/setup-bucket-job/base-for-annotations.yaml" timeout: "60s" - description: "y-kustomize serving blobs bases" + description: "y-kustomize serving blobs bases (API proxy)" }, { kind: "exec" command: "kubectl --context=$CONTEXT get --raw /api/v1/namespaces/ystack/services/y-kustomize:80/proxy/v1/kafka/setup-topic-job/base-for-annotations.yaml" timeout: "60s" - description: "y-kustomize serving kafka bases" + description: "y-kustomize serving kafka bases (API proxy)" + }, + { + kind: "exec" + command: "curl -sSf --connect-timeout 2 --max-time 5 http://y-kustomize.ystack.svc.cluster.local/v1/blobs/setup-bucket-job/base-for-annotations.yaml >/dev/null" + timeout: "60s" + description: "y-kustomize serving blobs bases (Traefik)" + }, + { + kind: "exec" + command: "curl -sSf --connect-timeout 2 --max-time 5 http://y-kustomize.ystack.svc.cluster.local/v1/kafka/setup-topic-job/base-for-annotations.yaml >/dev/null" + timeout: "60s" + description: "y-kustomize serving kafka bases (Traefik)" }, ] } diff --git a/k3s/40-kafka/y-k8s.cue b/k3s/40-kafka/y-k8s.cue index a717225..815796f 100644 --- a/k3s/40-kafka/y-k8s.cue +++ b/k3s/40-kafka/y-k8s.cue @@ -10,10 +10,18 @@ _deps: kafka_ystack.step step: converge.#Step & { kustomization: "k3s/40-kafka" namespace: "kafka" - checks: [{ - kind: "exec" - command: "kubectl --context=$CONTEXT exec -n kafka redpanda-0 -c redpanda -- rpk cluster info" - timeout: "120s" - description: "redpanda cluster healthy" - }] + checks: [ + { + kind: "rollout" + resource: "statefulset/redpanda" + namespace: "kafka" + timeout: "120s" + }, + { + kind: "exec" + command: "kubectl --context=$CONTEXT exec -n kafka redpanda-0 -c redpanda -- rpk cluster info" + timeout: "30s" + description: "redpanda cluster healthy" + }, + ] } From bbfbae78fc8af0543220e814fa69faca6b9b8f1d Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Thu, 2 Apr 2026 13:44:46 +0000 Subject: [PATCH 07/36] Document yconverge.cue integration and y-kustomize refresh tracking Two proposed design additions: - yconverge.cue: auto-invoke checks from kubectl-yconverge on exit 0 - y-kustomize refresh: hash-based restart tracking via annotation Co-Authored-By: Claude Opus 4.6 (1M context) --- TODO_CONVERGE_DAG.md | 47 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/TODO_CONVERGE_DAG.md b/TODO_CONVERGE_DAG.md index b0ee228..9bd411e 100644 --- a/TODO_CONVERGE_DAG.md +++ b/TODO_CONVERGE_DAG.md @@ -199,6 +199,53 @@ y-cue cmd check --context=local dev # checks only, no apply y-cue vet ./... # validate all y-k8s.cue ``` +## Proposed: yconverge.cue integration with kubectl-yconverge + +Rename `y-k8s.cue` to `yconverge.cue`. The only valid location is +next to a `kustomization.yaml` file. + +When `kubectl yconverge -k ` completes with exit 0, it looks for +`yconverge.cue` in `/`. If found, it invokes the framework to +run that step's checks. This means any script that uses `kubectl yconverge` +automatically gets check verification — no separate orchestration needed. + +One level of `resources:` indirection: if the kustomization has exactly +one `resources:` item pointing to a local directory, and the current +directory has no `yconverge.cue`, look for `yconverge.cue` in that +resource directory. This handles the common pattern where +`cluster-local/kafka-v3/kustomization.yaml` has `resources: [../../kafka-v3/cluster-backend]` +— the checks can live in `kafka-v3/cluster-backend/yconverge.cue`. + +### Trade-off + +Pro: Breaks up monolithic provision scripts. Any `kubectl yconverge -k` +call becomes self-validating. No separate engine invocation needed. + +Con: Adds checking overhead to every `kubectl yconverge` call. In +`y-site-upgrade` which converges many modules in sequence, each apply +would trigger CUE evaluation + checks. Mitigation: checks should be +fast (rollout status and kubectl wait already return quickly when +resources are already ready). Could add `--skip-checks` flag for +batch operations that do their own validation. + +## y-kustomize refresh tracking + +When y-kustomize serves content from secrets mounted as volumes, +it needs a restart when those secrets change. Currently handled by +an explicit action in `40-kafka-ystack`. + +Proposed: y-kustomize stores a hash of its secret contents as an +annotation on its own deployment: + +``` +yolean.se/y-kustomize-secrets-hash: sha256: +``` + +After any step applies secrets in the ystack namespace matching +`y-kustomize.*`, the engine computes the current hash and compares +to the annotation. Restart only on mismatch. This makes re-converge +of a fully converged cluster skip the restart entirely. + ## Migration path 1. Add schema to ystack `cue/converge/` From f9e5aca60a936e9e14b54739197970c0ef31ab1d Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Thu, 2 Apr 2026 14:19:42 +0000 Subject: [PATCH 08/36] Implement yconverge.cue design: rename, auto-check, --skip-checks Rename all y-k8s.cue to yconverge.cue (must be next to kustomization.yaml). kubectl-yconverge now auto-invokes checks from yconverge.cue after successful apply. Supports one level of resources: indirection for finding yconverge.cue in referenced directories. Add --skip-checks flag for batch operations (used by CUE engine which runs its own checks). Checks are also skipped for --dry-run and --diff. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/kubectl-yconverge | 96 ++++++++++++++++++- cue/converge-ystack/converge_tool.cue | 2 +- .../{y-k8s.cue => yconverge.cue} | 0 .../{y-k8s.cue => yconverge.cue} | 0 .../{y-k8s.cue => yconverge.cue} | 0 .../{y-k8s.cue => yconverge.cue} | 0 .../{y-k8s.cue => yconverge.cue} | 0 .../{y-k8s.cue => yconverge.cue} | 0 .../{y-k8s.cue => yconverge.cue} | 0 k3s/20-gateway/{y-k8s.cue => yconverge.cue} | 0 .../{y-k8s.cue => yconverge.cue} | 0 .../{y-k8s.cue => yconverge.cue} | 0 .../{y-k8s.cue => yconverge.cue} | 0 k3s/30-blobs/{y-k8s.cue => yconverge.cue} | 0 .../{y-k8s.cue => yconverge.cue} | 0 k3s/40-kafka/{y-k8s.cue => yconverge.cue} | 0 .../{y-k8s.cue => yconverge.cue} | 0 .../{y-k8s.cue => yconverge.cue} | 0 .../{y-k8s.cue => yconverge.cue} | 0 k3s/62-buildkit/{y-k8s.cue => yconverge.cue} | 0 20 files changed, 96 insertions(+), 2 deletions(-) rename k3s/00-namespace-ystack/{y-k8s.cue => yconverge.cue} (100%) rename k3s/01-namespace-blobs/{y-k8s.cue => yconverge.cue} (100%) rename k3s/02-namespace-kafka/{y-k8s.cue => yconverge.cue} (100%) rename k3s/03-namespace-monitoring/{y-k8s.cue => yconverge.cue} (100%) rename k3s/09-y-kustomize-secrets-init/{y-k8s.cue => yconverge.cue} (100%) rename k3s/10-gateway-api/{y-k8s.cue => yconverge.cue} (100%) rename k3s/11-monitoring-operator/{y-k8s.cue => yconverge.cue} (100%) rename k3s/20-gateway/{y-k8s.cue => yconverge.cue} (100%) rename k3s/29-y-kustomize/{y-k8s.cue => yconverge.cue} (100%) rename k3s/30-blobs-minio-disabled/{y-k8s.cue => yconverge.cue} (100%) rename k3s/30-blobs-ystack/{y-k8s.cue => yconverge.cue} (100%) rename k3s/30-blobs/{y-k8s.cue => yconverge.cue} (100%) rename k3s/40-kafka-ystack/{y-k8s.cue => yconverge.cue} (100%) rename k3s/40-kafka/{y-k8s.cue => yconverge.cue} (100%) rename k3s/50-monitoring/{y-k8s.cue => yconverge.cue} (100%) rename k3s/60-builds-registry/{y-k8s.cue => yconverge.cue} (100%) rename k3s/61-prod-registry/{y-k8s.cue => yconverge.cue} (100%) rename k3s/62-buildkit/{y-k8s.cue => yconverge.cue} (100%) diff --git a/bin/kubectl-yconverge b/bin/kubectl-yconverge index 27e19e8..d2a27a2 100755 --- a/bin/kubectl-yconverge +++ b/bin/kubectl-yconverge @@ -16,12 +16,20 @@ Flags (must come before kustomize args): --context=NAME required --diff=true run kubectl diff instead of apply --dry-run=true pass --dry-run=server to kubectl apply + --skip-checks skip yconverge.cue check invocation after apply + +After a successful apply, if the kustomization directory contains +a yconverge.cue file, checks declared in it are executed. +If no yconverge.cue exists in the directory but it has exactly one +local resources: entry, checks are looked up there instead. Honors KUBECONFIG if set. ' && exit 0 DIFF=false DRY_RUN=false +SKIP_CHECKS=false +KUSTOMIZE_DIR="" ctx=$1 case $ctx in @@ -33,15 +41,28 @@ while true; do case "${1:-}" in --diff=true) DIFF=true; shift ;; --dry-run=true) DRY_RUN=true; shift ;; + --skip-checks) SKIP_CHECKS=true; shift ;; *) break ;; esac done +# Extract -k directory from args for yconverge.cue lookup for arg in "$@"; do case "$arg" in -l|--selector) echo "Error: yconverge can not be combined with other selectors" >&2 && exit 1 ;; esac done +_prev="" +for arg in "$@"; do + if [ "$_prev" = "-k" ]; then + KUSTOMIZE_DIR="$arg" + break + fi + case "$arg" in + -k) _prev="-k" ;; + -k*) KUSTOMIZE_DIR="${arg#-k}"; break ;; + esac +done if [ "$DIFF" = "true" ]; then kubectl $ctx diff "$@" @@ -63,7 +84,7 @@ _apply_if_any() { output=$(kubectl "$@" 2>&1) || { case "$output" in *"no objects passed to apply"*) return 0 ;; - *"not found"*) return 0 ;; # y-script-lint:disable=or-true # selector matched no server-side resources + *"not found"*) return 0 ;; *) printf '%s\n' "$output" >&2; return 1 ;; esac } @@ -73,3 +94,76 @@ _apply_if_any() { _apply_if_any $ctx apply --server-side --force-conflicts $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside-force "$@" _apply_if_any $ctx apply --server-side $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside "$@" _apply_if_any $ctx apply $DRY_RUN_FLAG --selector='yolean.se/converge-mode!=create,yolean.se/converge-mode!=serverside,yolean.se/converge-mode!=serverside-force' "$@" + +# --- yconverge.cue check invocation --- + +if [ "$SKIP_CHECKS" = "true" ] || [ "$DRY_RUN" = "true" ] || [ "$DIFF" = "true" ]; then + exit 0 +fi + +# Strip trailing slash for consistent lookup +KUSTOMIZE_DIR="${KUSTOMIZE_DIR%/}" + +if [ -z "$KUSTOMIZE_DIR" ]; then + exit 0 +fi + +# Find yconverge.cue: direct or one level of resources indirection +_find_yconverge() { + dir="$1" + if [ -f "$dir/yconverge.cue" ]; then + echo "$dir/yconverge.cue" + return 0 + fi + # One level indirection: if kustomization has exactly one local resources entry + if [ -f "$dir/kustomization.yaml" ]; then + resources=$(y-yq eval '.resources // [] | .[] | select(test("^[^h]") and test("^[^.]"))' "$dir/kustomization.yaml" 2>/dev/null) + count=$(printf '%s\n' "$resources" | grep -c . 2>/dev/null) + if [ "$count" = "1" ] && [ -d "$dir/$resources" ]; then + if [ -f "$dir/$resources/yconverge.cue" ]; then + echo "$dir/$resources/yconverge.cue" + return 0 + fi + fi + fi + return 1 +} + +YCONVERGE_FILE=$(_find_yconverge "$KUSTOMIZE_DIR") || exit 0 + +CONTEXT="${ctx#--context=}" +export CONTEXT + +echo " [yconverge] found $YCONVERGE_FILE" + +# Read checks from the CUE file and execute them +# Extract check commands using y-cue eval +YCONVERGE_DIR=$(dirname "$YCONVERGE_FILE") +CHECKS=$(y-cue eval "$YCONVERGE_DIR" -e 'step.checks' --out json 2>/dev/null) || exit 0 + +if [ "$CHECKS" = "[]" ] || [ -z "$CHECKS" ]; then + exit 0 +fi + +echo "$CHECKS" | y-yq -oj eval '.[] | .kind + "|||" + (.description // "") + "|||" + (.resource // "") + "|||" + (.for // "") + "|||" + (.namespace // "") + "|||" + (.timeout // "60s") + "|||" + (.command // "")' - 2>/dev/null | while IFS='|||' read -r kind desc resource forcond ns timeout command; do + case "$kind" in + wait) + ns_flag="" + [ -n "$ns" ] && ns_flag="-n $ns" + echo " [yconverge] check: wait $resource $forcond" + kubectl --context="$CONTEXT" wait --for="$forcond" --timeout="$timeout" $ns_flag "$resource" + ;; + rollout) + ns_flag="" + [ -n "$ns" ] && ns_flag="-n $ns" + echo " [yconverge] check: rollout $resource" + kubectl --context="$CONTEXT" rollout status --timeout="$timeout" $ns_flag "$resource" + ;; + exec) + echo " [yconverge] check: $desc" + for _retry_i in $(seq 1 15); do + sh -c "$command" && break || sleep 2 # y-script-lint:disable=or-true # retry loop + done + ;; + esac +done diff --git a/cue/converge-ystack/converge_tool.cue b/cue/converge-ystack/converge_tool.cue index b0e64cc..b1b0486 100644 --- a/cue/converge-ystack/converge_tool.cue +++ b/cue/converge-ystack/converge_tool.cue @@ -48,7 +48,7 @@ _plan: strings.Join(list.Concat([ // Generate shell commands per step, wrapped in error handler _stepCmds: [for s in _activeSteps { - let _apply = "kubectl-yconverge --context=\(_context) -k \(s.kustomization)/" + let _apply = "kubectl-yconverge --context=\(_context) --skip-checks -k \(s.kustomization)/" let _actionCmds = [for a in s.actions {"echo ' action: \(a.description)' && " + a.command}] let _checkCmds = [for c in s.checks { if c.kind == "wait" { diff --git a/k3s/00-namespace-ystack/y-k8s.cue b/k3s/00-namespace-ystack/yconverge.cue similarity index 100% rename from k3s/00-namespace-ystack/y-k8s.cue rename to k3s/00-namespace-ystack/yconverge.cue diff --git a/k3s/01-namespace-blobs/y-k8s.cue b/k3s/01-namespace-blobs/yconverge.cue similarity index 100% rename from k3s/01-namespace-blobs/y-k8s.cue rename to k3s/01-namespace-blobs/yconverge.cue diff --git a/k3s/02-namespace-kafka/y-k8s.cue b/k3s/02-namespace-kafka/yconverge.cue similarity index 100% rename from k3s/02-namespace-kafka/y-k8s.cue rename to k3s/02-namespace-kafka/yconverge.cue diff --git a/k3s/03-namespace-monitoring/y-k8s.cue b/k3s/03-namespace-monitoring/yconverge.cue similarity index 100% rename from k3s/03-namespace-monitoring/y-k8s.cue rename to k3s/03-namespace-monitoring/yconverge.cue diff --git a/k3s/09-y-kustomize-secrets-init/y-k8s.cue b/k3s/09-y-kustomize-secrets-init/yconverge.cue similarity index 100% rename from k3s/09-y-kustomize-secrets-init/y-k8s.cue rename to k3s/09-y-kustomize-secrets-init/yconverge.cue diff --git a/k3s/10-gateway-api/y-k8s.cue b/k3s/10-gateway-api/yconverge.cue similarity index 100% rename from k3s/10-gateway-api/y-k8s.cue rename to k3s/10-gateway-api/yconverge.cue diff --git a/k3s/11-monitoring-operator/y-k8s.cue b/k3s/11-monitoring-operator/yconverge.cue similarity index 100% rename from k3s/11-monitoring-operator/y-k8s.cue rename to k3s/11-monitoring-operator/yconverge.cue diff --git a/k3s/20-gateway/y-k8s.cue b/k3s/20-gateway/yconverge.cue similarity index 100% rename from k3s/20-gateway/y-k8s.cue rename to k3s/20-gateway/yconverge.cue diff --git a/k3s/29-y-kustomize/y-k8s.cue b/k3s/29-y-kustomize/yconverge.cue similarity index 100% rename from k3s/29-y-kustomize/y-k8s.cue rename to k3s/29-y-kustomize/yconverge.cue diff --git a/k3s/30-blobs-minio-disabled/y-k8s.cue b/k3s/30-blobs-minio-disabled/yconverge.cue similarity index 100% rename from k3s/30-blobs-minio-disabled/y-k8s.cue rename to k3s/30-blobs-minio-disabled/yconverge.cue diff --git a/k3s/30-blobs-ystack/y-k8s.cue b/k3s/30-blobs-ystack/yconverge.cue similarity index 100% rename from k3s/30-blobs-ystack/y-k8s.cue rename to k3s/30-blobs-ystack/yconverge.cue diff --git a/k3s/30-blobs/y-k8s.cue b/k3s/30-blobs/yconverge.cue similarity index 100% rename from k3s/30-blobs/y-k8s.cue rename to k3s/30-blobs/yconverge.cue diff --git a/k3s/40-kafka-ystack/y-k8s.cue b/k3s/40-kafka-ystack/yconverge.cue similarity index 100% rename from k3s/40-kafka-ystack/y-k8s.cue rename to k3s/40-kafka-ystack/yconverge.cue diff --git a/k3s/40-kafka/y-k8s.cue b/k3s/40-kafka/yconverge.cue similarity index 100% rename from k3s/40-kafka/y-k8s.cue rename to k3s/40-kafka/yconverge.cue diff --git a/k3s/50-monitoring/y-k8s.cue b/k3s/50-monitoring/yconverge.cue similarity index 100% rename from k3s/50-monitoring/y-k8s.cue rename to k3s/50-monitoring/yconverge.cue diff --git a/k3s/60-builds-registry/y-k8s.cue b/k3s/60-builds-registry/yconverge.cue similarity index 100% rename from k3s/60-builds-registry/y-k8s.cue rename to k3s/60-builds-registry/yconverge.cue diff --git a/k3s/61-prod-registry/y-k8s.cue b/k3s/61-prod-registry/yconverge.cue similarity index 100% rename from k3s/61-prod-registry/y-k8s.cue rename to k3s/61-prod-registry/yconverge.cue diff --git a/k3s/62-buildkit/y-k8s.cue b/k3s/62-buildkit/yconverge.cue similarity index 100% rename from k3s/62-buildkit/y-k8s.cue rename to k3s/62-buildkit/yconverge.cue From ae8c32d9e561a65dbb41b35a293e6da3d6a40756 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Thu, 2 Apr 2026 14:39:56 +0000 Subject: [PATCH 09/36] Add itest suite and implement prechecks/postchecks in kubectl-yconverge Integration tests (cue/itest/) using kwok as lightweight test cluster: 17 tests covering schema validation, auto-checks, dependency preconditions, transitive deps, disabled steps, resources indirection, idempotency, error reporting, and --skip-checks. kubectl-yconverge now: - Finds yconverge.cue next to kustomization.yaml (or one level indirection) - Runs step.prechecks BEFORE apply (dependency checks) - Runs step.checks AFTER apply (this step's verification) - Skips entirely when enabled: false Schema adds prechecks field to #Step for explicit dependency check export. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/kubectl-yconverge | 184 +++++++++-------- cue/converge/schema.cue | 3 + cue/itest/example-configmap/configmap.yaml | 6 + .../example-configmap/kustomization.yaml | 5 + cue/itest/example-configmap/yconverge.cue | 18 ++ cue/itest/example-disabled/configmap.yaml | 6 + cue/itest/example-disabled/kustomization.yaml | 5 + cue/itest/example-disabled/yconverge.cue | 15 ++ cue/itest/example-indirect/kustomization.yaml | 4 + .../example-namespace/kustomization.yaml | 4 + cue/itest/example-namespace/namespace.yaml | 4 + cue/itest/example-namespace/yconverge.cue | 14 ++ .../example-with-dependency/configmap.yaml | 6 + .../kustomization.yaml | 5 + .../example-with-dependency/yconverge.cue | 18 ++ cue/itest/test.sh | 187 ++++++++++++++++++ 16 files changed, 405 insertions(+), 79 deletions(-) create mode 100644 cue/itest/example-configmap/configmap.yaml create mode 100644 cue/itest/example-configmap/kustomization.yaml create mode 100644 cue/itest/example-configmap/yconverge.cue create mode 100644 cue/itest/example-disabled/configmap.yaml create mode 100644 cue/itest/example-disabled/kustomization.yaml create mode 100644 cue/itest/example-disabled/yconverge.cue create mode 100644 cue/itest/example-indirect/kustomization.yaml create mode 100644 cue/itest/example-namespace/kustomization.yaml create mode 100644 cue/itest/example-namespace/namespace.yaml create mode 100644 cue/itest/example-namespace/yconverge.cue create mode 100644 cue/itest/example-with-dependency/configmap.yaml create mode 100644 cue/itest/example-with-dependency/kustomization.yaml create mode 100644 cue/itest/example-with-dependency/yconverge.cue create mode 100755 cue/itest/test.sh diff --git a/bin/kubectl-yconverge b/bin/kubectl-yconverge index d2a27a2..4d4f1c8 100755 --- a/bin/kubectl-yconverge +++ b/bin/kubectl-yconverge @@ -16,12 +16,12 @@ Flags (must come before kustomize args): --context=NAME required --diff=true run kubectl diff instead of apply --dry-run=true pass --dry-run=server to kubectl apply - --skip-checks skip yconverge.cue check invocation after apply + --skip-checks skip yconverge.cue check invocation -After a successful apply, if the kustomization directory contains -a yconverge.cue file, checks declared in it are executed. -If no yconverge.cue exists in the directory but it has exactly one -local resources: entry, checks are looked up there instead. +If the kustomization directory contains a yconverge.cue file (or one is +found one level of resources: indirection away), precondition checks from +dependencies run before apply, and this steps checks run after apply. +Steps with enabled: false are skipped entirely. Honors KUBECONFIG if set. ' && exit 0 @@ -46,7 +46,7 @@ while true; do esac done -# Extract -k directory from args for yconverge.cue lookup +# Extract -k directory from args for arg in "$@"; do case "$arg" in -l|--selector) echo "Error: yconverge can not be combined with other selectors" >&2 && exit 1 ;; @@ -64,6 +64,101 @@ for arg in "$@"; do esac done +# --- yconverge.cue lookup --- + +KUSTOMIZE_DIR="${KUSTOMIZE_DIR%/}" +YCONVERGE_DIR="" + +_find_yconverge_dir() { + dir="$1" + if [ -f "$dir/yconverge.cue" ]; then + echo "$dir" + return 0 + fi + # One level indirection: single local resources entry + if [ -f "$dir/kustomization.yaml" ]; then + resources=$(y-yq '.resources // [] | .[] | select(test("^[^h]") and test("^(http|github)") | not)' "$dir/kustomization.yaml" 2>/dev/null) + count=$(printf '%s\n' "$resources" | grep -c . 2>/dev/null) + if [ "$count" = "1" ] && [ -d "$dir/$resources" ] && [ -f "$dir/$resources/yconverge.cue" ]; then + echo "$dir/$resources" + return 0 + fi + fi + return 1 +} + +if [ -n "$KUSTOMIZE_DIR" ] && [ "$SKIP_CHECKS" = "false" ] && [ "$DRY_RUN" = "false" ] && [ "$DIFF" = "false" ]; then + YCONVERGE_DIR=$(_find_yconverge_dir "$KUSTOMIZE_DIR") || true # y-script-lint:disable=or-true # no yconverge.cue is ok +fi + +CONTEXT="${ctx#--context=}" +export CONTEXT + +# --- yconverge.cue: check enabled, run preconditions --- + +_run_checks() { + checks_json="$1" + label="$2" + [ -z "$checks_json" ] || [ "$checks_json" = "[]" ] && return 0 + count=$(echo "$checks_json" | y-yq '. | length' - 2>/dev/null) + [ "$count" = "0" ] && return 0 + i=0 + while [ "$i" -lt "$count" ]; do + kind=$(echo "$checks_json" | y-yq ".[$i].kind" -) + desc=$(echo "$checks_json" | y-yq ".[$i].description // \"\"" -) + resource=$(echo "$checks_json" | y-yq ".[$i].resource // \"\"" -) + forcond=$(echo "$checks_json" | y-yq ".[$i].for // \"\"" -) + ns=$(echo "$checks_json" | y-yq ".[$i].namespace // \"\"" -) + timeout=$(echo "$checks_json" | y-yq ".[$i].timeout // \"60s\"" -) + command=$(echo "$checks_json" | y-yq ".[$i].command // \"\"" -) + ns_flag="" + [ -n "$ns" ] && ns_flag="-n $ns" + case "$kind" in + wait) + echo " [yconverge] $label wait $resource $forcond" + kubectl --context="$CONTEXT" wait --for="$forcond" --timeout="$timeout" $ns_flag "$resource" + ;; + rollout) + echo " [yconverge] $label rollout $resource" + kubectl --context="$CONTEXT" rollout status --timeout="$timeout" $ns_flag "$resource" + ;; + exec) + echo " [yconverge] $label $desc" + for _retry_i in $(seq 1 15); do + sh -c "$command" && break || sleep 2 # y-script-lint:disable=or-true # retry loop + done + ;; + esac + i=$((i + 1)) + done +} + +if [ -n "$YCONVERGE_DIR" ]; then + echo " [yconverge] found $YCONVERGE_DIR/yconverge.cue" + + # Ensure path starts with ./ for CUE eval + case "$YCONVERGE_DIR" in + ./*|/*) ;; # already prefixed + *) YCONVERGE_DIR="./$YCONVERGE_DIR" ;; + esac + + # Check if step is disabled + ENABLED=$(y-cue eval "$YCONVERGE_DIR" -e 'step.enabled' 2>/dev/null) || ENABLED="true" + if [ "$ENABLED" = "false" ]; then + echo " [yconverge] step disabled, skipping" + exit 0 + fi + + # Run dependency precondition checks + PRECHECKS=$(y-cue eval "$YCONVERGE_DIR" -e 'step.prechecks' --out json 2>/dev/null) || true # y-script-lint:disable=or-true # no prechecks is ok + if [ -n "$PRECHECKS" ] && [ "$PRECHECKS" != "[]" ]; then + echo " [yconverge] running precondition checks" + _run_checks "$PRECHECKS" "precheck:" + fi +fi + +# --- apply --- + if [ "$DIFF" = "true" ]; then kubectl $ctx diff "$@" exit $? @@ -72,14 +167,11 @@ fi DRY_RUN_FLAG="" [ "$DRY_RUN" = "true" ] && DRY_RUN_FLAG="--dry-run=server" -# Each mode applies only to resources with the matching label. -# create and delete may find no matching resources — that's expected. kubectl $ctx create --save-config $DRY_RUN_FLAG --selector=yolean.se/converge-mode=create "$@" 2>/dev/null || true # y-script-lint:disable=or-true # idempotent, ignore already-exists if [ "$DRY_RUN" = "false" ]; then kubectl $ctx delete --selector=yolean.se/converge-mode=replace "$@" 2>/dev/null || true # y-script-lint:disable=or-true # ignore not-found fi -# "no objects passed to apply" means nothing matched the selector — not an error. _apply_if_any() { output=$(kubectl "$@" 2>&1) || { case "$output" in @@ -95,75 +187,9 @@ _apply_if_any $ctx apply --server-side --force-conflicts $DRY_RUN_FLAG --selecto _apply_if_any $ctx apply --server-side $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside "$@" _apply_if_any $ctx apply $DRY_RUN_FLAG --selector='yolean.se/converge-mode!=create,yolean.se/converge-mode!=serverside,yolean.se/converge-mode!=serverside-force' "$@" -# --- yconverge.cue check invocation --- - -if [ "$SKIP_CHECKS" = "true" ] || [ "$DRY_RUN" = "true" ] || [ "$DIFF" = "true" ]; then - exit 0 -fi - -# Strip trailing slash for consistent lookup -KUSTOMIZE_DIR="${KUSTOMIZE_DIR%/}" - -if [ -z "$KUSTOMIZE_DIR" ]; then - exit 0 -fi - -# Find yconverge.cue: direct or one level of resources indirection -_find_yconverge() { - dir="$1" - if [ -f "$dir/yconverge.cue" ]; then - echo "$dir/yconverge.cue" - return 0 - fi - # One level indirection: if kustomization has exactly one local resources entry - if [ -f "$dir/kustomization.yaml" ]; then - resources=$(y-yq eval '.resources // [] | .[] | select(test("^[^h]") and test("^[^.]"))' "$dir/kustomization.yaml" 2>/dev/null) - count=$(printf '%s\n' "$resources" | grep -c . 2>/dev/null) - if [ "$count" = "1" ] && [ -d "$dir/$resources" ]; then - if [ -f "$dir/$resources/yconverge.cue" ]; then - echo "$dir/$resources/yconverge.cue" - return 0 - fi - fi - fi - return 1 -} - -YCONVERGE_FILE=$(_find_yconverge "$KUSTOMIZE_DIR") || exit 0 - -CONTEXT="${ctx#--context=}" -export CONTEXT - -echo " [yconverge] found $YCONVERGE_FILE" - -# Read checks from the CUE file and execute them -# Extract check commands using y-cue eval -YCONVERGE_DIR=$(dirname "$YCONVERGE_FILE") -CHECKS=$(y-cue eval "$YCONVERGE_DIR" -e 'step.checks' --out json 2>/dev/null) || exit 0 +# --- yconverge.cue: post-apply checks --- -if [ "$CHECKS" = "[]" ] || [ -z "$CHECKS" ]; then - exit 0 +if [ -n "$YCONVERGE_DIR" ]; then + CHECKS=$(y-cue eval "$YCONVERGE_DIR" -e 'step.checks' --out json 2>/dev/null) || true # y-script-lint:disable=or-true # no checks is ok + _run_checks "$CHECKS" "check:" fi - -echo "$CHECKS" | y-yq -oj eval '.[] | .kind + "|||" + (.description // "") + "|||" + (.resource // "") + "|||" + (.for // "") + "|||" + (.namespace // "") + "|||" + (.timeout // "60s") + "|||" + (.command // "")' - 2>/dev/null | while IFS='|||' read -r kind desc resource forcond ns timeout command; do - case "$kind" in - wait) - ns_flag="" - [ -n "$ns" ] && ns_flag="-n $ns" - echo " [yconverge] check: wait $resource $forcond" - kubectl --context="$CONTEXT" wait --for="$forcond" --timeout="$timeout" $ns_flag "$resource" - ;; - rollout) - ns_flag="" - [ -n "$ns" ] && ns_flag="-n $ns" - echo " [yconverge] check: rollout $resource" - kubectl --context="$CONTEXT" rollout status --timeout="$timeout" $ns_flag "$resource" - ;; - exec) - echo " [yconverge] check: $desc" - for _retry_i in $(seq 1 15); do - sh -c "$command" && break || sleep 2 # y-script-lint:disable=or-true # retry loop - done - ;; - esac -done diff --git a/cue/converge/schema.cue b/cue/converge/schema.cue index c8f668d..6e5a9b3 100644 --- a/cue/converge/schema.cue +++ b/cue/converge/schema.cue @@ -10,6 +10,9 @@ package converge enabled: *true | bool // One-shot mutations that run after apply (not retried). actions: [...#Action] + // Precondition checks from dependencies. Modules populate this + // from their imported dependencies' checks. + prechecks: [...#Check] // Checks that must pass after apply. Downstream steps that import // this package use these as preconditions. // Empty list means the step is ready immediately after apply. diff --git a/cue/itest/example-configmap/configmap.yaml b/cue/itest/example-configmap/configmap.yaml new file mode 100644 index 0000000..1f0e5e9 --- /dev/null +++ b/cue/itest/example-configmap/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: itest-config +data: + key: value diff --git a/cue/itest/example-configmap/kustomization.yaml b/cue/itest/example-configmap/kustomization.yaml new file mode 100644 index 0000000..a29fc9b --- /dev/null +++ b/cue/itest/example-configmap/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: itest +resources: +- configmap.yaml diff --git a/cue/itest/example-configmap/yconverge.cue b/cue/itest/example-configmap/yconverge.cue new file mode 100644 index 0000000..de3076e --- /dev/null +++ b/cue/itest/example-configmap/yconverge.cue @@ -0,0 +1,18 @@ +package example_configmap + +import ( + "yolean.se/ystack/cue/converge" + "yolean.se/ystack/cue/itest/example-namespace:example_namespace" +) + +step: converge.#Step & { + kustomization: "cue/itest/example-configmap" + namespace: "itest" + prechecks: example_namespace.step.checks + checks: [{ + kind: "exec" + command: "kubectl --context=$CONTEXT -n itest get configmap itest-config" + timeout: "10s" + description: "configmap exists" + }] +} diff --git a/cue/itest/example-disabled/configmap.yaml b/cue/itest/example-disabled/configmap.yaml new file mode 100644 index 0000000..16a7857 --- /dev/null +++ b/cue/itest/example-disabled/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: itest-should-not-exist +data: + disabled: "true" diff --git a/cue/itest/example-disabled/kustomization.yaml b/cue/itest/example-disabled/kustomization.yaml new file mode 100644 index 0000000..a29fc9b --- /dev/null +++ b/cue/itest/example-disabled/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: itest +resources: +- configmap.yaml diff --git a/cue/itest/example-disabled/yconverge.cue b/cue/itest/example-disabled/yconverge.cue new file mode 100644 index 0000000..1ae1e28 --- /dev/null +++ b/cue/itest/example-disabled/yconverge.cue @@ -0,0 +1,15 @@ +package example_disabled + +import "yolean.se/ystack/cue/converge" + +step: converge.#Step & { + kustomization: "cue/itest/example-disabled" + namespace: "itest" + enabled: false + checks: [{ + kind: "exec" + command: "false" + timeout: "5s" + description: "should never run" + }] +} diff --git a/cue/itest/example-indirect/kustomization.yaml b/cue/itest/example-indirect/kustomization.yaml new file mode 100644 index 0000000..033d2f8 --- /dev/null +++ b/cue/itest/example-indirect/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- ../example-namespace diff --git a/cue/itest/example-namespace/kustomization.yaml b/cue/itest/example-namespace/kustomization.yaml new file mode 100644 index 0000000..c313b54 --- /dev/null +++ b/cue/itest/example-namespace/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- namespace.yaml diff --git a/cue/itest/example-namespace/namespace.yaml b/cue/itest/example-namespace/namespace.yaml new file mode 100644 index 0000000..a751051 --- /dev/null +++ b/cue/itest/example-namespace/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: itest diff --git a/cue/itest/example-namespace/yconverge.cue b/cue/itest/example-namespace/yconverge.cue new file mode 100644 index 0000000..391becc --- /dev/null +++ b/cue/itest/example-namespace/yconverge.cue @@ -0,0 +1,14 @@ +package example_namespace + +import "yolean.se/ystack/cue/converge" + +step: converge.#Step & { + kustomization: "cue/itest/example-namespace" + namespace: "itest" + checks: [{ + kind: "wait" + resource: "ns/itest" + for: "jsonpath={.status.phase}=Active" + timeout: "10s" + }] +} diff --git a/cue/itest/example-with-dependency/configmap.yaml b/cue/itest/example-with-dependency/configmap.yaml new file mode 100644 index 0000000..578b383 --- /dev/null +++ b/cue/itest/example-with-dependency/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: itest-dependent +data: + depends-on: itest-config diff --git a/cue/itest/example-with-dependency/kustomization.yaml b/cue/itest/example-with-dependency/kustomization.yaml new file mode 100644 index 0000000..a29fc9b --- /dev/null +++ b/cue/itest/example-with-dependency/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: itest +resources: +- configmap.yaml diff --git a/cue/itest/example-with-dependency/yconverge.cue b/cue/itest/example-with-dependency/yconverge.cue new file mode 100644 index 0000000..f5a4831 --- /dev/null +++ b/cue/itest/example-with-dependency/yconverge.cue @@ -0,0 +1,18 @@ +package example_with_dependency + +import ( + "yolean.se/ystack/cue/converge" + "yolean.se/ystack/cue/itest/example-configmap:example_configmap" +) + +step: converge.#Step & { + kustomization: "cue/itest/example-with-dependency" + namespace: "itest" + prechecks: example_configmap.step.checks + checks: [{ + kind: "exec" + command: "kubectl --context=$CONTEXT -n itest get configmap itest-dependent" + timeout: "10s" + description: "dependent configmap exists" + }] +} diff --git a/cue/itest/test.sh b/cue/itest/test.sh new file mode 100755 index 0000000..1378c8f --- /dev/null +++ b/cue/itest/test.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env bash +[ -z "$DEBUG" ] || set -x +set -eo pipefail + +[ "$1" = "help" ] && echo ' +Integration tests for the yconverge framework. +Uses kwok (registry.k8s.io/kwok/cluster) as a lightweight test cluster. + +Requires: docker, kubectl, y-cue, kubectl-yconverge +' && exit 0 + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +YSTACK_HOME="$(cd "$SCRIPT_DIR/../.." && pwd)" +CONTAINER_NAME="yconverge-itest-$$" +CTX="yconverge-itest" +PASS=0 +FAIL=0 + +pass() { PASS=$((PASS + 1)); echo " PASS $1"; } +fail() { FAIL=$((FAIL + 1)); echo " FAIL $1"; } + +cleanup() { + echo "# Cleaning up ..." + docker rm -f "$CONTAINER_NAME" 2>/dev/null || true # y-script-lint:disable=or-true # best-effort cleanup + kubectl config delete-context "$CTX" 2>/dev/null || true # y-script-lint:disable=or-true # best-effort cleanup +} +trap cleanup EXIT + +echo "=== yconverge framework integration tests ===" + +# --- start kwok cluster --- + +echo "# Starting kwok cluster ..." +docker run -d --name "$CONTAINER_NAME" \ + -p 0:8080 \ + registry.k8s.io/kwok/cluster:v0.7.0-k8s.v1.33.0 +PORT=$(docker port "$CONTAINER_NAME" 8080 | head -1 | cut -d: -f2) + +# Wait for API server +for i in $(seq 1 30); do + kubectl --server="http://127.0.0.1:$PORT" get ns default >/dev/null 2>&1 && break + sleep 1 +done + +# Set up context +kubectl config set-cluster "$CTX" --server="http://127.0.0.1:$PORT" >/dev/null +kubectl config set-context "$CTX" --cluster="$CTX" >/dev/null + +# Verify cluster works +kubectl --context="$CTX" get ns default >/dev/null 2>&1 \ + && echo "# kwok cluster ready at port $PORT" \ + || { echo "# FATAL: kwok cluster not reachable"; exit 1; } + +export CONTEXT="$CTX" + +cd "$YSTACK_HOME" + +# --- test: CUE schema validation --- + +echo "" +echo "# Test: CUE schema validation" +y-cue vet ./cue/itest/example-namespace/ \ + && pass "example-namespace validates" \ + || fail "example-namespace validation" + +y-cue vet ./cue/itest/example-configmap/ \ + && pass "example-configmap validates (with dependency)" \ + || fail "example-configmap validation" + +y-cue vet ./cue/itest/example-with-dependency/ \ + && pass "example-with-dependency validates (transitive)" \ + || fail "example-with-dependency validation" + +y-cue vet ./cue/itest/example-disabled/ \ + && pass "example-disabled validates" \ + || fail "example-disabled validation" + +# --- test: plain kubectl-yconverge (no yconverge.cue) --- + +echo "" +echo "# Test: plain apply without yconverge.cue" +kubectl-yconverge --context="$CTX" -k cue/itest/example-namespace/ >/dev/null 2>&1 \ + && pass "plain apply namespace" \ + || fail "plain apply namespace" + +kubectl --context="$CTX" get ns itest >/dev/null 2>&1 \ + && pass "namespace itest exists after apply" \ + || fail "namespace itest missing after apply" + +# Clean up for next test +kubectl --context="$CTX" delete ns itest --wait=true >/dev/null 2>&1 + +# --- test: kubectl-yconverge with yconverge.cue checks --- + +echo "" +echo "# Test: apply with auto-checks" +OUTPUT=$(kubectl-yconverge --context="$CTX" -k cue/itest/example-namespace/ 2>&1) +echo "$OUTPUT" +echo "$OUTPUT" | grep -q "\[yconverge\]" \ + && pass "yconverge.cue detected and checks ran" \ + || fail "yconverge.cue not detected" + +# --- test: dependency precondition checks --- + +echo "" +echo "# Test: dependency precondition (configmap depends on namespace)" +OUTPUT=$(kubectl-yconverge --context="$CTX" -k cue/itest/example-configmap/ 2>&1) || true # y-script-lint:disable=or-true # capture output even on failure +echo "$OUTPUT" +echo "$OUTPUT" | grep -q "\[yconverge\]" \ + && pass "configmap applied with dependency checks" \ + || fail "configmap apply failed" + +kubectl --context="$CTX" -n itest get configmap itest-config >/dev/null 2>&1 \ + && pass "configmap itest-config exists" \ + || fail "configmap itest-config missing" + +# --- test: transitive dependency --- + +echo "" +echo "# Test: transitive dependency (depends on configmap which depends on namespace)" +OUTPUT=$(kubectl-yconverge --context="$CTX" -k cue/itest/example-with-dependency/ 2>&1) || true # y-script-lint:disable=or-true # capture output +echo "$OUTPUT" +echo "$OUTPUT" | grep -q "\[yconverge\]" \ + && pass "transitive dependency converge" \ + || fail "transitive dependency failed" + +kubectl --context="$CTX" -n itest get configmap itest-dependent >/dev/null 2>&1 \ + && pass "dependent configmap exists" \ + || fail "dependent configmap missing" + +# --- test: disabled step --- + +echo "" +echo "# Test: disabled step should not apply" +kubectl-yconverge --context="$CTX" -k cue/itest/example-disabled/ >/dev/null 2>&1 +kubectl --context="$CTX" -n itest get configmap itest-should-not-exist >/dev/null 2>&1 \ + && fail "disabled configmap should NOT exist" \ + || pass "disabled step correctly skipped" + +# --- test: one-level indirection --- + +echo "" +echo "# Test: yconverge.cue found via resources indirection" +kubectl --context="$CTX" delete ns itest --wait=true >/dev/null 2>&1 || true # y-script-lint:disable=or-true # clean slate +OUTPUT=$(kubectl-yconverge --context="$CTX" -k cue/itest/example-indirect/ 2>&1) || true # y-script-lint:disable=or-true # capture output +echo "$OUTPUT" +echo "$OUTPUT" | grep -q "\[yconverge\]" \ + && pass "indirection: yconverge.cue found in referenced dir" \ + || fail "indirection: yconverge.cue not found" + +# --- test: idempotent re-converge --- + +echo "" +echo "# Test: idempotent re-apply" +kubectl-yconverge --context="$CTX" -k cue/itest/example-namespace/ >/dev/null 2>&1 \ + && pass "re-apply namespace (idempotent)" \ + || fail "re-apply namespace failed" + +kubectl-yconverge --context="$CTX" -k cue/itest/example-configmap/ >/dev/null 2>&1 \ + && pass "re-apply configmap (idempotent)" \ + || fail "re-apply configmap failed" + +# --- test: error reporting --- + +echo "" +echo "# Test: error reporting on check failure" +# Apply to a non-existent namespace to trigger a check failure +OUTPUT=$(kubectl-yconverge --context="$CTX" -k cue/itest/example-configmap/ 2>&1) || true # y-script-lint:disable=or-true # expect possible failure +echo "$OUTPUT" | grep -q "configmap" \ + && pass "error output mentions the resource" \ + || fail "error output unhelpful" + +# --- test: --skip-checks flag --- + +echo "" +echo "# Test: --skip-checks suppresses check invocation" +OUTPUT=$(kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-namespace/ 2>&1) +echo "$OUTPUT" | grep -q "\[yconverge\]" \ + && fail "--skip-checks still ran checks" \ + || pass "--skip-checks suppressed checks" + +# --- results --- + +echo "" +echo "=== Results: $PASS passed, $FAIL failed ===" + +[ "$FAIL" -eq 0 ] From a670b9acb3961a43293160edbd5340f90b08a9fc Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Thu, 2 Apr 2026 15:07:00 +0000 Subject: [PATCH 10/36] Support multiple -k args in kubectl-yconverge Each -k runs the full cycle (prechecks -> apply -> checks) sequentially. Other args like -n are passed to every apply. This enables: kubectl-yconverge --context=local \ -k cluster-local/00-cluster/ \ -k cluster-local/mysql/ \ -k keycloak-v3/topic-events/ Reduces repetition in provisioning scripts. Adds itest coverage for multi -k (19/19 tests pass). Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/kubectl-yconverge | 150 ++++++++++++++++++++++-------------------- cue/itest/test.sh | 18 +++++ 2 files changed, 97 insertions(+), 71 deletions(-) diff --git a/bin/kubectl-yconverge b/bin/kubectl-yconverge index 4d4f1c8..3cafb5f 100755 --- a/bin/kubectl-yconverge +++ b/bin/kubectl-yconverge @@ -18,9 +18,12 @@ Flags (must come before kustomize args): --dry-run=true pass --dry-run=server to kubectl apply --skip-checks skip yconverge.cue check invocation -If the kustomization directory contains a yconverge.cue file (or one is -found one level of resources: indirection away), precondition checks from -dependencies run before apply, and this steps checks run after apply. +Multiple -k args are supported. Each runs the full cycle +(prechecks -> apply -> checks) sequentially in order. + +If a kustomization directory contains a yconverge.cue file (or one is +found one level of resources: indirection away), precondition checks +run before apply, and this steps checks run after apply. Steps with enabled: false are skipped entirely. Honors KUBECONFIG if set. @@ -29,7 +32,6 @@ Honors KUBECONFIG if set. DIFF=false DRY_RUN=false SKIP_CHECKS=false -KUSTOMIZE_DIR="" ctx=$1 case $ctx in @@ -46,28 +48,30 @@ while true; do esac done -# Extract -k directory from args +# Separate -k dirs from other args +KUSTOMIZE_DIRS="" +OTHER_ARGS="" +_prev="" for arg in "$@"; do case "$arg" in -l|--selector) echo "Error: yconverge can not be combined with other selectors" >&2 && exit 1 ;; esac -done -_prev="" -for arg in "$@"; do if [ "$_prev" = "-k" ]; then - KUSTOMIZE_DIR="$arg" - break + KUSTOMIZE_DIRS="$KUSTOMIZE_DIRS ${arg%/}" + _prev="" + continue fi case "$arg" in -k) _prev="-k" ;; - -k*) KUSTOMIZE_DIR="${arg#-k}"; break ;; + -k*) KUSTOMIZE_DIRS="$KUSTOMIZE_DIRS ${arg#-k}" ;; + *) OTHER_ARGS="$OTHER_ARGS $arg"; _prev="" ;; esac done -# --- yconverge.cue lookup --- +CONTEXT="${ctx#--context=}" +export CONTEXT -KUSTOMIZE_DIR="${KUSTOMIZE_DIR%/}" -YCONVERGE_DIR="" +# --- shared functions --- _find_yconverge_dir() { dir="$1" @@ -75,7 +79,6 @@ _find_yconverge_dir() { echo "$dir" return 0 fi - # One level indirection: single local resources entry if [ -f "$dir/kustomization.yaml" ]; then resources=$(y-yq '.resources // [] | .[] | select(test("^[^h]") and test("^(http|github)") | not)' "$dir/kustomization.yaml" 2>/dev/null) count=$(printf '%s\n' "$resources" | grep -c . 2>/dev/null) @@ -87,15 +90,6 @@ _find_yconverge_dir() { return 1 } -if [ -n "$KUSTOMIZE_DIR" ] && [ "$SKIP_CHECKS" = "false" ] && [ "$DRY_RUN" = "false" ] && [ "$DIFF" = "false" ]; then - YCONVERGE_DIR=$(_find_yconverge_dir "$KUSTOMIZE_DIR") || true # y-script-lint:disable=or-true # no yconverge.cue is ok -fi - -CONTEXT="${ctx#--context=}" -export CONTEXT - -# --- yconverge.cue: check enabled, run preconditions --- - _run_checks() { checks_json="$1" label="$2" @@ -133,45 +127,6 @@ _run_checks() { done } -if [ -n "$YCONVERGE_DIR" ]; then - echo " [yconverge] found $YCONVERGE_DIR/yconverge.cue" - - # Ensure path starts with ./ for CUE eval - case "$YCONVERGE_DIR" in - ./*|/*) ;; # already prefixed - *) YCONVERGE_DIR="./$YCONVERGE_DIR" ;; - esac - - # Check if step is disabled - ENABLED=$(y-cue eval "$YCONVERGE_DIR" -e 'step.enabled' 2>/dev/null) || ENABLED="true" - if [ "$ENABLED" = "false" ]; then - echo " [yconverge] step disabled, skipping" - exit 0 - fi - - # Run dependency precondition checks - PRECHECKS=$(y-cue eval "$YCONVERGE_DIR" -e 'step.prechecks' --out json 2>/dev/null) || true # y-script-lint:disable=or-true # no prechecks is ok - if [ -n "$PRECHECKS" ] && [ "$PRECHECKS" != "[]" ]; then - echo " [yconverge] running precondition checks" - _run_checks "$PRECHECKS" "precheck:" - fi -fi - -# --- apply --- - -if [ "$DIFF" = "true" ]; then - kubectl $ctx diff "$@" - exit $? -fi - -DRY_RUN_FLAG="" -[ "$DRY_RUN" = "true" ] && DRY_RUN_FLAG="--dry-run=server" - -kubectl $ctx create --save-config $DRY_RUN_FLAG --selector=yolean.se/converge-mode=create "$@" 2>/dev/null || true # y-script-lint:disable=or-true # idempotent, ignore already-exists -if [ "$DRY_RUN" = "false" ]; then - kubectl $ctx delete --selector=yolean.se/converge-mode=replace "$@" 2>/dev/null || true # y-script-lint:disable=or-true # ignore not-found -fi - _apply_if_any() { output=$(kubectl "$@" 2>&1) || { case "$output" in @@ -183,13 +138,66 @@ _apply_if_any() { [ -n "$output" ] && printf '%s\n' "$output" } -_apply_if_any $ctx apply --server-side --force-conflicts $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside-force "$@" -_apply_if_any $ctx apply --server-side $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside "$@" -_apply_if_any $ctx apply $DRY_RUN_FLAG --selector='yolean.se/converge-mode!=create,yolean.se/converge-mode!=serverside,yolean.se/converge-mode!=serverside-force' "$@" +_converge_one() { + kdir="$1" + yconverge_dir="" -# --- yconverge.cue: post-apply checks --- + if [ "$SKIP_CHECKS" = "false" ] && [ "$DRY_RUN" = "false" ] && [ "$DIFF" = "false" ]; then + yconverge_dir=$(_find_yconverge_dir "$kdir") || true # y-script-lint:disable=or-true # no yconverge.cue is ok + fi -if [ -n "$YCONVERGE_DIR" ]; then - CHECKS=$(y-cue eval "$YCONVERGE_DIR" -e 'step.checks' --out json 2>/dev/null) || true # y-script-lint:disable=or-true # no checks is ok - _run_checks "$CHECKS" "check:" -fi + # --- yconverge.cue: enabled check + preconditions --- + + if [ -n "$yconverge_dir" ]; then + echo " [yconverge] found $yconverge_dir/yconverge.cue" + + case "$yconverge_dir" in + ./*|/*) ;; + *) yconverge_dir="./$yconverge_dir" ;; + esac + + ENABLED=$(y-cue eval "$yconverge_dir" -e 'step.enabled' 2>/dev/null) || ENABLED="true" + if [ "$ENABLED" = "false" ]; then + echo " [yconverge] step disabled, skipping" + return 0 + fi + + PRECHECKS=$(y-cue eval "$yconverge_dir" -e 'step.prechecks' --out json 2>/dev/null) || true # y-script-lint:disable=or-true # no prechecks is ok + if [ -n "$PRECHECKS" ] && [ "$PRECHECKS" != "[]" ]; then + echo " [yconverge] running precondition checks" + _run_checks "$PRECHECKS" "precheck:" + fi + fi + + # --- apply --- + + if [ "$DIFF" = "true" ]; then + kubectl $ctx diff -k "$kdir" $OTHER_ARGS + return $? + fi + + DRY_RUN_FLAG="" + [ "$DRY_RUN" = "true" ] && DRY_RUN_FLAG="--dry-run=server" + + kubectl $ctx create --save-config $DRY_RUN_FLAG --selector=yolean.se/converge-mode=create -k "$kdir" $OTHER_ARGS 2>/dev/null || true # y-script-lint:disable=or-true # idempotent + if [ "$DRY_RUN" = "false" ]; then + kubectl $ctx delete --selector=yolean.se/converge-mode=replace -k "$kdir" $OTHER_ARGS 2>/dev/null || true # y-script-lint:disable=or-true # ignore not-found + fi + + _apply_if_any $ctx apply --server-side --force-conflicts $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside-force -k "$kdir" $OTHER_ARGS + _apply_if_any $ctx apply --server-side $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside -k "$kdir" $OTHER_ARGS + _apply_if_any $ctx apply $DRY_RUN_FLAG --selector='yolean.se/converge-mode!=create,yolean.se/converge-mode!=serverside,yolean.se/converge-mode!=serverside-force' -k "$kdir" $OTHER_ARGS + + # --- yconverge.cue: post-apply checks --- + + if [ -n "$yconverge_dir" ]; then + CHECKS=$(y-cue eval "$yconverge_dir" -e 'step.checks' --out json 2>/dev/null) || true # y-script-lint:disable=or-true # no checks is ok + _run_checks "$CHECKS" "check:" + fi +} + +# --- main --- + +for kdir in $KUSTOMIZE_DIRS; do + _converge_one "$kdir" +done diff --git a/cue/itest/test.sh b/cue/itest/test.sh index 1378c8f..c78aeda 100755 --- a/cue/itest/test.sh +++ b/cue/itest/test.sh @@ -170,6 +170,24 @@ echo "$OUTPUT" | grep -q "configmap" \ && pass "error output mentions the resource" \ || fail "error output unhelpful" +# --- test: multiple -k args --- + +echo "" +echo "# Test: multiple -k args in one invocation" +kubectl --context="$CTX" delete ns itest --wait=true >/dev/null 2>&1 || true # y-script-lint:disable=or-true # clean slate +OUTPUT=$(kubectl-yconverge --context="$CTX" \ + -k cue/itest/example-namespace/ \ + -k cue/itest/example-configmap/ \ + -k cue/itest/example-with-dependency/ 2>&1) || true # y-script-lint:disable=or-true # capture output +echo "$OUTPUT" +echo "$OUTPUT" | grep -c "\[yconverge\] found" | grep -q "3" \ + && pass "three yconverge.cue files found" \ + || fail "expected 3 yconverge.cue files" + +kubectl --context="$CTX" -n itest get configmap itest-config itest-dependent >/dev/null 2>&1 \ + && pass "both configmaps created via multi -k" \ + || fail "configmaps missing after multi -k" + # --- test: --skip-checks flag --- echo "" From fcf15a8ea17460a67a8d3ac55b5ed1eeca6dbc0c Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Thu, 2 Apr 2026 15:27:46 +0000 Subject: [PATCH 11/36] Add y-cue to runner.Dockerfile, add itest to CI workflow runner.Dockerfile: add y-cue binary download and version check. lint.yaml: add itest job that runs cue/itest/test.sh with kwok as a lightweight test cluster. Runs on push to main and PRs. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/lint.yaml | 19 +++++++++++++++++++ runner.Dockerfile | 3 +++ 2 files changed, 22 insertions(+) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 144ef91..945ae4a 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -26,3 +26,22 @@ jobs: with: key: script-lint-${{ github.ref_name }}-${{ github.run_id }} path: ~/.cache/ystack + + itest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/cache/restore@v4 + with: + key: itest-${{ github.ref_name }}- + restore-keys: | + itest-main- + path: ~/.cache/ystack + - name: Integration tests (yconverge framework) + run: cue/itest/test.sh + env: + PATH: ${{ github.workspace }}/bin:/usr/local/bin:/usr/bin:/bin + - uses: actions/cache/save@v4 + with: + key: itest-${{ github.ref_name }}-${{ github.run_id }} + path: ~/.cache/ystack diff --git a/runner.Dockerfile b/runner.Dockerfile index 984fcc1..e71231a 100644 --- a/runner.Dockerfile +++ b/runner.Dockerfile @@ -80,6 +80,9 @@ RUN y-esbuild --version COPY bin/y-turbo /usr/local/src/ystack/bin/ RUN y-turbo --version +COPY bin/y-cue /usr/local/src/ystack/bin/ +RUN y-cue version + FROM --platform=$TARGETPLATFORM base COPY --from=node --link /usr/local/lib/node_modules /usr/local/lib/node_modules From 0998b7b429b4df718c33fea92973266a8a5845f2 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Fri, 3 Apr 2026 04:48:10 +0000 Subject: [PATCH 12/36] Add itest for converge-mode labels and empty selector handling Tests serverside-force label where create/delete/regular selectors match nothing. Verifies _apply_if_any handles empty results gracefully. Co-Authored-By: Claude Opus 4.6 (1M context) --- cue/itest/example-serverside/configmap.yaml | 6 ++++++ cue/itest/example-serverside/kustomization.yaml | 7 +++++++ cue/itest/test.sh | 14 ++++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 cue/itest/example-serverside/configmap.yaml create mode 100644 cue/itest/example-serverside/kustomization.yaml diff --git a/cue/itest/example-serverside/configmap.yaml b/cue/itest/example-serverside/configmap.yaml new file mode 100644 index 0000000..b3f5159 --- /dev/null +++ b/cue/itest/example-serverside/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: itest-serverside +data: + applied: via-serverside-force diff --git a/cue/itest/example-serverside/kustomization.yaml b/cue/itest/example-serverside/kustomization.yaml new file mode 100644 index 0000000..b05b126 --- /dev/null +++ b/cue/itest/example-serverside/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: itest +commonLabels: + yolean.se/converge-mode: serverside-force +resources: +- configmap.yaml diff --git a/cue/itest/test.sh b/cue/itest/test.sh index c78aeda..6a91d1a 100755 --- a/cue/itest/test.sh +++ b/cue/itest/test.sh @@ -188,6 +188,20 @@ kubectl --context="$CTX" -n itest get configmap itest-config itest-dependent >/d && pass "both configmaps created via multi -k" \ || fail "configmaps missing after multi -k" +# --- test: converge-mode labels (empty selector results) --- + +echo "" +echo "# Test: serverside-force label (other selectors match nothing)" +kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-serverside/ 2>&1 +kubectl --context="$CTX" -n itest get configmap itest-serverside >/dev/null 2>&1 \ + && pass "serverside-force configmap created" \ + || fail "serverside-force configmap missing" + +# Re-apply should be idempotent (all empty selectors handled gracefully) +kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-serverside/ >/dev/null 2>&1 \ + && pass "serverside-force re-apply idempotent" \ + || fail "serverside-force re-apply failed" + # --- test: --skip-checks flag --- echo "" From a5b824a8e77869d4fa4e779661e3c4363ee5e973 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Fri, 3 Apr 2026 05:00:07 +0000 Subject: [PATCH 13/36] Remove enabled:false handling from kubectl-yconverge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The enabled field belongs to the DAG engine's step filtering, not to the apply tool. When someone runs kubectl yconverge -k dir/, they're explicitly asking to apply — refusing is confusing. enabled:false remains in the CUE schema for the DAG engine. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/kubectl-yconverge | 7 ------- cue/itest/test.sh | 11 +---------- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/bin/kubectl-yconverge b/bin/kubectl-yconverge index 3cafb5f..862f964 100755 --- a/bin/kubectl-yconverge +++ b/bin/kubectl-yconverge @@ -24,7 +24,6 @@ Multiple -k args are supported. Each runs the full cycle If a kustomization directory contains a yconverge.cue file (or one is found one level of resources: indirection away), precondition checks run before apply, and this steps checks run after apply. -Steps with enabled: false are skipped entirely. Honors KUBECONFIG if set. ' && exit 0 @@ -156,12 +155,6 @@ _converge_one() { *) yconverge_dir="./$yconverge_dir" ;; esac - ENABLED=$(y-cue eval "$yconverge_dir" -e 'step.enabled' 2>/dev/null) || ENABLED="true" - if [ "$ENABLED" = "false" ]; then - echo " [yconverge] step disabled, skipping" - return 0 - fi - PRECHECKS=$(y-cue eval "$yconverge_dir" -e 'step.prechecks' --out json 2>/dev/null) || true # y-script-lint:disable=or-true # no prechecks is ok if [ -n "$PRECHECKS" ] && [ "$PRECHECKS" != "[]" ]; then echo " [yconverge] running precondition checks" diff --git a/cue/itest/test.sh b/cue/itest/test.sh index 6a91d1a..2eaaf14 100755 --- a/cue/itest/test.sh +++ b/cue/itest/test.sh @@ -72,7 +72,7 @@ y-cue vet ./cue/itest/example-with-dependency/ \ || fail "example-with-dependency validation" y-cue vet ./cue/itest/example-disabled/ \ - && pass "example-disabled validates" \ + && pass "example-disabled validates (schema only, not enforced by kubectl-yconverge)" \ || fail "example-disabled validation" # --- test: plain kubectl-yconverge (no yconverge.cue) --- @@ -128,15 +128,6 @@ kubectl --context="$CTX" -n itest get configmap itest-dependent >/dev/null 2>&1 && pass "dependent configmap exists" \ || fail "dependent configmap missing" -# --- test: disabled step --- - -echo "" -echo "# Test: disabled step should not apply" -kubectl-yconverge --context="$CTX" -k cue/itest/example-disabled/ >/dev/null 2>&1 -kubectl --context="$CTX" -n itest get configmap itest-should-not-exist >/dev/null 2>&1 \ - && fail "disabled configmap should NOT exist" \ - || pass "disabled step correctly skipped" - # --- test: one-level indirection --- echo "" From 147ce44f159bb1611709c571ce243cac3cb0e6b3 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Fri, 3 Apr 2026 05:03:55 +0000 Subject: [PATCH 14/36] Add tool binary prereq check to itest, fix GHA failures Ensure y-cue, y-yq, kubectl are available before running tests. Fixes CUE vet failures in GHA where binaries weren't pre-downloaded. Co-Authored-By: Claude Opus 4.6 (1M context) --- cue/itest/test.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cue/itest/test.sh b/cue/itest/test.sh index 2eaaf14..d8fb0e2 100755 --- a/cue/itest/test.sh +++ b/cue/itest/test.sh @@ -55,6 +55,13 @@ export CONTEXT="$CTX" cd "$YSTACK_HOME" +# --- prerequisites --- + +echo "# Ensuring tool binaries are available ..." +y-cue version >/dev/null +y-yq --version >/dev/null +kubectl version --client=true >/dev/null 2>&1 + # --- test: CUE schema validation --- echo "" From 508625e3a3add41c646f8837d2a638770c090bea Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Fri, 3 Apr 2026 05:05:20 +0000 Subject: [PATCH 15/36] Set YSTACK_HOME in GHA itest job for binary downloads y-bin-download needs YSTACK_HOME to find the bin directory. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/lint.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 945ae4a..c8f29a3 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -40,6 +40,7 @@ jobs: - name: Integration tests (yconverge framework) run: cue/itest/test.sh env: + YSTACK_HOME: ${{ github.workspace }} PATH: ${{ github.workspace }}/bin:/usr/local/bin:/usr/bin:/bin - uses: actions/cache/save@v4 with: From 173a5aa1c2eb68ee24954a73d36e40bfed519210 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Fri, 3 Apr 2026 05:41:00 +0000 Subject: [PATCH 16/36] Simplify schema per PR review, restore #Wait/#Rollout with namespace Schema changes: - Remove kustomization (implicit from yconverge.cue placement) - Remove enabled, actions, prechecks from #Step - Keep checks and up (core DAG field) - Keep namespace on #Wait/#Rollout (namespace handling TBD) Remove prechecks from kubectl-yconverge. Move y-kustomize restart from action to exec check in 30-blobs-ystack and 40-kafka-ystack. Namespace on modeled checks is kept for now. Proper namespace propagation behavior needs specification and test cases. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/kubectl-yconverge | 8 ---- cue/converge-ystack/converge_tool.cue | 30 +++++------- cue/converge-ystack/steps.cue | 48 +++++++++++-------- cue/converge/schema.cue | 35 ++++---------- cue/itest/example-configmap/yconverge.cue | 5 +- cue/itest/example-disabled/yconverge.cue | 3 -- cue/itest/example-namespace/yconverge.cue | 2 - .../example-with-dependency/yconverge.cue | 5 +- cue/itest/test.sh | 4 +- k3s/00-namespace-ystack/yconverge.cue | 2 - k3s/01-namespace-blobs/yconverge.cue | 2 - k3s/02-namespace-kafka/yconverge.cue | 2 - k3s/03-namespace-monitoring/yconverge.cue | 2 - k3s/09-y-kustomize-secrets-init/yconverge.cue | 4 +- k3s/10-gateway-api/yconverge.cue | 3 +- k3s/11-monitoring-operator/yconverge.cue | 4 +- k3s/20-gateway/yconverge.cue | 26 +++++----- k3s/29-y-kustomize/yconverge.cue | 2 - k3s/30-blobs-minio-disabled/yconverge.cue | 3 -- k3s/30-blobs-ystack/yconverge.cue | 9 ++-- k3s/30-blobs/yconverge.cue | 4 +- k3s/40-kafka-ystack/yconverge.cue | 13 +++-- k3s/40-kafka/yconverge.cue | 4 +- k3s/50-monitoring/yconverge.cue | 4 +- k3s/60-builds-registry/yconverge.cue | 2 - k3s/61-prod-registry/yconverge.cue | 4 +- k3s/62-buildkit/yconverge.cue | 4 +- 27 files changed, 85 insertions(+), 149 deletions(-) diff --git a/bin/kubectl-yconverge b/bin/kubectl-yconverge index 862f964..7a96b2c 100755 --- a/bin/kubectl-yconverge +++ b/bin/kubectl-yconverge @@ -145,8 +145,6 @@ _converge_one() { yconverge_dir=$(_find_yconverge_dir "$kdir") || true # y-script-lint:disable=or-true # no yconverge.cue is ok fi - # --- yconverge.cue: enabled check + preconditions --- - if [ -n "$yconverge_dir" ]; then echo " [yconverge] found $yconverge_dir/yconverge.cue" @@ -154,12 +152,6 @@ _converge_one() { ./*|/*) ;; *) yconverge_dir="./$yconverge_dir" ;; esac - - PRECHECKS=$(y-cue eval "$yconverge_dir" -e 'step.prechecks' --out json 2>/dev/null) || true # y-script-lint:disable=or-true # no prechecks is ok - if [ -n "$PRECHECKS" ] && [ "$PRECHECKS" != "[]" ]; then - echo " [yconverge] running precondition checks" - _run_checks "$PRECHECKS" "precheck:" - fi fi # --- apply --- diff --git a/cue/converge-ystack/converge_tool.cue b/cue/converge-ystack/converge_tool.cue index b1b0486..3edc379 100644 --- a/cue/converge-ystack/converge_tool.cue +++ b/cue/converge-ystack/converge_tool.cue @@ -21,36 +21,28 @@ _env: { } } -_activeSteps: [for s in steps if s.enabled {s}] - // Build human-readable plan -_planLines: [for s in _activeSteps { - let _nsLabel = { - if s.namespace != _|_ {" [ns: \(s.namespace)]"} - if s.namespace == _|_ {""} - } - let _header = " \(s.kustomization)\(_nsLabel)" - let _actionLines = [for a in s.actions {" action: \(a.description)"}] - let _checkLines = [for c in s.checks { +_planLines: [for s in steps { + let _header = " \(s.path)" + let _checkLines = [for c in s.step.checks { if c.kind == "wait" {" check: wait \(c.resource) \(c.for)"} if c.kind == "rollout" {" check: rollout \(c.resource)"} if c.kind == "exec" {" check: \(c.description)"} }] - strings.Join(list.Concat([[_header], _actionLines, _checkLines]), "\n") + strings.Join(list.Concat([[_header], _checkLines]), "\n") }] _plan: strings.Join(list.Concat([ ["=== Converge plan (context=\(_context)) ==="], - ["Steps (\(len(_activeSteps))):"], + ["Steps (\(len(steps))):"], _planLines, ["==="], ]), "\n") -// Generate shell commands per step, wrapped in error handler -_stepCmds: [for s in _activeSteps { - let _apply = "kubectl-yconverge --context=\(_context) --skip-checks -k \(s.kustomization)/" - let _actionCmds = [for a in s.actions {"echo ' action: \(a.description)' && " + a.command}] - let _checkCmds = [for c in s.checks { +// Generate shell commands per step +_stepCmds: [for s in steps { + let _apply = "kubectl-yconverge --context=\(_context) --skip-checks -k \(s.path)/" + let _checkCmds = [for c in s.step.checks { if c.kind == "wait" { let _ns = { if c.namespace != _|_ {"-n \(c.namespace) "} @@ -67,8 +59,8 @@ _stepCmds: [for s in _activeSteps { } if c.kind == "exec" {"echo ' check: \(c.description)' && { for _retry_i in $(seq 1 15); do " + c.command + " && break || sleep 2; done; }"} }] - let _body = strings.Join(list.Concat([[_apply], _actionCmds, _checkCmds]), "\n") - "echo '>>> \(s.kustomization)'\nif ! (\n\(_body)\n); then\n echo ''\n echo \"FAILED: \(s.kustomization)\"\n echo 'The step above failed. Re-run to retry from this point.'\n exit 1\nfi" + let _body = strings.Join(list.Concat([[_apply], _checkCmds]), "\n") + "echo '>>> \(s.path)'\nif ! (\n\(_body)\n); then\n echo ''\n echo \"FAILED: \(s.path)\"\n echo 'The step above failed. Re-run to retry from this point.'\n exit 1\nfi" }] _script: strings.Join(list.Concat([ diff --git a/cue/converge-ystack/steps.cue b/cue/converge-ystack/steps.cue index 224575a..4730163 100644 --- a/cue/converge-ystack/steps.cue +++ b/cue/converge-ystack/steps.cue @@ -21,25 +21,31 @@ import ( "yolean.se/ystack/k3s/62-buildkit:buildkit" ) -// All steps in dependency order. The order here matches the DAG -// but CUE imports enforce the actual dependency constraints. -steps: [ - namespace_ystack.step, - namespace_blobs.step, - namespace_kafka.step, - namespace_monitoring.step, - y_kustomize_secrets_init.step, - gateway_api.step, - monitoring_operator.step, - gateway.step, - y_kustomize.step, - blobs_ystack.step, - blobs.step, - blobs_minio_disabled.step, - kafka_ystack.step, - kafka.step, - monitoring.step, - builds_registry.step, - prod_registry.step, - buildkit.step, +import "yolean.se/ystack/cue/converge" + +_entry: { + path: string + step: converge.#Step +} + +// All steps in dependency order. +steps: [..._entry] & [ + {path: "k3s/00-namespace-ystack", step: namespace_ystack.step}, + {path: "k3s/01-namespace-blobs", step: namespace_blobs.step}, + {path: "k3s/02-namespace-kafka", step: namespace_kafka.step}, + {path: "k3s/03-namespace-monitoring", step: namespace_monitoring.step}, + {path: "k3s/09-y-kustomize-secrets-init", step: y_kustomize_secrets_init.step}, + {path: "k3s/10-gateway-api", step: gateway_api.step}, + {path: "k3s/11-monitoring-operator", step: monitoring_operator.step}, + {path: "k3s/20-gateway", step: gateway.step}, + {path: "k3s/29-y-kustomize", step: y_kustomize.step}, + {path: "k3s/30-blobs-ystack", step: blobs_ystack.step}, + {path: "k3s/30-blobs", step: blobs.step}, + {path: "k3s/30-blobs-minio-disabled", step: blobs_minio_disabled.step}, + {path: "k3s/40-kafka-ystack", step: kafka_ystack.step}, + {path: "k3s/40-kafka", step: kafka.step}, + {path: "k3s/50-monitoring", step: monitoring.step}, + {path: "k3s/60-builds-registry", step: builds_registry.step}, + {path: "k3s/61-prod-registry", step: prod_registry.step}, + {path: "k3s/62-buildkit", step: buildkit.step}, ] diff --git a/cue/converge/schema.cue b/cue/converge/schema.cue index 6e5a9b3..3c735a4 100644 --- a/cue/converge/schema.cue +++ b/cue/converge/schema.cue @@ -1,25 +1,14 @@ package converge // A convergence step: apply a kustomize base, then verify. +// The yconverge.cue file must be next to a kustomization.yaml. +// The kustomization path is implicit from the file location. #Step: { - // Path to kustomize directory, relative to repo root. - kustomization: string - // Namespace this step targets. Used for filtering (--exclude-namespace). - namespace?: string - // Set to false to disable this step (e.g. alternative implementations). - enabled: *true | bool - // One-shot mutations that run after apply (not retried). - actions: [...#Action] - // Precondition checks from dependencies. Modules populate this - // from their imported dependencies' checks. - prechecks: [...#Check] - // Checks that must pass after apply. Downstream steps that import - // this package use these as preconditions. + // Checks that must pass after apply. // Empty list means the step is ready immediately after apply. checks: [...#Check] - // True after apply + actions + checks complete successfully. - // Downstream steps reference this to express dependencies. - // Default false; set by the engine at runtime. + // True after apply + checks complete successfully. + // Downstream steps that import this package gate on this value. up: *false | bool } @@ -31,8 +20,8 @@ package converge // Timeout and output are managed by kubectl. #Wait: { kind: "wait" - resource: string // e.g. "pod/redpanda-0" or "crd/gateways.gateway.networking.k8s.io" - for: string // e.g. "condition=Ready" or "condition=Established" + resource: string + for: string namespace?: string timeout: *"60s" | string description: *"" | string @@ -42,7 +31,7 @@ package converge // Timeout and output are managed by kubectl. #Rollout: { kind: "rollout" - resource: string // e.g. "deploy/y-kustomize" or "statefulset/redpanda" + resource: string namespace?: string timeout: *"60s" | string description: *"" | string @@ -56,11 +45,3 @@ package converge timeout: *"60s" | string description: string } - -// An imperative action that runs once after apply. -// Unlike checks, actions are not retried -- they either succeed or fail. -#Action: { - kind: "action" - command: string - description: string -} diff --git a/cue/itest/example-configmap/yconverge.cue b/cue/itest/example-configmap/yconverge.cue index de3076e..dea4f09 100644 --- a/cue/itest/example-configmap/yconverge.cue +++ b/cue/itest/example-configmap/yconverge.cue @@ -5,10 +5,9 @@ import ( "yolean.se/ystack/cue/itest/example-namespace:example_namespace" ) +_dep_ns: example_namespace.step + step: converge.#Step & { - kustomization: "cue/itest/example-configmap" - namespace: "itest" - prechecks: example_namespace.step.checks checks: [{ kind: "exec" command: "kubectl --context=$CONTEXT -n itest get configmap itest-config" diff --git a/cue/itest/example-disabled/yconverge.cue b/cue/itest/example-disabled/yconverge.cue index 1ae1e28..9bb5ba2 100644 --- a/cue/itest/example-disabled/yconverge.cue +++ b/cue/itest/example-disabled/yconverge.cue @@ -3,9 +3,6 @@ package example_disabled import "yolean.se/ystack/cue/converge" step: converge.#Step & { - kustomization: "cue/itest/example-disabled" - namespace: "itest" - enabled: false checks: [{ kind: "exec" command: "false" diff --git a/cue/itest/example-namespace/yconverge.cue b/cue/itest/example-namespace/yconverge.cue index 391becc..3aaa07b 100644 --- a/cue/itest/example-namespace/yconverge.cue +++ b/cue/itest/example-namespace/yconverge.cue @@ -3,8 +3,6 @@ package example_namespace import "yolean.se/ystack/cue/converge" step: converge.#Step & { - kustomization: "cue/itest/example-namespace" - namespace: "itest" checks: [{ kind: "wait" resource: "ns/itest" diff --git a/cue/itest/example-with-dependency/yconverge.cue b/cue/itest/example-with-dependency/yconverge.cue index f5a4831..59d2913 100644 --- a/cue/itest/example-with-dependency/yconverge.cue +++ b/cue/itest/example-with-dependency/yconverge.cue @@ -5,10 +5,9 @@ import ( "yolean.se/ystack/cue/itest/example-configmap:example_configmap" ) +_dep_config: example_configmap.step + step: converge.#Step & { - kustomization: "cue/itest/example-with-dependency" - namespace: "itest" - prechecks: example_configmap.step.checks checks: [{ kind: "exec" command: "kubectl --context=$CONTEXT -n itest get configmap itest-dependent" diff --git a/cue/itest/test.sh b/cue/itest/test.sh index d8fb0e2..66c539e 100755 --- a/cue/itest/test.sh +++ b/cue/itest/test.sh @@ -107,10 +107,10 @@ echo "$OUTPUT" | grep -q "\[yconverge\]" \ && pass "yconverge.cue detected and checks ran" \ || fail "yconverge.cue not detected" -# --- test: dependency precondition checks --- +# --- test: apply with dependency (configmap after namespace) --- echo "" -echo "# Test: dependency precondition (configmap depends on namespace)" +echo "# Test: apply with dependency (configmap needs namespace)" OUTPUT=$(kubectl-yconverge --context="$CTX" -k cue/itest/example-configmap/ 2>&1) || true # y-script-lint:disable=or-true # capture output even on failure echo "$OUTPUT" echo "$OUTPUT" | grep -q "\[yconverge\]" \ diff --git a/k3s/00-namespace-ystack/yconverge.cue b/k3s/00-namespace-ystack/yconverge.cue index 2222a21..d9d7562 100644 --- a/k3s/00-namespace-ystack/yconverge.cue +++ b/k3s/00-namespace-ystack/yconverge.cue @@ -3,7 +3,5 @@ package namespace_ystack import "yolean.se/ystack/cue/converge" step: converge.#Step & { - kustomization: "k3s/00-namespace-ystack" - namespace: "ystack" checks: [] } diff --git a/k3s/01-namespace-blobs/yconverge.cue b/k3s/01-namespace-blobs/yconverge.cue index 56e395e..d2af883 100644 --- a/k3s/01-namespace-blobs/yconverge.cue +++ b/k3s/01-namespace-blobs/yconverge.cue @@ -3,7 +3,5 @@ package namespace_blobs import "yolean.se/ystack/cue/converge" step: converge.#Step & { - kustomization: "k3s/01-namespace-blobs" - namespace: "blobs" checks: [] } diff --git a/k3s/02-namespace-kafka/yconverge.cue b/k3s/02-namespace-kafka/yconverge.cue index c4330ad..7e4a090 100644 --- a/k3s/02-namespace-kafka/yconverge.cue +++ b/k3s/02-namespace-kafka/yconverge.cue @@ -3,7 +3,5 @@ package namespace_kafka import "yolean.se/ystack/cue/converge" step: converge.#Step & { - kustomization: "k3s/02-namespace-kafka" - namespace: "kafka" checks: [] } diff --git a/k3s/03-namespace-monitoring/yconverge.cue b/k3s/03-namespace-monitoring/yconverge.cue index 062ffa2..dd04626 100644 --- a/k3s/03-namespace-monitoring/yconverge.cue +++ b/k3s/03-namespace-monitoring/yconverge.cue @@ -3,7 +3,5 @@ package namespace_monitoring import "yolean.se/ystack/cue/converge" step: converge.#Step & { - kustomization: "k3s/03-namespace-monitoring" - namespace: "monitoring" checks: [] } diff --git a/k3s/09-y-kustomize-secrets-init/yconverge.cue b/k3s/09-y-kustomize-secrets-init/yconverge.cue index 374d66c..0c98e9a 100644 --- a/k3s/09-y-kustomize-secrets-init/yconverge.cue +++ b/k3s/09-y-kustomize-secrets-init/yconverge.cue @@ -5,10 +5,8 @@ import ( "yolean.se/ystack/k3s/00-namespace-ystack:namespace_ystack" ) -_deps: namespace_ystack.step +_dep_ns: namespace_ystack.step step: converge.#Step & { - kustomization: "k3s/09-y-kustomize-secrets-init" - namespace: "ystack" checks: [] } diff --git a/k3s/10-gateway-api/yconverge.cue b/k3s/10-gateway-api/yconverge.cue index 2837b8c..c2bc7b7 100644 --- a/k3s/10-gateway-api/yconverge.cue +++ b/k3s/10-gateway-api/yconverge.cue @@ -5,10 +5,9 @@ import ( "yolean.se/ystack/k3s/00-namespace-ystack:namespace_ystack" ) -_deps: namespace_ystack.step +_dep_ns: namespace_ystack.step step: converge.#Step & { - kustomization: "k3s/10-gateway-api" checks: [{ kind: "exec" command: "for i in $(seq 1 30); do kubectl --context=$CONTEXT wait --for=condition=Established --timeout=2s crd/gateways.gateway.networking.k8s.io 2>/dev/null && break; sleep 2; done && kubectl --context=$CONTEXT wait --for=condition=Established --timeout=5s crd/gateways.gateway.networking.k8s.io" diff --git a/k3s/11-monitoring-operator/yconverge.cue b/k3s/11-monitoring-operator/yconverge.cue index 0b67cc0..4969c95 100644 --- a/k3s/11-monitoring-operator/yconverge.cue +++ b/k3s/11-monitoring-operator/yconverge.cue @@ -5,11 +5,9 @@ import ( "yolean.se/ystack/k3s/03-namespace-monitoring:namespace_monitoring" ) -_deps: namespace_monitoring.step +_dep_ns: namespace_monitoring.step step: converge.#Step & { - kustomization: "k3s/11-monitoring-operator" - namespace: "monitoring" checks: [{ kind: "rollout" resource: "deploy/prometheus-operator" diff --git a/k3s/20-gateway/yconverge.cue b/k3s/20-gateway/yconverge.cue index e947230..c052608 100644 --- a/k3s/20-gateway/yconverge.cue +++ b/k3s/20-gateway/yconverge.cue @@ -5,28 +5,28 @@ import ( "yolean.se/ystack/k3s/10-gateway-api:gateway_api" ) -_deps: gateway_api.step +_dep_crds: gateway_api.step step: converge.#Step & { - kustomization: "k3s/20-gateway" - namespace: "ystack" - actions: [ + checks: [ { - kind: "action" + kind: "exec" command: "[ -z \"$OVERRIDE_IP\" ] || kubectl --context=$CONTEXT -n ystack annotate gateway ystack yolean.se/override-ip=$OVERRIDE_IP --overwrite" + timeout: "10s" description: "annotate gateway with override-ip (if set)" }, { - kind: "action" + kind: "exec" command: "y-k8s-ingress-hosts --context=$CONTEXT --ensure || echo 'WARNING: /etc/hosts update failed (may need manual sudo)'" + timeout: "10s" description: "update /etc/hosts for gateway routes" }, + { + kind: "wait" + resource: "gateway/ystack" + namespace: "ystack" + for: "condition=Programmed" + timeout: "60s" + }, ] - checks: [{ - kind: "wait" - resource: "gateway/ystack" - namespace: "ystack" - for: "condition=Programmed" - timeout: "60s" - }] } diff --git a/k3s/29-y-kustomize/yconverge.cue b/k3s/29-y-kustomize/yconverge.cue index 849b1c5..a0f7e19 100644 --- a/k3s/29-y-kustomize/yconverge.cue +++ b/k3s/29-y-kustomize/yconverge.cue @@ -10,8 +10,6 @@ _dep_secrets: y_kustomize_secrets_init.step _dep_gateway: gateway.step step: converge.#Step & { - kustomization: "k3s/29-y-kustomize" - namespace: "ystack" checks: [{ kind: "rollout" resource: "deploy/y-kustomize" diff --git a/k3s/30-blobs-minio-disabled/yconverge.cue b/k3s/30-blobs-minio-disabled/yconverge.cue index b164a9d..4a5505c 100644 --- a/k3s/30-blobs-minio-disabled/yconverge.cue +++ b/k3s/30-blobs-minio-disabled/yconverge.cue @@ -3,8 +3,5 @@ package blobs_minio_disabled import "yolean.se/ystack/cue/converge" step: converge.#Step & { - kustomization: "k3s/30-blobs-minio-disabled" - namespace: "blobs" - enabled: false checks: [] } diff --git a/k3s/30-blobs-ystack/yconverge.cue b/k3s/30-blobs-ystack/yconverge.cue index 2d54ae0..3a78a2a 100644 --- a/k3s/30-blobs-ystack/yconverge.cue +++ b/k3s/30-blobs-ystack/yconverge.cue @@ -10,7 +10,10 @@ _dep_ns: namespace_blobs.step _dep_kustomize: y_kustomize.step step: converge.#Step & { - kustomization: "k3s/30-blobs-ystack" - namespace: "blobs" - checks: [] + checks: [{ + kind: "exec" + command: "kubectl --context=$CONTEXT -n ystack rollout restart deploy/y-kustomize && kubectl --context=$CONTEXT -n ystack rollout status deploy/y-kustomize --timeout=60s" + timeout: "90s" + description: "restart y-kustomize to pick up blobs secrets" + }] } diff --git a/k3s/30-blobs/yconverge.cue b/k3s/30-blobs/yconverge.cue index b022436..602efbb 100644 --- a/k3s/30-blobs/yconverge.cue +++ b/k3s/30-blobs/yconverge.cue @@ -5,11 +5,9 @@ import ( "yolean.se/ystack/k3s/30-blobs-ystack:blobs_ystack" ) -_deps: blobs_ystack.step +_dep_ystack: blobs_ystack.step step: converge.#Step & { - kustomization: "k3s/30-blobs" - namespace: "blobs" checks: [{ kind: "rollout" resource: "deploy/versitygw" diff --git a/k3s/40-kafka-ystack/yconverge.cue b/k3s/40-kafka-ystack/yconverge.cue index 9af895f..6b55438 100644 --- a/k3s/40-kafka-ystack/yconverge.cue +++ b/k3s/40-kafka-ystack/yconverge.cue @@ -10,14 +10,13 @@ _dep_ns: namespace_kafka.step _dep_kustomize: y_kustomize.step step: converge.#Step & { - kustomization: "k3s/40-kafka-ystack" - namespace: "kafka" - actions: [{ - kind: "action" - command: "kubectl --context=$CONTEXT -n ystack rollout restart deploy/y-kustomize && kubectl --context=$CONTEXT -n ystack rollout status deploy/y-kustomize --timeout=60s" - description: "restart y-kustomize to pick up kafka secrets" - }] checks: [ + { + kind: "exec" + command: "kubectl --context=$CONTEXT -n ystack rollout restart deploy/y-kustomize && kubectl --context=$CONTEXT -n ystack rollout status deploy/y-kustomize --timeout=60s" + timeout: "90s" + description: "restart y-kustomize to pick up kafka secrets" + }, { kind: "exec" command: "kubectl --context=$CONTEXT get --raw /api/v1/namespaces/ystack/services/y-kustomize:80/proxy/v1/blobs/setup-bucket-job/base-for-annotations.yaml" diff --git a/k3s/40-kafka/yconverge.cue b/k3s/40-kafka/yconverge.cue index 815796f..e7473de 100644 --- a/k3s/40-kafka/yconverge.cue +++ b/k3s/40-kafka/yconverge.cue @@ -5,11 +5,9 @@ import ( "yolean.se/ystack/k3s/40-kafka-ystack:kafka_ystack" ) -_deps: kafka_ystack.step +_dep_ystack: kafka_ystack.step step: converge.#Step & { - kustomization: "k3s/40-kafka" - namespace: "kafka" checks: [ { kind: "rollout" diff --git a/k3s/50-monitoring/yconverge.cue b/k3s/50-monitoring/yconverge.cue index 420189c..defe858 100644 --- a/k3s/50-monitoring/yconverge.cue +++ b/k3s/50-monitoring/yconverge.cue @@ -5,11 +5,9 @@ import ( "yolean.se/ystack/k3s/11-monitoring-operator:monitoring_operator" ) -_deps: monitoring_operator.step +_dep_operator: monitoring_operator.step step: converge.#Step & { - kustomization: "k3s/50-monitoring" - namespace: "monitoring" checks: [{ kind: "rollout" resource: "deploy/kube-state-metrics" diff --git a/k3s/60-builds-registry/yconverge.cue b/k3s/60-builds-registry/yconverge.cue index 3912d0e..c2bb093 100644 --- a/k3s/60-builds-registry/yconverge.cue +++ b/k3s/60-builds-registry/yconverge.cue @@ -12,8 +12,6 @@ _dep_kafka: kafka_ystack.step _dep_kustomize: y_kustomize.step step: converge.#Step & { - kustomization: "k3s/60-builds-registry" - namespace: "ystack" checks: [ { kind: "rollout" diff --git a/k3s/61-prod-registry/yconverge.cue b/k3s/61-prod-registry/yconverge.cue index 3af7472..1de7d2b 100644 --- a/k3s/61-prod-registry/yconverge.cue +++ b/k3s/61-prod-registry/yconverge.cue @@ -5,10 +5,8 @@ import ( "yolean.se/ystack/k3s/00-namespace-ystack:namespace_ystack" ) -_deps: namespace_ystack.step +_dep_ns: namespace_ystack.step step: converge.#Step & { - kustomization: "k3s/61-prod-registry" - namespace: "ystack" checks: [] } diff --git a/k3s/62-buildkit/yconverge.cue b/k3s/62-buildkit/yconverge.cue index c4af64a..4ca1ea2 100644 --- a/k3s/62-buildkit/yconverge.cue +++ b/k3s/62-buildkit/yconverge.cue @@ -5,11 +5,9 @@ import ( "yolean.se/ystack/k3s/60-builds-registry:builds_registry" ) -_deps: builds_registry.step +_dep_registry: builds_registry.step step: converge.#Step & { - kustomization: "k3s/62-buildkit" - namespace: "ystack" checks: [{ kind: "exec" command: "kubectl --context=$CONTEXT -n ystack get statefulset buildkitd" From 0ae85e221c364dc0c979d9be2519f7eb9bce3e72 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Fri, 3 Apr 2026 05:43:27 +0000 Subject: [PATCH 17/36] Add namespaceGuess field, resolve from CLI/-n/kustomization/context namespaceGuess is set by the engine (like up), not by user CUE files. Priority: -n CLI arg > kustomization.yaml namespace: > context default. Used as fallback for #Wait/#Rollout checks that omit namespace. Namespace test cases TBD. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/kubectl-yconverge | 21 +++++++++++++++++++++ cue/converge/schema.cue | 8 ++++++++ 2 files changed, 29 insertions(+) diff --git a/bin/kubectl-yconverge b/bin/kubectl-yconverge index 7a96b2c..1eea197 100755 --- a/bin/kubectl-yconverge +++ b/bin/kubectl-yconverge @@ -104,6 +104,8 @@ _run_checks() { ns=$(echo "$checks_json" | y-yq ".[$i].namespace // \"\"" -) timeout=$(echo "$checks_json" | y-yq ".[$i].timeout // \"60s\"" -) command=$(echo "$checks_json" | y-yq ".[$i].command // \"\"" -) + # Use NS_GUESS as fallback when check doesn't specify namespace + [ -z "$ns" ] && ns="$NS_GUESS" ns_flag="" [ -n "$ns" ] && ns_flag="-n $ns" case "$kind" in @@ -154,6 +156,25 @@ _converge_one() { esac fi + # --- resolve namespace guess --- + # Priority: 1. -n from OTHER_ARGS, 2. kustomization namespace:, 3. context default + NS_GUESS="" + _prev_arg="" + for _arg in $OTHER_ARGS; do + if [ "$_prev_arg" = "-n" ]; then + NS_GUESS="$_arg" + break + fi + _prev_arg="$_arg" + done + if [ -z "$NS_GUESS" ] && [ -f "$kdir/kustomization.yaml" ]; then + NS_GUESS=$(y-yq '.namespace // ""' "$kdir/kustomization.yaml" 2>/dev/null) + fi + if [ -z "$NS_GUESS" ]; then + NS_GUESS=$(kubectl config view --minify --context="$CONTEXT" -o jsonpath='{.contexts[0].context.namespace}' 2>/dev/null) + fi + [ -z "$NS_GUESS" ] && NS_GUESS="default" + # --- apply --- if [ "$DIFF" = "true" ]; then diff --git a/cue/converge/schema.cue b/cue/converge/schema.cue index 3c735a4..2dc7ac4 100644 --- a/cue/converge/schema.cue +++ b/cue/converge/schema.cue @@ -9,7 +9,15 @@ package converge checks: [...#Check] // True after apply + checks complete successfully. // Downstream steps that import this package gate on this value. + // Set by the engine, not by user CUE files. up: *false | bool + // Namespace derived by the engine from: + // 1. -n CLI arg to kubectl-yconverge + // 2. kustomization.yaml namespace: field + // 3. kubectl context default namespace + // Used as default for #Wait/#Rollout checks that omit namespace. + // Set by the engine, not by user CUE files. + namespaceGuess: *"" | string } // Check is a discriminated union. Each variant maps to a kubectl From 83e225d10a3d3487e76d74c242824706f0c14509 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Fri, 3 Apr 2026 05:55:19 +0000 Subject: [PATCH 18/36] Resolve namespaceGuess from referenced base on indirection When indirection is in effect (yconverge.cue found in a referenced resource directory), the referenced base's kustomization.yaml namespace: field is checked before the current directory's. Priority order is now: 1. -n CLI arg 2. referenced base namespace (indirection) 3. kustomization.yaml namespace: 4. context default namespace Updated indirection itest to use namespaced resource (configmap in itest namespace) instead of cluster-scoped (namespace). Verifies namespace resolution from referenced base. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/kubectl-yconverge | 14 +++++++++++++- cue/converge/schema.cue | 5 +++-- cue/itest/example-indirect/kustomization.yaml | 2 +- cue/itest/test.sh | 12 +++++++++--- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/bin/kubectl-yconverge b/bin/kubectl-yconverge index 1eea197..d249a04 100755 --- a/bin/kubectl-yconverge +++ b/bin/kubectl-yconverge @@ -157,7 +157,10 @@ _converge_one() { fi # --- resolve namespace guess --- - # Priority: 1. -n from OTHER_ARGS, 2. kustomization namespace:, 3. context default + # Priority: 1. -n from OTHER_ARGS + # 2. referenced base namespace (when indirection found yconverge.cue) + # 3. kustomization namespace: + # 4. context default NS_GUESS="" _prev_arg="" for _arg in $OTHER_ARGS; do @@ -167,6 +170,15 @@ _converge_one() { fi _prev_arg="$_arg" done + # When indirection is in effect, the yconverge_dir differs from kdir — + # check the referenced base's kustomization for namespace + if [ -z "$NS_GUESS" ] && [ -n "$yconverge_dir" ] && [ "$yconverge_dir" != "$kdir" ] && [ "$yconverge_dir" != "./$kdir" ]; then + _ref_kust="$yconverge_dir/kustomization.yaml" + [ ! -f "$_ref_kust" ] && _ref_kust="$yconverge_dir/kustomization.yml" + if [ -f "$_ref_kust" ]; then + NS_GUESS=$(y-yq '.namespace // ""' "$_ref_kust" 2>/dev/null) + fi + fi if [ -z "$NS_GUESS" ] && [ -f "$kdir/kustomization.yaml" ]; then NS_GUESS=$(y-yq '.namespace // ""' "$kdir/kustomization.yaml" 2>/dev/null) fi diff --git a/cue/converge/schema.cue b/cue/converge/schema.cue index 2dc7ac4..d7c3539 100644 --- a/cue/converge/schema.cue +++ b/cue/converge/schema.cue @@ -13,8 +13,9 @@ package converge up: *false | bool // Namespace derived by the engine from: // 1. -n CLI arg to kubectl-yconverge - // 2. kustomization.yaml namespace: field - // 3. kubectl context default namespace + // 2. referenced base's kustomization.yaml namespace: (when indirection is in effect) + // 3. kustomization.yaml namespace: field + // 4. kubectl context default namespace // Used as default for #Wait/#Rollout checks that omit namespace. // Set by the engine, not by user CUE files. namespaceGuess: *"" | string diff --git a/cue/itest/example-indirect/kustomization.yaml b/cue/itest/example-indirect/kustomization.yaml index 033d2f8..49829b9 100644 --- a/cue/itest/example-indirect/kustomization.yaml +++ b/cue/itest/example-indirect/kustomization.yaml @@ -1,4 +1,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: -- ../example-namespace +- ../example-configmap diff --git a/cue/itest/test.sh b/cue/itest/test.sh index 66c539e..6f6ff28 100755 --- a/cue/itest/test.sh +++ b/cue/itest/test.sh @@ -135,17 +135,23 @@ kubectl --context="$CTX" -n itest get configmap itest-dependent >/dev/null 2>&1 && pass "dependent configmap exists" \ || fail "dependent configmap missing" -# --- test: one-level indirection --- +# --- test: one-level indirection with namespace from referenced base --- echo "" -echo "# Test: yconverge.cue found via resources indirection" -kubectl --context="$CTX" delete ns itest --wait=true >/dev/null 2>&1 || true # y-script-lint:disable=or-true # clean slate +echo "# Test: indirection finds yconverge.cue and namespace from referenced base" +# example-indirect has no namespace: field but references example-configmap which has namespace: itest +# The check in example-configmap/yconverge.cue uses #Exec with explicit -n itest +# But the namespaceGuess should resolve to "itest" from the referenced base OUTPUT=$(kubectl-yconverge --context="$CTX" -k cue/itest/example-indirect/ 2>&1) || true # y-script-lint:disable=or-true # capture output echo "$OUTPUT" echo "$OUTPUT" | grep -q "\[yconverge\]" \ && pass "indirection: yconverge.cue found in referenced dir" \ || fail "indirection: yconverge.cue not found" +kubectl --context="$CTX" -n itest get configmap itest-config >/dev/null 2>&1 \ + && pass "indirection: configmap created in itest namespace" \ + || fail "indirection: configmap not in itest namespace" + # --- test: idempotent re-converge --- echo "" From 4fbb49f0e7c6b82c4a0e6ef61051408a6991a259 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Fri, 3 Apr 2026 06:03:24 +0000 Subject: [PATCH 19/36] Fail on broken yconverge.cue instead of silently skipping checks If a yconverge.cue file is found, y-cue eval must succeed. A broken file now produces a clear error instead of silently falling back to no checks. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/kubectl-yconverge | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/kubectl-yconverge b/bin/kubectl-yconverge index d249a04..95d1566 100755 --- a/bin/kubectl-yconverge +++ b/bin/kubectl-yconverge @@ -209,7 +209,10 @@ _converge_one() { # --- yconverge.cue: post-apply checks --- if [ -n "$yconverge_dir" ]; then - CHECKS=$(y-cue eval "$yconverge_dir" -e 'step.checks' --out json 2>/dev/null) || true # y-script-lint:disable=or-true # no checks is ok + CHECKS=$(y-cue eval "$yconverge_dir" -e 'step.checks' --out json) || { + echo " [yconverge] ERROR: failed to evaluate $yconverge_dir/yconverge.cue" >&2 + return 1 + } _run_checks "$CHECKS" "check:" fi } From d856bf112843895bff266f59d0650975bbfdc791 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Fri, 3 Apr 2026 06:09:40 +0000 Subject: [PATCH 20/36] Simplify itest to oneliner kubectl-yconverge calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each test is echo + kubectl-yconverge. Negative tests negate with !. No pass/fail counters — set -eo pipefail stops on first failure. Adds negative test for broken yconverge.cue (must fail, not skip). Co-Authored-By: Claude Opus 4.6 (1M context) --- cue/itest/test.sh | 195 +++++++++++++--------------------------------- 1 file changed, 56 insertions(+), 139 deletions(-) diff --git a/cue/itest/test.sh b/cue/itest/test.sh index 6f6ff28..3477e84 100755 --- a/cue/itest/test.sh +++ b/cue/itest/test.sh @@ -13,11 +13,6 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" YSTACK_HOME="$(cd "$SCRIPT_DIR/../.." && pwd)" CONTAINER_NAME="yconverge-itest-$$" CTX="yconverge-itest" -PASS=0 -FAIL=0 - -pass() { PASS=$((PASS + 1)); echo " PASS $1"; } -fail() { FAIL=$((FAIL + 1)); echo " FAIL $1"; } cleanup() { echo "# Cleaning up ..." @@ -36,17 +31,13 @@ docker run -d --name "$CONTAINER_NAME" \ registry.k8s.io/kwok/cluster:v0.7.0-k8s.v1.33.0 PORT=$(docker port "$CONTAINER_NAME" 8080 | head -1 | cut -d: -f2) -# Wait for API server for i in $(seq 1 30); do kubectl --server="http://127.0.0.1:$PORT" get ns default >/dev/null 2>&1 && break sleep 1 done -# Set up context kubectl config set-cluster "$CTX" --server="http://127.0.0.1:$PORT" >/dev/null kubectl config set-context "$CTX" --cluster="$CTX" >/dev/null - -# Verify cluster works kubectl --context="$CTX" get ns default >/dev/null 2>&1 \ && echo "# kwok cluster ready at port $PORT" \ || { echo "# FATAL: kwok cluster not reachable"; exit 1; } @@ -55,169 +46,95 @@ export CONTEXT="$CTX" cd "$YSTACK_HOME" -# --- prerequisites --- - echo "# Ensuring tool binaries are available ..." y-cue version >/dev/null y-yq --version >/dev/null kubectl version --client=true >/dev/null 2>&1 -# --- test: CUE schema validation --- +# --- schema validation --- echo "" -echo "# Test: CUE schema validation" -y-cue vet ./cue/itest/example-namespace/ \ - && pass "example-namespace validates" \ - || fail "example-namespace validation" - -y-cue vet ./cue/itest/example-configmap/ \ - && pass "example-configmap validates (with dependency)" \ - || fail "example-configmap validation" - -y-cue vet ./cue/itest/example-with-dependency/ \ - && pass "example-with-dependency validates (transitive)" \ - || fail "example-with-dependency validation" - -y-cue vet ./cue/itest/example-disabled/ \ - && pass "example-disabled validates (schema only, not enforced by kubectl-yconverge)" \ - || fail "example-disabled validation" +echo "# CUE schema validation" +y-cue vet ./cue/itest/example-namespace/ +y-cue vet ./cue/itest/example-configmap/ +y-cue vet ./cue/itest/example-with-dependency/ +y-cue vet ./cue/itest/example-disabled/ -# --- test: plain kubectl-yconverge (no yconverge.cue) --- +# --- apply with auto-checks --- echo "" -echo "# Test: plain apply without yconverge.cue" -kubectl-yconverge --context="$CTX" -k cue/itest/example-namespace/ >/dev/null 2>&1 \ - && pass "plain apply namespace" \ - || fail "plain apply namespace" - -kubectl --context="$CTX" get ns itest >/dev/null 2>&1 \ - && pass "namespace itest exists after apply" \ - || fail "namespace itest missing after apply" - -# Clean up for next test -kubectl --context="$CTX" delete ns itest --wait=true >/dev/null 2>&1 - -# --- test: kubectl-yconverge with yconverge.cue checks --- +echo "# Apply with auto-checks (namespace)" +kubectl-yconverge --context="$CTX" -k cue/itest/example-namespace/ echo "" -echo "# Test: apply with auto-checks" -OUTPUT=$(kubectl-yconverge --context="$CTX" -k cue/itest/example-namespace/ 2>&1) -echo "$OUTPUT" -echo "$OUTPUT" | grep -q "\[yconverge\]" \ - && pass "yconverge.cue detected and checks ran" \ - || fail "yconverge.cue not detected" - -# --- test: apply with dependency (configmap after namespace) --- +echo "# Apply with checks (configmap depends on namespace)" +kubectl-yconverge --context="$CTX" -k cue/itest/example-configmap/ echo "" -echo "# Test: apply with dependency (configmap needs namespace)" -OUTPUT=$(kubectl-yconverge --context="$CTX" -k cue/itest/example-configmap/ 2>&1) || true # y-script-lint:disable=or-true # capture output even on failure -echo "$OUTPUT" -echo "$OUTPUT" | grep -q "\[yconverge\]" \ - && pass "configmap applied with dependency checks" \ - || fail "configmap apply failed" - -kubectl --context="$CTX" -n itest get configmap itest-config >/dev/null 2>&1 \ - && pass "configmap itest-config exists" \ - || fail "configmap itest-config missing" +echo "# Transitive dependency (depends on configmap which depends on namespace)" +kubectl-yconverge --context="$CTX" -k cue/itest/example-with-dependency/ -# --- test: transitive dependency --- +# --- indirection with namespace from referenced base --- echo "" -echo "# Test: transitive dependency (depends on configmap which depends on namespace)" -OUTPUT=$(kubectl-yconverge --context="$CTX" -k cue/itest/example-with-dependency/ 2>&1) || true # y-script-lint:disable=or-true # capture output -echo "$OUTPUT" -echo "$OUTPUT" | grep -q "\[yconverge\]" \ - && pass "transitive dependency converge" \ - || fail "transitive dependency failed" +echo "# Indirection: yconverge.cue and namespace from referenced base" +kubectl-yconverge --context="$CTX" -k cue/itest/example-indirect/ -kubectl --context="$CTX" -n itest get configmap itest-dependent >/dev/null 2>&1 \ - && pass "dependent configmap exists" \ - || fail "dependent configmap missing" - -# --- test: one-level indirection with namespace from referenced base --- - -echo "" -echo "# Test: indirection finds yconverge.cue and namespace from referenced base" -# example-indirect has no namespace: field but references example-configmap which has namespace: itest -# The check in example-configmap/yconverge.cue uses #Exec with explicit -n itest -# But the namespaceGuess should resolve to "itest" from the referenced base -OUTPUT=$(kubectl-yconverge --context="$CTX" -k cue/itest/example-indirect/ 2>&1) || true # y-script-lint:disable=or-true # capture output -echo "$OUTPUT" -echo "$OUTPUT" | grep -q "\[yconverge\]" \ - && pass "indirection: yconverge.cue found in referenced dir" \ - || fail "indirection: yconverge.cue not found" - -kubectl --context="$CTX" -n itest get configmap itest-config >/dev/null 2>&1 \ - && pass "indirection: configmap created in itest namespace" \ - || fail "indirection: configmap not in itest namespace" - -# --- test: idempotent re-converge --- - -echo "" -echo "# Test: idempotent re-apply" -kubectl-yconverge --context="$CTX" -k cue/itest/example-namespace/ >/dev/null 2>&1 \ - && pass "re-apply namespace (idempotent)" \ - || fail "re-apply namespace failed" - -kubectl-yconverge --context="$CTX" -k cue/itest/example-configmap/ >/dev/null 2>&1 \ - && pass "re-apply configmap (idempotent)" \ - || fail "re-apply configmap failed" - -# --- test: error reporting --- +# --- idempotent re-converge --- echo "" -echo "# Test: error reporting on check failure" -# Apply to a non-existent namespace to trigger a check failure -OUTPUT=$(kubectl-yconverge --context="$CTX" -k cue/itest/example-configmap/ 2>&1) || true # y-script-lint:disable=or-true # expect possible failure -echo "$OUTPUT" | grep -q "configmap" \ - && pass "error output mentions the resource" \ - || fail "error output unhelpful" +echo "# Idempotent re-apply" +kubectl-yconverge --context="$CTX" -k cue/itest/example-namespace/ +kubectl-yconverge --context="$CTX" -k cue/itest/example-configmap/ -# --- test: multiple -k args --- +# --- multiple -k args --- echo "" -echo "# Test: multiple -k args in one invocation" +echo "# Multiple -k args" kubectl --context="$CTX" delete ns itest --wait=true >/dev/null 2>&1 || true # y-script-lint:disable=or-true # clean slate -OUTPUT=$(kubectl-yconverge --context="$CTX" \ +kubectl-yconverge --context="$CTX" \ -k cue/itest/example-namespace/ \ -k cue/itest/example-configmap/ \ - -k cue/itest/example-with-dependency/ 2>&1) || true # y-script-lint:disable=or-true # capture output -echo "$OUTPUT" -echo "$OUTPUT" | grep -c "\[yconverge\] found" | grep -q "3" \ - && pass "three yconverge.cue files found" \ - || fail "expected 3 yconverge.cue files" - -kubectl --context="$CTX" -n itest get configmap itest-config itest-dependent >/dev/null 2>&1 \ - && pass "both configmaps created via multi -k" \ - || fail "configmaps missing after multi -k" + -k cue/itest/example-with-dependency/ -# --- test: converge-mode labels (empty selector results) --- +# --- converge-mode labels --- echo "" -echo "# Test: serverside-force label (other selectors match nothing)" -kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-serverside/ 2>&1 -kubectl --context="$CTX" -n itest get configmap itest-serverside >/dev/null 2>&1 \ - && pass "serverside-force configmap created" \ - || fail "serverside-force configmap missing" +echo "# Serverside-force label (other selectors match nothing)" +kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-serverside/ +kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-serverside/ -# Re-apply should be idempotent (all empty selectors handled gracefully) -kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-serverside/ >/dev/null 2>&1 \ - && pass "serverside-force re-apply idempotent" \ - || fail "serverside-force re-apply failed" - -# --- test: --skip-checks flag --- +# --- negative: --skip-checks suppresses check invocation --- echo "" -echo "# Test: --skip-checks suppresses check invocation" -OUTPUT=$(kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-namespace/ 2>&1) -echo "$OUTPUT" | grep -q "\[yconverge\]" \ - && fail "--skip-checks still ran checks" \ - || pass "--skip-checks suppressed checks" +echo "# --skip-checks must not produce [yconverge] output" +! kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-namespace/ 2>&1 | grep -q "\[yconverge\]" -# --- results --- +# --- negative: broken yconverge.cue must fail --- echo "" -echo "=== Results: $PASS passed, $FAIL failed ===" +echo "# Broken yconverge.cue must fail" +mkdir -p /tmp/yconverge-itest-broken +cat > /tmp/yconverge-itest-broken/kustomization.yaml << 'YAML' +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- configmap.yaml +YAML +cat > /tmp/yconverge-itest-broken/configmap.yaml << 'YAML' +apiVersion: v1 +kind: ConfigMap +metadata: + name: broken-test + namespace: default +data: {} +YAML +cat > /tmp/yconverge-itest-broken/yconverge.cue << 'CUE' +package broken +this_is_not_valid_cue: !!! +CUE +! kubectl-yconverge --context="$CTX" -k /tmp/yconverge-itest-broken/ +rm -rf /tmp/yconverge-itest-broken -[ "$FAIL" -eq 0 ] +echo "" +echo "=== All tests passed ===" From 88a582a57295a39dd419cf1eb05ddede5732e4d5 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Fri, 3 Apr 2026 06:10:22 +0000 Subject: [PATCH 21/36] Prefix all itest output with [cue itest] Co-Authored-By: Claude Opus 4.6 (1M context) --- cue/itest/test.sh | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/cue/itest/test.sh b/cue/itest/test.sh index 3477e84..913db6c 100755 --- a/cue/itest/test.sh +++ b/cue/itest/test.sh @@ -15,17 +15,17 @@ CONTAINER_NAME="yconverge-itest-$$" CTX="yconverge-itest" cleanup() { - echo "# Cleaning up ..." + echo "[cue itest] Cleaning up ..." docker rm -f "$CONTAINER_NAME" 2>/dev/null || true # y-script-lint:disable=or-true # best-effort cleanup kubectl config delete-context "$CTX" 2>/dev/null || true # y-script-lint:disable=or-true # best-effort cleanup } trap cleanup EXIT -echo "=== yconverge framework integration tests ===" +echo "[cue itest] yconverge framework integration tests" # --- start kwok cluster --- -echo "# Starting kwok cluster ..." +echo "[cue itest] Starting kwok cluster ..." docker run -d --name "$CONTAINER_NAME" \ -p 0:8080 \ registry.k8s.io/kwok/cluster:v0.7.0-k8s.v1.33.0 @@ -39,14 +39,14 @@ done kubectl config set-cluster "$CTX" --server="http://127.0.0.1:$PORT" >/dev/null kubectl config set-context "$CTX" --cluster="$CTX" >/dev/null kubectl --context="$CTX" get ns default >/dev/null 2>&1 \ - && echo "# kwok cluster ready at port $PORT" \ - || { echo "# FATAL: kwok cluster not reachable"; exit 1; } + && echo "[cue itest] kwok cluster ready at port $PORT" \ + || { echo "[cue itest] FATAL: kwok cluster not reachable"; exit 1; } export CONTEXT="$CTX" cd "$YSTACK_HOME" -echo "# Ensuring tool binaries are available ..." +echo "[cue itest] Ensuring tool binaries are available ..." y-cue version >/dev/null y-yq --version >/dev/null kubectl version --client=true >/dev/null 2>&1 @@ -54,7 +54,7 @@ kubectl version --client=true >/dev/null 2>&1 # --- schema validation --- echo "" -echo "# CUE schema validation" +echo "[cue itest] CUE schema validation" y-cue vet ./cue/itest/example-namespace/ y-cue vet ./cue/itest/example-configmap/ y-cue vet ./cue/itest/example-with-dependency/ @@ -63,34 +63,34 @@ y-cue vet ./cue/itest/example-disabled/ # --- apply with auto-checks --- echo "" -echo "# Apply with auto-checks (namespace)" +echo "[cue itest] Apply with auto-checks (namespace)" kubectl-yconverge --context="$CTX" -k cue/itest/example-namespace/ echo "" -echo "# Apply with checks (configmap depends on namespace)" +echo "[cue itest] Apply with checks (configmap depends on namespace)" kubectl-yconverge --context="$CTX" -k cue/itest/example-configmap/ echo "" -echo "# Transitive dependency (depends on configmap which depends on namespace)" +echo "[cue itest] Transitive dependency (depends on configmap which depends on namespace)" kubectl-yconverge --context="$CTX" -k cue/itest/example-with-dependency/ # --- indirection with namespace from referenced base --- echo "" -echo "# Indirection: yconverge.cue and namespace from referenced base" +echo "[cue itest] Indirection: yconverge.cue and namespace from referenced base" kubectl-yconverge --context="$CTX" -k cue/itest/example-indirect/ # --- idempotent re-converge --- echo "" -echo "# Idempotent re-apply" +echo "[cue itest] Idempotent re-apply" kubectl-yconverge --context="$CTX" -k cue/itest/example-namespace/ kubectl-yconverge --context="$CTX" -k cue/itest/example-configmap/ # --- multiple -k args --- echo "" -echo "# Multiple -k args" +echo "[cue itest] Multiple -k args" kubectl --context="$CTX" delete ns itest --wait=true >/dev/null 2>&1 || true # y-script-lint:disable=or-true # clean slate kubectl-yconverge --context="$CTX" \ -k cue/itest/example-namespace/ \ @@ -100,20 +100,20 @@ kubectl-yconverge --context="$CTX" \ # --- converge-mode labels --- echo "" -echo "# Serverside-force label (other selectors match nothing)" +echo "[cue itest] Serverside-force label (other selectors match nothing)" kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-serverside/ kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-serverside/ # --- negative: --skip-checks suppresses check invocation --- echo "" -echo "# --skip-checks must not produce [yconverge] output" +echo "[cue itest] --skip-checks must not produce [yconverge] output" ! kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-namespace/ 2>&1 | grep -q "\[yconverge\]" # --- negative: broken yconverge.cue must fail --- echo "" -echo "# Broken yconverge.cue must fail" +echo "[cue itest] Broken yconverge.cue must fail" mkdir -p /tmp/yconverge-itest-broken cat > /tmp/yconverge-itest-broken/kustomization.yaml << 'YAML' apiVersion: kustomize.config.k8s.io/v1beta1 @@ -137,4 +137,4 @@ CUE rm -rf /tmp/yconverge-itest-broken echo "" -echo "=== All tests passed ===" +echo "[cue itest] All tests passed" From 538710cc2e673f143de003d9b9806e37c5b5d755 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Fri, 3 Apr 2026 06:12:45 +0000 Subject: [PATCH 22/36] Add output assertions via tee to itest Assert on stdout/stderr using tee to tmp file + grep: - Multi -k shows exactly 3 yconverge.cue discoveries - Indirection output references the base directory path - --skip-checks produces no [yconverge] output - Broken yconverge.cue produces ERROR message Co-Authored-By: Claude Opus 4.6 (1M context) --- cue/itest/test.sh | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/cue/itest/test.sh b/cue/itest/test.sh index 913db6c..d29d09c 100755 --- a/cue/itest/test.sh +++ b/cue/itest/test.sh @@ -87,33 +87,44 @@ echo "[cue itest] Idempotent re-apply" kubectl-yconverge --context="$CTX" -k cue/itest/example-namespace/ kubectl-yconverge --context="$CTX" -k cue/itest/example-configmap/ -# --- multiple -k args --- +# --- converge-mode labels --- echo "" -echo "[cue itest] Multiple -k args" +echo "[cue itest] Serverside-force label (other selectors match nothing)" +kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-serverside/ +kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-serverside/ + +_OUT=$(mktemp /tmp/yconverge-itest-out.XXXXXX) + +# --- assert: multi -k shows 3 yconverge.cue discoveries --- + +echo "" +echo "[cue itest] Multi -k output must show 3 yconverge.cue files" kubectl --context="$CTX" delete ns itest --wait=true >/dev/null 2>&1 || true # y-script-lint:disable=or-true # clean slate kubectl-yconverge --context="$CTX" \ -k cue/itest/example-namespace/ \ -k cue/itest/example-configmap/ \ - -k cue/itest/example-with-dependency/ + -k cue/itest/example-with-dependency/ 2>&1 | tee "$_OUT" +test "$(grep -c '\[yconverge\] found' "$_OUT")" = "3" -# --- converge-mode labels --- +# --- assert: indirection output shows referenced path --- echo "" -echo "[cue itest] Serverside-force label (other selectors match nothing)" -kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-serverside/ -kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-serverside/ +echo "[cue itest] Indirection output must reference the base directory" +kubectl-yconverge --context="$CTX" -k cue/itest/example-indirect/ 2>&1 | tee "$_OUT" +grep -q "example-configmap/yconverge.cue" "$_OUT" # --- negative: --skip-checks suppresses check invocation --- echo "" echo "[cue itest] --skip-checks must not produce [yconverge] output" -! kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-namespace/ 2>&1 | grep -q "\[yconverge\]" +kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-namespace/ 2>&1 | tee "$_OUT" +! grep -q "\[yconverge\]" "$_OUT" # --- negative: broken yconverge.cue must fail --- echo "" -echo "[cue itest] Broken yconverge.cue must fail" +echo "[cue itest] Broken yconverge.cue must fail with error message" mkdir -p /tmp/yconverge-itest-broken cat > /tmp/yconverge-itest-broken/kustomization.yaml << 'YAML' apiVersion: kustomize.config.k8s.io/v1beta1 @@ -133,8 +144,11 @@ cat > /tmp/yconverge-itest-broken/yconverge.cue << 'CUE' package broken this_is_not_valid_cue: !!! CUE -! kubectl-yconverge --context="$CTX" -k /tmp/yconverge-itest-broken/ +! kubectl-yconverge --context="$CTX" -k /tmp/yconverge-itest-broken/ 2>&1 | tee "$_OUT" +grep -q "ERROR" "$_OUT" rm -rf /tmp/yconverge-itest-broken +rm -f "$_OUT" + echo "" echo "[cue itest] All tests passed" From f2759be0ce5d05690cdd2da76fa2a17264836d0d Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Fri, 3 Apr 2026 06:16:00 +0000 Subject: [PATCH 23/36] Drop multi -k support from kubectl-yconverge Simplifies arg parsing and control flow. Each invocation handles one -k directory. Callers use separate calls for multiple steps. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/kubectl-yconverge | 251 +++++++++++++++++++----------------------- cue/itest/test.sh | 11 -- 2 files changed, 115 insertions(+), 147 deletions(-) diff --git a/bin/kubectl-yconverge b/bin/kubectl-yconverge index 95d1566..c7b511d 100755 --- a/bin/kubectl-yconverge +++ b/bin/kubectl-yconverge @@ -18,12 +18,8 @@ Flags (must come before kustomize args): --dry-run=true pass --dry-run=server to kubectl apply --skip-checks skip yconverge.cue check invocation -Multiple -k args are supported. Each runs the full cycle -(prechecks -> apply -> checks) sequentially in order. - -If a kustomization directory contains a yconverge.cue file (or one is -found one level of resources: indirection away), precondition checks -run before apply, and this steps checks run after apply. +If the -k directory contains a yconverge.cue file (or one is found +one level of resources: indirection away), checks run after apply. Honors KUBECONFIG if set. ' && exit 0 @@ -47,30 +43,29 @@ while true; do esac done -# Separate -k dirs from other args -KUSTOMIZE_DIRS="" -OTHER_ARGS="" -_prev="" +# Extract -k directory from remaining args +KUSTOMIZE_DIR="" for arg in "$@"; do case "$arg" in -l|--selector) echo "Error: yconverge can not be combined with other selectors" >&2 && exit 1 ;; esac +done +_prev="" +for arg in "$@"; do if [ "$_prev" = "-k" ]; then - KUSTOMIZE_DIRS="$KUSTOMIZE_DIRS ${arg%/}" - _prev="" - continue + KUSTOMIZE_DIR="${arg%/}" + break fi case "$arg" in -k) _prev="-k" ;; - -k*) KUSTOMIZE_DIRS="$KUSTOMIZE_DIRS ${arg#-k}" ;; - *) OTHER_ARGS="$OTHER_ARGS $arg"; _prev="" ;; + -k*) KUSTOMIZE_DIR="${arg#-k}"; KUSTOMIZE_DIR="${KUSTOMIZE_DIR%/}"; break ;; esac done CONTEXT="${ctx#--context=}" export CONTEXT -# --- shared functions --- +# --- yconverge.cue lookup --- _find_yconverge_dir() { dir="$1" @@ -89,44 +84,60 @@ _find_yconverge_dir() { return 1 } -_run_checks() { - checks_json="$1" - label="$2" - [ -z "$checks_json" ] || [ "$checks_json" = "[]" ] && return 0 - count=$(echo "$checks_json" | y-yq '. | length' - 2>/dev/null) - [ "$count" = "0" ] && return 0 - i=0 - while [ "$i" -lt "$count" ]; do - kind=$(echo "$checks_json" | y-yq ".[$i].kind" -) - desc=$(echo "$checks_json" | y-yq ".[$i].description // \"\"" -) - resource=$(echo "$checks_json" | y-yq ".[$i].resource // \"\"" -) - forcond=$(echo "$checks_json" | y-yq ".[$i].for // \"\"" -) - ns=$(echo "$checks_json" | y-yq ".[$i].namespace // \"\"" -) - timeout=$(echo "$checks_json" | y-yq ".[$i].timeout // \"60s\"" -) - command=$(echo "$checks_json" | y-yq ".[$i].command // \"\"" -) - # Use NS_GUESS as fallback when check doesn't specify namespace - [ -z "$ns" ] && ns="$NS_GUESS" - ns_flag="" - [ -n "$ns" ] && ns_flag="-n $ns" - case "$kind" in - wait) - echo " [yconverge] $label wait $resource $forcond" - kubectl --context="$CONTEXT" wait --for="$forcond" --timeout="$timeout" $ns_flag "$resource" - ;; - rollout) - echo " [yconverge] $label rollout $resource" - kubectl --context="$CONTEXT" rollout status --timeout="$timeout" $ns_flag "$resource" - ;; - exec) - echo " [yconverge] $label $desc" - for _retry_i in $(seq 1 15); do - sh -c "$command" && break || sleep 2 # y-script-lint:disable=or-true # retry loop - done - ;; - esac - i=$((i + 1)) - done -} +yconverge_dir="" +if [ -n "$KUSTOMIZE_DIR" ] && [ "$SKIP_CHECKS" = "false" ] && [ "$DRY_RUN" = "false" ] && [ "$DIFF" = "false" ]; then + yconverge_dir=$(_find_yconverge_dir "$KUSTOMIZE_DIR") || true # y-script-lint:disable=or-true # no yconverge.cue is ok +fi + +if [ -n "$yconverge_dir" ]; then + echo " [yconverge] found $yconverge_dir/yconverge.cue" + case "$yconverge_dir" in + ./*|/*) ;; + *) yconverge_dir="./$yconverge_dir" ;; + esac +fi + +# --- resolve namespace guess --- +# Priority: 1. -n CLI arg +# 2. referenced base namespace (when indirection found yconverge.cue) +# 3. kustomization namespace: +# 4. context default +NS_GUESS="" +_prev="" +for arg in "$@"; do + if [ "$_prev" = "-n" ]; then + NS_GUESS="$arg" + break + fi + _prev="$arg" +done +if [ -z "$NS_GUESS" ] && [ -n "$yconverge_dir" ] && [ -n "$KUSTOMIZE_DIR" ] && [ "$yconverge_dir" != "$KUSTOMIZE_DIR" ] && [ "$yconverge_dir" != "./$KUSTOMIZE_DIR" ]; then + _ref_kust="$yconverge_dir/kustomization.yaml" + [ ! -f "$_ref_kust" ] && _ref_kust="$yconverge_dir/kustomization.yml" + [ -f "$_ref_kust" ] && NS_GUESS=$(y-yq '.namespace // ""' "$_ref_kust" 2>/dev/null) +fi +if [ -z "$NS_GUESS" ] && [ -n "$KUSTOMIZE_DIR" ] && [ -f "$KUSTOMIZE_DIR/kustomization.yaml" ]; then + NS_GUESS=$(y-yq '.namespace // ""' "$KUSTOMIZE_DIR/kustomization.yaml" 2>/dev/null) +fi +if [ -z "$NS_GUESS" ]; then + NS_GUESS=$(kubectl config view --minify --context="$CONTEXT" -o jsonpath='{.contexts[0].context.namespace}' 2>/dev/null) +fi +[ -z "$NS_GUESS" ] && NS_GUESS="default" + +# --- apply --- + +if [ "$DIFF" = "true" ]; then + kubectl $ctx diff "$@" + exit $? +fi + +DRY_RUN_FLAG="" +[ "$DRY_RUN" = "true" ] && DRY_RUN_FLAG="--dry-run=server" + +kubectl $ctx create --save-config $DRY_RUN_FLAG --selector=yolean.se/converge-mode=create "$@" 2>/dev/null || true # y-script-lint:disable=or-true # idempotent +if [ "$DRY_RUN" = "false" ]; then + kubectl $ctx delete --selector=yolean.se/converge-mode=replace "$@" 2>/dev/null || true # y-script-lint:disable=or-true # ignore not-found +fi _apply_if_any() { output=$(kubectl "$@" 2>&1) || { @@ -139,86 +150,54 @@ _apply_if_any() { [ -n "$output" ] && printf '%s\n' "$output" } -_converge_one() { - kdir="$1" - yconverge_dir="" - - if [ "$SKIP_CHECKS" = "false" ] && [ "$DRY_RUN" = "false" ] && [ "$DIFF" = "false" ]; then - yconverge_dir=$(_find_yconverge_dir "$kdir") || true # y-script-lint:disable=or-true # no yconverge.cue is ok - fi - - if [ -n "$yconverge_dir" ]; then - echo " [yconverge] found $yconverge_dir/yconverge.cue" - - case "$yconverge_dir" in - ./*|/*) ;; - *) yconverge_dir="./$yconverge_dir" ;; - esac - fi - - # --- resolve namespace guess --- - # Priority: 1. -n from OTHER_ARGS - # 2. referenced base namespace (when indirection found yconverge.cue) - # 3. kustomization namespace: - # 4. context default - NS_GUESS="" - _prev_arg="" - for _arg in $OTHER_ARGS; do - if [ "$_prev_arg" = "-n" ]; then - NS_GUESS="$_arg" - break - fi - _prev_arg="$_arg" - done - # When indirection is in effect, the yconverge_dir differs from kdir — - # check the referenced base's kustomization for namespace - if [ -z "$NS_GUESS" ] && [ -n "$yconverge_dir" ] && [ "$yconverge_dir" != "$kdir" ] && [ "$yconverge_dir" != "./$kdir" ]; then - _ref_kust="$yconverge_dir/kustomization.yaml" - [ ! -f "$_ref_kust" ] && _ref_kust="$yconverge_dir/kustomization.yml" - if [ -f "$_ref_kust" ]; then - NS_GUESS=$(y-yq '.namespace // ""' "$_ref_kust" 2>/dev/null) - fi - fi - if [ -z "$NS_GUESS" ] && [ -f "$kdir/kustomization.yaml" ]; then - NS_GUESS=$(y-yq '.namespace // ""' "$kdir/kustomization.yaml" 2>/dev/null) - fi - if [ -z "$NS_GUESS" ]; then - NS_GUESS=$(kubectl config view --minify --context="$CONTEXT" -o jsonpath='{.contexts[0].context.namespace}' 2>/dev/null) - fi - [ -z "$NS_GUESS" ] && NS_GUESS="default" - - # --- apply --- - - if [ "$DIFF" = "true" ]; then - kubectl $ctx diff -k "$kdir" $OTHER_ARGS - return $? - fi - - DRY_RUN_FLAG="" - [ "$DRY_RUN" = "true" ] && DRY_RUN_FLAG="--dry-run=server" - - kubectl $ctx create --save-config $DRY_RUN_FLAG --selector=yolean.se/converge-mode=create -k "$kdir" $OTHER_ARGS 2>/dev/null || true # y-script-lint:disable=or-true # idempotent - if [ "$DRY_RUN" = "false" ]; then - kubectl $ctx delete --selector=yolean.se/converge-mode=replace -k "$kdir" $OTHER_ARGS 2>/dev/null || true # y-script-lint:disable=or-true # ignore not-found - fi - - _apply_if_any $ctx apply --server-side --force-conflicts $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside-force -k "$kdir" $OTHER_ARGS - _apply_if_any $ctx apply --server-side $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside -k "$kdir" $OTHER_ARGS - _apply_if_any $ctx apply $DRY_RUN_FLAG --selector='yolean.se/converge-mode!=create,yolean.se/converge-mode!=serverside,yolean.se/converge-mode!=serverside-force' -k "$kdir" $OTHER_ARGS - - # --- yconverge.cue: post-apply checks --- - - if [ -n "$yconverge_dir" ]; then - CHECKS=$(y-cue eval "$yconverge_dir" -e 'step.checks' --out json) || { - echo " [yconverge] ERROR: failed to evaluate $yconverge_dir/yconverge.cue" >&2 - return 1 - } - _run_checks "$CHECKS" "check:" - fi -} - -# --- main --- +_apply_if_any $ctx apply --server-side --force-conflicts $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside-force "$@" +_apply_if_any $ctx apply --server-side $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside "$@" +_apply_if_any $ctx apply $DRY_RUN_FLAG --selector='yolean.se/converge-mode!=create,yolean.se/converge-mode!=serverside,yolean.se/converge-mode!=serverside-force' "$@" + +# --- yconverge.cue: post-apply checks --- + +if [ -n "$yconverge_dir" ]; then + _run_checks() { + checks_json="$1" + label="$2" + [ -z "$checks_json" ] || [ "$checks_json" = "[]" ] && return 0 + count=$(echo "$checks_json" | y-yq '. | length' - 2>/dev/null) + [ "$count" = "0" ] && return 0 + i=0 + while [ "$i" -lt "$count" ]; do + kind=$(echo "$checks_json" | y-yq ".[$i].kind" -) + desc=$(echo "$checks_json" | y-yq ".[$i].description // \"\"" -) + resource=$(echo "$checks_json" | y-yq ".[$i].resource // \"\"" -) + forcond=$(echo "$checks_json" | y-yq ".[$i].for // \"\"" -) + ns=$(echo "$checks_json" | y-yq ".[$i].namespace // \"\"" -) + timeout=$(echo "$checks_json" | y-yq ".[$i].timeout // \"60s\"" -) + command=$(echo "$checks_json" | y-yq ".[$i].command // \"\"" -) + [ -z "$ns" ] && ns="$NS_GUESS" + ns_flag="" + [ -n "$ns" ] && ns_flag="-n $ns" + case "$kind" in + wait) + echo " [yconverge] $label wait $resource $forcond" + kubectl --context="$CONTEXT" wait --for="$forcond" --timeout="$timeout" $ns_flag "$resource" + ;; + rollout) + echo " [yconverge] $label rollout $resource" + kubectl --context="$CONTEXT" rollout status --timeout="$timeout" $ns_flag "$resource" + ;; + exec) + echo " [yconverge] $label $desc" + for _retry_i in $(seq 1 15); do + sh -c "$command" && break || sleep 2 # y-script-lint:disable=or-true # retry loop + done + ;; + esac + i=$((i + 1)) + done + } -for kdir in $KUSTOMIZE_DIRS; do - _converge_one "$kdir" -done + CHECKS=$(y-cue eval "$yconverge_dir" -e 'step.checks' --out json) || { + echo " [yconverge] ERROR: failed to evaluate $yconverge_dir/yconverge.cue" >&2 + exit 1 + } + _run_checks "$CHECKS" "check:" +fi diff --git a/cue/itest/test.sh b/cue/itest/test.sh index d29d09c..12060b1 100755 --- a/cue/itest/test.sh +++ b/cue/itest/test.sh @@ -96,17 +96,6 @@ kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-serverside _OUT=$(mktemp /tmp/yconverge-itest-out.XXXXXX) -# --- assert: multi -k shows 3 yconverge.cue discoveries --- - -echo "" -echo "[cue itest] Multi -k output must show 3 yconverge.cue files" -kubectl --context="$CTX" delete ns itest --wait=true >/dev/null 2>&1 || true # y-script-lint:disable=or-true # clean slate -kubectl-yconverge --context="$CTX" \ - -k cue/itest/example-namespace/ \ - -k cue/itest/example-configmap/ \ - -k cue/itest/example-with-dependency/ 2>&1 | tee "$_OUT" -test "$(grep -c '\[yconverge\] found' "$_OUT")" = "3" - # --- assert: indirection output shows referenced path --- echo "" From cf79ad04be83dba892888ddead5684a851a9da08 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Fri, 3 Apr 2026 06:25:15 +0000 Subject: [PATCH 24/36] Remove explicit namespace from checks where namespaceGuess suffices 60-builds-registry: kustomization has namespace: ystack, so rollout check uses namespaceGuess instead of explicit namespace. 11-monitoring-operator: targets default namespace which is what namespaceGuess resolves to. Fix: kubectl config view for namespace resolution must not fail under set -e when context has no default namespace. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/kubectl-yconverge | 2 +- cue/converge-ystack/converge_tool.cue | 24 ++++++++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/bin/kubectl-yconverge b/bin/kubectl-yconverge index c7b511d..3ac10d8 100755 --- a/bin/kubectl-yconverge +++ b/bin/kubectl-yconverge @@ -120,7 +120,7 @@ if [ -z "$NS_GUESS" ] && [ -n "$KUSTOMIZE_DIR" ] && [ -f "$KUSTOMIZE_DIR/kustomi NS_GUESS=$(y-yq '.namespace // ""' "$KUSTOMIZE_DIR/kustomization.yaml" 2>/dev/null) fi if [ -z "$NS_GUESS" ]; then - NS_GUESS=$(kubectl config view --minify --context="$CONTEXT" -o jsonpath='{.contexts[0].context.namespace}' 2>/dev/null) + NS_GUESS=$(kubectl config view --minify --context="$CONTEXT" -o jsonpath='{.contexts[0].context.namespace}' 2>/dev/null) || true # y-script-lint:disable=or-true # context may not have default namespace fi [ -z "$NS_GUESS" ] && NS_GUESS="default" diff --git a/cue/converge-ystack/converge_tool.cue b/cue/converge-ystack/converge_tool.cue index 3edc379..b563fc4 100644 --- a/cue/converge-ystack/converge_tool.cue +++ b/cue/converge-ystack/converge_tool.cue @@ -13,12 +13,10 @@ _kubeconfig: *"" | string @tag(kubeconfig) _overrideIP: *"" | string @tag(overrideIP) _env: { - CONTEXT: _context - PATH: _path + CONTEXT: _context + PATH: _path OVERRIDE_IP: _overrideIP - if _kubeconfig != "" { - KUBECONFIG: _kubeconfig - } + KUBECONFIG: _kubeconfig } // Build human-readable plan @@ -60,7 +58,7 @@ _stepCmds: [for s in steps { if c.kind == "exec" {"echo ' check: \(c.description)' && { for _retry_i in $(seq 1 15); do " + c.command + " && break || sleep 2; done; }"} }] let _body = strings.Join(list.Concat([[_apply], _checkCmds]), "\n") - "echo '>>> \(s.path)'\nif ! (\n\(_body)\n); then\n echo ''\n echo \"FAILED: \(s.path)\"\n echo 'The step above failed. Re-run to retry from this point.'\n exit 1\nfi" + "echo '>>> \(s.path)'\n\(_body)" }] _script: strings.Join(list.Concat([ @@ -77,14 +75,20 @@ command: converge: { writeScript: exec.Run & { $after: printPlan - cmd: ["sh", "-c", "SCRIPT=$(mktemp /tmp/ystack-converge.XXXXXX.sh) && cat > $SCRIPT && echo $SCRIPT"] - stdin: _script + cmd: ["sh", "-c", "F=$(mktemp /tmp/ystack-converge.XXXXXX.sh) && cat > $F && echo $F"] + stdin: _script stdout: string } run: exec.Run & { $after: writeScript - cmd: ["sh", "-c", "sh " + strings.TrimSpace(writeScript.stdout) + "; EXIT=$?; rm -f " + strings.TrimSpace(writeScript.stdout) + "; exit $EXIT"] - env: _env + cmd: [ + "bash", "-c", + "export CONTEXT=" + _context + " OVERRIDE_IP=" + _overrideIP + + " PATH=" + _path + + " KUBECONFIG=" + _kubeconfig + + "; F=" + strings.TrimSpace(writeScript.stdout) + + "; bash $F; E=$?; rm -f $F; exit $E", + ] } } From 10081e625d6432409d6160a2af4a7d664caa7bfe Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Sat, 4 Apr 2026 13:39:34 +0000 Subject: [PATCH 25/36] Move dependency resolution into kubectl-yconverge, remove CUE engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kubectl-yconverge now resolves the dependency tree from yconverge.cue imports on first invocation. Each dependency is converged by calling kubectl-yconverge recursively (with _YCONVERGE_RESOLVING=1 to prevent infinite recursion). New: bin/y-yconverge-deps — extracts topological order from CUE imports. Removed: cue/converge-ystack/ — the CUE runtime engine is replaced by dependency resolution in kubectl-yconverge itself. y-cluster-converge-ystack simplified to four leaf-target calls. Each resolves its own dependency chain. Shared deps are idempotent. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/kubectl-yconverge | 25 ++++++- bin/y-cluster-converge-ystack | 26 +++++--- bin/y-yconverge-deps | 63 ++++++++++++++++++ cue/converge-ystack/converge_tool.cue | 94 --------------------------- cue/converge-ystack/steps.cue | 51 --------------- 5 files changed, 103 insertions(+), 156 deletions(-) create mode 100755 bin/y-yconverge-deps delete mode 100644 cue/converge-ystack/converge_tool.cue delete mode 100644 cue/converge-ystack/steps.cue diff --git a/bin/kubectl-yconverge b/bin/kubectl-yconverge index 3ac10d8..5e5285c 100755 --- a/bin/kubectl-yconverge +++ b/bin/kubectl-yconverge @@ -19,7 +19,9 @@ Flags (must come before kustomize args): --skip-checks skip yconverge.cue check invocation If the -k directory contains a yconverge.cue file (or one is found -one level of resources: indirection away), checks run after apply. +one level of resources: indirection away): + - Dependencies from CUE imports are resolved and converged first + - Checks run after apply Honors KUBECONFIG if set. ' && exit 0 @@ -65,6 +67,27 @@ done CONTEXT="${ctx#--context=}" export CONTEXT +# --- dependency resolution --- +# On first invocation, resolve the dependency tree and call self for each step. +# Inner calls (during resolution) skip this and go straight to apply + checks. + +if [ -z "$_YCONVERGE_RESOLVING" ] && [ -n "$KUSTOMIZE_DIR" ] && [ "$SKIP_CHECKS" = "false" ] && [ "$DIFF" = "false" ]; then + deps=$(y-yconverge-deps "$KUSTOMIZE_DIR" 2>/dev/null) || true # y-script-lint:disable=or-true # no deps script or no yconverge.cue + dep_count=$(printf '%s\n' "$deps" | grep -c . 2>/dev/null) || true # y-script-lint:disable=or-true # empty + if [ "$dep_count" -gt 1 ] 2>/dev/null; then + echo "=== Converge plan (context=$CONTEXT) ===" + echo "Steps ($dep_count):" + for d in $deps; do echo " $d"; done + echo "===" + export _YCONVERGE_RESOLVING=1 + for d in $deps; do + echo ">>> $d" + kubectl-yconverge $ctx -k "$d/" + done + exit 0 + fi +fi + # --- yconverge.cue lookup --- _find_yconverge_dir() { diff --git a/bin/y-cluster-converge-ystack b/bin/y-cluster-converge-ystack index 6d596fc..28f95aa 100755 --- a/bin/y-cluster-converge-ystack +++ b/bin/y-cluster-converge-ystack @@ -2,29 +2,35 @@ [ -z "$DEBUG" ] || set -x set -eo pipefail +[ "$1" = "help" ] && echo ' +Converge all ystack infrastructure on a k3s cluster. +Resolves dependencies from yconverge.cue imports automatically. + +Usage: y-cluster-converge-ystack --context= [--override-ip=IP] +' && exit 0 + YSTACK_HOME="$(cd "$(dirname "$0")/.." && pwd)" CONTEXT="" -EXCLUDE="" OVERRIDE_IP="" while [ $# -gt 0 ]; do case "$1" in --context=*) CONTEXT="${1#*=}"; shift ;; - --exclude=*) EXCLUDE="${1#*=}"; shift ;; --override-ip=*) OVERRIDE_IP="${1#*=}"; shift ;; *) echo "Unknown flag: $1" >&2; exit 1 ;; esac done -[ -z "$CONTEXT" ] && echo "Usage: y-cluster-converge-ystack --context= [--exclude=SUBSTRING] [--override-ip=IP]" && exit 1 +[ -z "$CONTEXT" ] && echo "Usage: y-cluster-converge-ystack --context= [--override-ip=IP]" && exit 1 -TAGS="-t context=$CONTEXT -t path=$PATH" -[ -n "$KUBECONFIG" ] && TAGS="$TAGS -t kubeconfig=$KUBECONFIG" +export OVERRIDE_IP -# Override-ip is applied as a gateway annotation after the gateway step -if [ -n "$OVERRIDE_IP" ]; then - TAGS="$TAGS -t overrideIP=$OVERRIDE_IP" -fi +cd "$YSTACK_HOME" -(cd "$YSTACK_HOME" && y-cue cmd converge $TAGS ./cue/converge-ystack/) +# Converge all leaf targets. Each resolves its own dependency chain. +# Shared dependencies are idempotent — re-applying is a no-op. +kubectl-yconverge --context="$CONTEXT" -k k3s/62-buildkit/ +kubectl-yconverge --context="$CONTEXT" -k k3s/50-monitoring/ +kubectl-yconverge --context="$CONTEXT" -k k3s/61-prod-registry/ +kubectl-yconverge --context="$CONTEXT" -k k3s/40-kafka/ diff --git a/bin/y-yconverge-deps b/bin/y-yconverge-deps new file mode 100755 index 0000000..294121c --- /dev/null +++ b/bin/y-yconverge-deps @@ -0,0 +1,63 @@ +#!/bin/bash +[ -z "$DEBUG" ] || set -x +set -eo pipefail + +[ "$1" = "help" ] && echo ' +Resolve the dependency tree for a yconverge.cue file. +Outputs kustomization paths in topological order (dependencies first). + +Usage: y-yconverge-deps +' && exit 0 + +YSTACK_HOME="${YSTACK_HOME:-$(cd "$(dirname "$0")/.." && pwd)}" +MODULE_PREFIX="yolean.se/ystack/" + +dir="${1%/}" +[ -z "$dir" ] && echo "Usage: y-yconverge-deps " >&2 && exit 1 + +_VISITED=$(mktemp /tmp/yconverge-deps.XXXXXX) + +_find_cue() { + local d="$1" + if [ -f "$d/yconverge.cue" ]; then + echo "$d" + return 0 + fi + if [ -f "$d/kustomization.yaml" ]; then + local resources count + resources=$(y-yq '.resources // [] | .[] | select(test("^[^h]") and test("^(http|github)") | not)' "$d/kustomization.yaml" 2>/dev/null) || true # y-script-lint:disable=or-true # no local resources + count=$(printf '%s\n' "$resources" | grep -c . 2>/dev/null) || true # y-script-lint:disable=or-true # empty + if [ "$count" = "1" ] && [ -f "$d/$resources/yconverge.cue" ]; then + echo "$d/$resources" + return 0 + fi + fi + return 1 +} + +_imports() { + grep "\"${MODULE_PREFIX}" "$1" 2>/dev/null \ + | grep -v '/cue/converge"' \ + | sed "s|.*\"${MODULE_PREFIX}\([^\":]*\).*|\1|" \ + || true # y-script-lint:disable=or-true # no deps is valid +} + +_resolve() { + local path="$1" + local cue_file dep cue_dir + + if grep -qx "$path" "$_VISITED" 2>/dev/null; then return 0; fi + + cue_dir=$(_find_cue "$path") || return 0 # y-script-lint:disable=or-true # no yconverge.cue + cue_file="$cue_dir/yconverge.cue" + + for dep in $(_imports "$cue_file"); do + _resolve "$dep" + done + + echo "$path" >> "$_VISITED" + echo "$path" +} + +_resolve "$dir" +rm -f "$_VISITED" diff --git a/cue/converge-ystack/converge_tool.cue b/cue/converge-ystack/converge_tool.cue deleted file mode 100644 index b563fc4..0000000 --- a/cue/converge-ystack/converge_tool.cue +++ /dev/null @@ -1,94 +0,0 @@ -package converge_ystack - -import ( - "list" - "strings" - "tool/cli" - "tool/exec" -) - -_context: string @tag(context) -_path: string @tag(path) -_kubeconfig: *"" | string @tag(kubeconfig) -_overrideIP: *"" | string @tag(overrideIP) - -_env: { - CONTEXT: _context - PATH: _path - OVERRIDE_IP: _overrideIP - KUBECONFIG: _kubeconfig -} - -// Build human-readable plan -_planLines: [for s in steps { - let _header = " \(s.path)" - let _checkLines = [for c in s.step.checks { - if c.kind == "wait" {" check: wait \(c.resource) \(c.for)"} - if c.kind == "rollout" {" check: rollout \(c.resource)"} - if c.kind == "exec" {" check: \(c.description)"} - }] - strings.Join(list.Concat([[_header], _checkLines]), "\n") -}] - -_plan: strings.Join(list.Concat([ - ["=== Converge plan (context=\(_context)) ==="], - ["Steps (\(len(steps))):"], - _planLines, - ["==="], -]), "\n") - -// Generate shell commands per step -_stepCmds: [for s in steps { - let _apply = "kubectl-yconverge --context=\(_context) --skip-checks -k \(s.path)/" - let _checkCmds = [for c in s.step.checks { - if c.kind == "wait" { - let _ns = { - if c.namespace != _|_ {"-n \(c.namespace) "} - if c.namespace == _|_ {""} - } - "echo ' check: wait \(c.resource)' && kubectl --context=\(_context) wait --for=\(c.for) --timeout=\(c.timeout) \(_ns)\(c.resource)" - } - if c.kind == "rollout" { - let _ns = { - if c.namespace != _|_ {"-n \(c.namespace) "} - if c.namespace == _|_ {""} - } - "echo ' check: rollout \(c.resource)' && kubectl --context=\(_context) rollout status --timeout=\(c.timeout) \(_ns)\(c.resource)" - } - if c.kind == "exec" {"echo ' check: \(c.description)' && { for _retry_i in $(seq 1 15); do " + c.command + " && break || sleep 2; done; }"} - }] - let _body = strings.Join(list.Concat([[_apply], _checkCmds]), "\n") - "echo '>>> \(s.path)'\n\(_body)" -}] - -_script: strings.Join(list.Concat([ - ["set -eo pipefail"], - _stepCmds, - ["echo '=== Converge complete ==='"], -]), "\n") - -// Write script to temp file so CUE error messages don't dump the entire script -command: converge: { - printPlan: cli.Print & { - text: _plan - } - - writeScript: exec.Run & { - $after: printPlan - cmd: ["sh", "-c", "F=$(mktemp /tmp/ystack-converge.XXXXXX.sh) && cat > $F && echo $F"] - stdin: _script - stdout: string - } - - run: exec.Run & { - $after: writeScript - cmd: [ - "bash", "-c", - "export CONTEXT=" + _context + " OVERRIDE_IP=" + _overrideIP + - " PATH=" + _path + - " KUBECONFIG=" + _kubeconfig + - "; F=" + strings.TrimSpace(writeScript.stdout) + - "; bash $F; E=$?; rm -f $F; exit $E", - ] - } -} diff --git a/cue/converge-ystack/steps.cue b/cue/converge-ystack/steps.cue deleted file mode 100644 index 4730163..0000000 --- a/cue/converge-ystack/steps.cue +++ /dev/null @@ -1,51 +0,0 @@ -package converge_ystack - -import ( - "yolean.se/ystack/k3s/00-namespace-ystack:namespace_ystack" - "yolean.se/ystack/k3s/01-namespace-blobs:namespace_blobs" - "yolean.se/ystack/k3s/02-namespace-kafka:namespace_kafka" - "yolean.se/ystack/k3s/03-namespace-monitoring:namespace_monitoring" - "yolean.se/ystack/k3s/09-y-kustomize-secrets-init:y_kustomize_secrets_init" - "yolean.se/ystack/k3s/10-gateway-api:gateway_api" - "yolean.se/ystack/k3s/11-monitoring-operator:monitoring_operator" - "yolean.se/ystack/k3s/20-gateway:gateway" - "yolean.se/ystack/k3s/29-y-kustomize:y_kustomize" - "yolean.se/ystack/k3s/30-blobs-ystack:blobs_ystack" - "yolean.se/ystack/k3s/30-blobs:blobs" - "yolean.se/ystack/k3s/30-blobs-minio-disabled:blobs_minio_disabled" - "yolean.se/ystack/k3s/40-kafka-ystack:kafka_ystack" - "yolean.se/ystack/k3s/40-kafka:kafka" - "yolean.se/ystack/k3s/50-monitoring:monitoring" - "yolean.se/ystack/k3s/60-builds-registry:builds_registry" - "yolean.se/ystack/k3s/61-prod-registry:prod_registry" - "yolean.se/ystack/k3s/62-buildkit:buildkit" -) - -import "yolean.se/ystack/cue/converge" - -_entry: { - path: string - step: converge.#Step -} - -// All steps in dependency order. -steps: [..._entry] & [ - {path: "k3s/00-namespace-ystack", step: namespace_ystack.step}, - {path: "k3s/01-namespace-blobs", step: namespace_blobs.step}, - {path: "k3s/02-namespace-kafka", step: namespace_kafka.step}, - {path: "k3s/03-namespace-monitoring", step: namespace_monitoring.step}, - {path: "k3s/09-y-kustomize-secrets-init", step: y_kustomize_secrets_init.step}, - {path: "k3s/10-gateway-api", step: gateway_api.step}, - {path: "k3s/11-monitoring-operator", step: monitoring_operator.step}, - {path: "k3s/20-gateway", step: gateway.step}, - {path: "k3s/29-y-kustomize", step: y_kustomize.step}, - {path: "k3s/30-blobs-ystack", step: blobs_ystack.step}, - {path: "k3s/30-blobs", step: blobs.step}, - {path: "k3s/30-blobs-minio-disabled", step: blobs_minio_disabled.step}, - {path: "k3s/40-kafka-ystack", step: kafka_ystack.step}, - {path: "k3s/40-kafka", step: kafka.step}, - {path: "k3s/50-monitoring", step: monitoring.step}, - {path: "k3s/60-builds-registry", step: builds_registry.step}, - {path: "k3s/61-prod-registry", step: prod_registry.step}, - {path: "k3s/62-buildkit", step: buildkit.step}, -] From 825cdba528d1f786a883f261f314a22edad63a43 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Sun, 5 Apr 2026 13:26:18 +0000 Subject: [PATCH 26/36] Move cue/ to yconverge/, update all import paths cue/converge/ -> yconverge/converge/ cue/itest/ -> yconverge/itest/ All imports updated from yolean.se/ystack/cue/converge to yolean.se/ystack/yconverge/converge. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/lint.yaml | 2 +- bin/y-yconverge-deps | 2 +- k3s/00-namespace-ystack/yconverge.cue | 2 +- k3s/01-namespace-blobs/yconverge.cue | 2 +- k3s/02-namespace-kafka/yconverge.cue | 2 +- k3s/03-namespace-monitoring/yconverge.cue | 2 +- k3s/09-y-kustomize-secrets-init/yconverge.cue | 2 +- k3s/10-gateway-api/yconverge.cue | 2 +- k3s/11-monitoring-operator/yconverge.cue | 2 +- k3s/20-gateway/yconverge.cue | 2 +- k3s/29-y-kustomize/yconverge.cue | 2 +- k3s/30-blobs-minio-disabled/yconverge.cue | 2 +- k3s/30-blobs-ystack/yconverge.cue | 2 +- k3s/30-blobs/yconverge.cue | 2 +- k3s/40-kafka-ystack/yconverge.cue | 2 +- k3s/40-kafka/yconverge.cue | 2 +- k3s/50-monitoring/yconverge.cue | 2 +- k3s/60-builds-registry/yconverge.cue | 2 +- k3s/61-prod-registry/yconverge.cue | 2 +- k3s/62-buildkit/yconverge.cue | 2 +- {cue => yconverge}/converge/schema.cue | 0 .../itest/example-configmap/configmap.yaml | 0 .../example-configmap/kustomization.yaml | 0 .../itest/example-configmap/yconverge.cue | 4 +-- .../itest/example-disabled/configmap.yaml | 0 .../itest/example-disabled/kustomization.yaml | 0 .../itest/example-disabled/yconverge.cue | 2 +- .../itest/example-indirect/kustomization.yaml | 0 .../example-namespace/kustomization.yaml | 0 .../itest/example-namespace/namespace.yaml | 0 .../itest/example-namespace/yconverge.cue | 2 +- .../itest/example-serverside/configmap.yaml | 0 .../example-serverside/kustomization.yaml | 0 .../example-with-dependency/configmap.yaml | 0 .../kustomization.yaml | 0 .../example-with-dependency/yconverge.cue | 4 +-- {cue => yconverge}/itest/test.sh | 28 +++++++++---------- 37 files changed, 40 insertions(+), 40 deletions(-) rename {cue => yconverge}/converge/schema.cue (100%) rename {cue => yconverge}/itest/example-configmap/configmap.yaml (100%) rename {cue => yconverge}/itest/example-configmap/kustomization.yaml (100%) rename {cue => yconverge}/itest/example-configmap/yconverge.cue (71%) rename {cue => yconverge}/itest/example-disabled/configmap.yaml (100%) rename {cue => yconverge}/itest/example-disabled/kustomization.yaml (100%) rename {cue => yconverge}/itest/example-disabled/yconverge.cue (78%) rename {cue => yconverge}/itest/example-indirect/kustomization.yaml (100%) rename {cue => yconverge}/itest/example-namespace/kustomization.yaml (100%) rename {cue => yconverge}/itest/example-namespace/namespace.yaml (100%) rename {cue => yconverge}/itest/example-namespace/yconverge.cue (79%) rename {cue => yconverge}/itest/example-serverside/configmap.yaml (100%) rename {cue => yconverge}/itest/example-serverside/kustomization.yaml (100%) rename {cue => yconverge}/itest/example-with-dependency/configmap.yaml (100%) rename {cue => yconverge}/itest/example-with-dependency/kustomization.yaml (100%) rename {cue => yconverge}/itest/example-with-dependency/yconverge.cue (72%) rename {cue => yconverge}/itest/test.sh (78%) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index c8f29a3..7876622 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -38,7 +38,7 @@ jobs: itest-main- path: ~/.cache/ystack - name: Integration tests (yconverge framework) - run: cue/itest/test.sh + run: yconverge/itest/test.sh env: YSTACK_HOME: ${{ github.workspace }} PATH: ${{ github.workspace }}/bin:/usr/local/bin:/usr/bin:/bin diff --git a/bin/y-yconverge-deps b/bin/y-yconverge-deps index 294121c..de530eb 100755 --- a/bin/y-yconverge-deps +++ b/bin/y-yconverge-deps @@ -37,7 +37,7 @@ _find_cue() { _imports() { grep "\"${MODULE_PREFIX}" "$1" 2>/dev/null \ - | grep -v '/cue/converge"' \ + | grep -v '/yconverge/cue"' \ | sed "s|.*\"${MODULE_PREFIX}\([^\":]*\).*|\1|" \ || true # y-script-lint:disable=or-true # no deps is valid } diff --git a/k3s/00-namespace-ystack/yconverge.cue b/k3s/00-namespace-ystack/yconverge.cue index d9d7562..f677cdf 100644 --- a/k3s/00-namespace-ystack/yconverge.cue +++ b/k3s/00-namespace-ystack/yconverge.cue @@ -1,6 +1,6 @@ package namespace_ystack -import "yolean.se/ystack/cue/converge" +import "yolean.se/ystack/yconverge/converge" step: converge.#Step & { checks: [] diff --git a/k3s/01-namespace-blobs/yconverge.cue b/k3s/01-namespace-blobs/yconverge.cue index d2af883..4c1ddc4 100644 --- a/k3s/01-namespace-blobs/yconverge.cue +++ b/k3s/01-namespace-blobs/yconverge.cue @@ -1,6 +1,6 @@ package namespace_blobs -import "yolean.se/ystack/cue/converge" +import "yolean.se/ystack/yconverge/converge" step: converge.#Step & { checks: [] diff --git a/k3s/02-namespace-kafka/yconverge.cue b/k3s/02-namespace-kafka/yconverge.cue index 7e4a090..0b13a30 100644 --- a/k3s/02-namespace-kafka/yconverge.cue +++ b/k3s/02-namespace-kafka/yconverge.cue @@ -1,6 +1,6 @@ package namespace_kafka -import "yolean.se/ystack/cue/converge" +import "yolean.se/ystack/yconverge/converge" step: converge.#Step & { checks: [] diff --git a/k3s/03-namespace-monitoring/yconverge.cue b/k3s/03-namespace-monitoring/yconverge.cue index dd04626..bf98010 100644 --- a/k3s/03-namespace-monitoring/yconverge.cue +++ b/k3s/03-namespace-monitoring/yconverge.cue @@ -1,6 +1,6 @@ package namespace_monitoring -import "yolean.se/ystack/cue/converge" +import "yolean.se/ystack/yconverge/converge" step: converge.#Step & { checks: [] diff --git a/k3s/09-y-kustomize-secrets-init/yconverge.cue b/k3s/09-y-kustomize-secrets-init/yconverge.cue index 0c98e9a..ce639da 100644 --- a/k3s/09-y-kustomize-secrets-init/yconverge.cue +++ b/k3s/09-y-kustomize-secrets-init/yconverge.cue @@ -1,7 +1,7 @@ package y_kustomize_secrets_init import ( - "yolean.se/ystack/cue/converge" + "yolean.se/ystack/yconverge/converge" "yolean.se/ystack/k3s/00-namespace-ystack:namespace_ystack" ) diff --git a/k3s/10-gateway-api/yconverge.cue b/k3s/10-gateway-api/yconverge.cue index c2bc7b7..8e57bee 100644 --- a/k3s/10-gateway-api/yconverge.cue +++ b/k3s/10-gateway-api/yconverge.cue @@ -1,7 +1,7 @@ package gateway_api import ( - "yolean.se/ystack/cue/converge" + "yolean.se/ystack/yconverge/converge" "yolean.se/ystack/k3s/00-namespace-ystack:namespace_ystack" ) diff --git a/k3s/11-monitoring-operator/yconverge.cue b/k3s/11-monitoring-operator/yconverge.cue index 4969c95..62dba43 100644 --- a/k3s/11-monitoring-operator/yconverge.cue +++ b/k3s/11-monitoring-operator/yconverge.cue @@ -1,7 +1,7 @@ package monitoring_operator import ( - "yolean.se/ystack/cue/converge" + "yolean.se/ystack/yconverge/converge" "yolean.se/ystack/k3s/03-namespace-monitoring:namespace_monitoring" ) diff --git a/k3s/20-gateway/yconverge.cue b/k3s/20-gateway/yconverge.cue index c052608..10505ef 100644 --- a/k3s/20-gateway/yconverge.cue +++ b/k3s/20-gateway/yconverge.cue @@ -1,7 +1,7 @@ package gateway import ( - "yolean.se/ystack/cue/converge" + "yolean.se/ystack/yconverge/converge" "yolean.se/ystack/k3s/10-gateway-api:gateway_api" ) diff --git a/k3s/29-y-kustomize/yconverge.cue b/k3s/29-y-kustomize/yconverge.cue index a0f7e19..72e4867 100644 --- a/k3s/29-y-kustomize/yconverge.cue +++ b/k3s/29-y-kustomize/yconverge.cue @@ -1,7 +1,7 @@ package y_kustomize import ( - "yolean.se/ystack/cue/converge" + "yolean.se/ystack/yconverge/converge" "yolean.se/ystack/k3s/09-y-kustomize-secrets-init:y_kustomize_secrets_init" "yolean.se/ystack/k3s/20-gateway:gateway" ) diff --git a/k3s/30-blobs-minio-disabled/yconverge.cue b/k3s/30-blobs-minio-disabled/yconverge.cue index 4a5505c..1a83ad3 100644 --- a/k3s/30-blobs-minio-disabled/yconverge.cue +++ b/k3s/30-blobs-minio-disabled/yconverge.cue @@ -1,6 +1,6 @@ package blobs_minio_disabled -import "yolean.se/ystack/cue/converge" +import "yolean.se/ystack/yconverge/converge" step: converge.#Step & { checks: [] diff --git a/k3s/30-blobs-ystack/yconverge.cue b/k3s/30-blobs-ystack/yconverge.cue index 3a78a2a..7b2ee31 100644 --- a/k3s/30-blobs-ystack/yconverge.cue +++ b/k3s/30-blobs-ystack/yconverge.cue @@ -1,7 +1,7 @@ package blobs_ystack import ( - "yolean.se/ystack/cue/converge" + "yolean.se/ystack/yconverge/converge" "yolean.se/ystack/k3s/01-namespace-blobs:namespace_blobs" "yolean.se/ystack/k3s/29-y-kustomize:y_kustomize" ) diff --git a/k3s/30-blobs/yconverge.cue b/k3s/30-blobs/yconverge.cue index 602efbb..6e29744 100644 --- a/k3s/30-blobs/yconverge.cue +++ b/k3s/30-blobs/yconverge.cue @@ -1,7 +1,7 @@ package blobs import ( - "yolean.se/ystack/cue/converge" + "yolean.se/ystack/yconverge/converge" "yolean.se/ystack/k3s/30-blobs-ystack:blobs_ystack" ) diff --git a/k3s/40-kafka-ystack/yconverge.cue b/k3s/40-kafka-ystack/yconverge.cue index 6b55438..c8e3462 100644 --- a/k3s/40-kafka-ystack/yconverge.cue +++ b/k3s/40-kafka-ystack/yconverge.cue @@ -1,7 +1,7 @@ package kafka_ystack import ( - "yolean.se/ystack/cue/converge" + "yolean.se/ystack/yconverge/converge" "yolean.se/ystack/k3s/02-namespace-kafka:namespace_kafka" "yolean.se/ystack/k3s/29-y-kustomize:y_kustomize" ) diff --git a/k3s/40-kafka/yconverge.cue b/k3s/40-kafka/yconverge.cue index e7473de..e70ba25 100644 --- a/k3s/40-kafka/yconverge.cue +++ b/k3s/40-kafka/yconverge.cue @@ -1,7 +1,7 @@ package kafka import ( - "yolean.se/ystack/cue/converge" + "yolean.se/ystack/yconverge/converge" "yolean.se/ystack/k3s/40-kafka-ystack:kafka_ystack" ) diff --git a/k3s/50-monitoring/yconverge.cue b/k3s/50-monitoring/yconverge.cue index defe858..8c8cc22 100644 --- a/k3s/50-monitoring/yconverge.cue +++ b/k3s/50-monitoring/yconverge.cue @@ -1,7 +1,7 @@ package monitoring import ( - "yolean.se/ystack/cue/converge" + "yolean.se/ystack/yconverge/converge" "yolean.se/ystack/k3s/11-monitoring-operator:monitoring_operator" ) diff --git a/k3s/60-builds-registry/yconverge.cue b/k3s/60-builds-registry/yconverge.cue index c2bb093..8dc3ea1 100644 --- a/k3s/60-builds-registry/yconverge.cue +++ b/k3s/60-builds-registry/yconverge.cue @@ -1,7 +1,7 @@ package builds_registry import ( - "yolean.se/ystack/cue/converge" + "yolean.se/ystack/yconverge/converge" "yolean.se/ystack/k3s/30-blobs:blobs" "yolean.se/ystack/k3s/40-kafka-ystack:kafka_ystack" "yolean.se/ystack/k3s/29-y-kustomize:y_kustomize" diff --git a/k3s/61-prod-registry/yconverge.cue b/k3s/61-prod-registry/yconverge.cue index 1de7d2b..798f8fa 100644 --- a/k3s/61-prod-registry/yconverge.cue +++ b/k3s/61-prod-registry/yconverge.cue @@ -1,7 +1,7 @@ package prod_registry import ( - "yolean.se/ystack/cue/converge" + "yolean.se/ystack/yconverge/converge" "yolean.se/ystack/k3s/00-namespace-ystack:namespace_ystack" ) diff --git a/k3s/62-buildkit/yconverge.cue b/k3s/62-buildkit/yconverge.cue index 4ca1ea2..bf29fc8 100644 --- a/k3s/62-buildkit/yconverge.cue +++ b/k3s/62-buildkit/yconverge.cue @@ -1,7 +1,7 @@ package buildkit import ( - "yolean.se/ystack/cue/converge" + "yolean.se/ystack/yconverge/converge" "yolean.se/ystack/k3s/60-builds-registry:builds_registry" ) diff --git a/cue/converge/schema.cue b/yconverge/converge/schema.cue similarity index 100% rename from cue/converge/schema.cue rename to yconverge/converge/schema.cue diff --git a/cue/itest/example-configmap/configmap.yaml b/yconverge/itest/example-configmap/configmap.yaml similarity index 100% rename from cue/itest/example-configmap/configmap.yaml rename to yconverge/itest/example-configmap/configmap.yaml diff --git a/cue/itest/example-configmap/kustomization.yaml b/yconverge/itest/example-configmap/kustomization.yaml similarity index 100% rename from cue/itest/example-configmap/kustomization.yaml rename to yconverge/itest/example-configmap/kustomization.yaml diff --git a/cue/itest/example-configmap/yconverge.cue b/yconverge/itest/example-configmap/yconverge.cue similarity index 71% rename from cue/itest/example-configmap/yconverge.cue rename to yconverge/itest/example-configmap/yconverge.cue index dea4f09..4f1557f 100644 --- a/cue/itest/example-configmap/yconverge.cue +++ b/yconverge/itest/example-configmap/yconverge.cue @@ -1,8 +1,8 @@ package example_configmap import ( - "yolean.se/ystack/cue/converge" - "yolean.se/ystack/cue/itest/example-namespace:example_namespace" + "yolean.se/ystack/yconverge/converge" + "yolean.se/ystack/yconverge/itest/example-namespace:example_namespace" ) _dep_ns: example_namespace.step diff --git a/cue/itest/example-disabled/configmap.yaml b/yconverge/itest/example-disabled/configmap.yaml similarity index 100% rename from cue/itest/example-disabled/configmap.yaml rename to yconverge/itest/example-disabled/configmap.yaml diff --git a/cue/itest/example-disabled/kustomization.yaml b/yconverge/itest/example-disabled/kustomization.yaml similarity index 100% rename from cue/itest/example-disabled/kustomization.yaml rename to yconverge/itest/example-disabled/kustomization.yaml diff --git a/cue/itest/example-disabled/yconverge.cue b/yconverge/itest/example-disabled/yconverge.cue similarity index 78% rename from cue/itest/example-disabled/yconverge.cue rename to yconverge/itest/example-disabled/yconverge.cue index 9bb5ba2..f1a1368 100644 --- a/cue/itest/example-disabled/yconverge.cue +++ b/yconverge/itest/example-disabled/yconverge.cue @@ -1,6 +1,6 @@ package example_disabled -import "yolean.se/ystack/cue/converge" +import "yolean.se/ystack/yconverge/converge" step: converge.#Step & { checks: [{ diff --git a/cue/itest/example-indirect/kustomization.yaml b/yconverge/itest/example-indirect/kustomization.yaml similarity index 100% rename from cue/itest/example-indirect/kustomization.yaml rename to yconverge/itest/example-indirect/kustomization.yaml diff --git a/cue/itest/example-namespace/kustomization.yaml b/yconverge/itest/example-namespace/kustomization.yaml similarity index 100% rename from cue/itest/example-namespace/kustomization.yaml rename to yconverge/itest/example-namespace/kustomization.yaml diff --git a/cue/itest/example-namespace/namespace.yaml b/yconverge/itest/example-namespace/namespace.yaml similarity index 100% rename from cue/itest/example-namespace/namespace.yaml rename to yconverge/itest/example-namespace/namespace.yaml diff --git a/cue/itest/example-namespace/yconverge.cue b/yconverge/itest/example-namespace/yconverge.cue similarity index 79% rename from cue/itest/example-namespace/yconverge.cue rename to yconverge/itest/example-namespace/yconverge.cue index 3aaa07b..72875e9 100644 --- a/cue/itest/example-namespace/yconverge.cue +++ b/yconverge/itest/example-namespace/yconverge.cue @@ -1,6 +1,6 @@ package example_namespace -import "yolean.se/ystack/cue/converge" +import "yolean.se/ystack/yconverge/converge" step: converge.#Step & { checks: [{ diff --git a/cue/itest/example-serverside/configmap.yaml b/yconverge/itest/example-serverside/configmap.yaml similarity index 100% rename from cue/itest/example-serverside/configmap.yaml rename to yconverge/itest/example-serverside/configmap.yaml diff --git a/cue/itest/example-serverside/kustomization.yaml b/yconverge/itest/example-serverside/kustomization.yaml similarity index 100% rename from cue/itest/example-serverside/kustomization.yaml rename to yconverge/itest/example-serverside/kustomization.yaml diff --git a/cue/itest/example-with-dependency/configmap.yaml b/yconverge/itest/example-with-dependency/configmap.yaml similarity index 100% rename from cue/itest/example-with-dependency/configmap.yaml rename to yconverge/itest/example-with-dependency/configmap.yaml diff --git a/cue/itest/example-with-dependency/kustomization.yaml b/yconverge/itest/example-with-dependency/kustomization.yaml similarity index 100% rename from cue/itest/example-with-dependency/kustomization.yaml rename to yconverge/itest/example-with-dependency/kustomization.yaml diff --git a/cue/itest/example-with-dependency/yconverge.cue b/yconverge/itest/example-with-dependency/yconverge.cue similarity index 72% rename from cue/itest/example-with-dependency/yconverge.cue rename to yconverge/itest/example-with-dependency/yconverge.cue index 59d2913..fa8684b 100644 --- a/cue/itest/example-with-dependency/yconverge.cue +++ b/yconverge/itest/example-with-dependency/yconverge.cue @@ -1,8 +1,8 @@ package example_with_dependency import ( - "yolean.se/ystack/cue/converge" - "yolean.se/ystack/cue/itest/example-configmap:example_configmap" + "yolean.se/ystack/yconverge/converge" + "yolean.se/ystack/yconverge/itest/example-configmap:example_configmap" ) _dep_config: example_configmap.step diff --git a/cue/itest/test.sh b/yconverge/itest/test.sh similarity index 78% rename from cue/itest/test.sh rename to yconverge/itest/test.sh index 12060b1..3b53c56 100755 --- a/cue/itest/test.sh +++ b/yconverge/itest/test.sh @@ -55,44 +55,44 @@ kubectl version --client=true >/dev/null 2>&1 echo "" echo "[cue itest] CUE schema validation" -y-cue vet ./cue/itest/example-namespace/ -y-cue vet ./cue/itest/example-configmap/ -y-cue vet ./cue/itest/example-with-dependency/ -y-cue vet ./cue/itest/example-disabled/ +y-cue vet ./yconverge/itest/example-namespace/ +y-cue vet ./yconverge/itest/example-configmap/ +y-cue vet ./yconverge/itest/example-with-dependency/ +y-cue vet ./yconverge/itest/example-disabled/ # --- apply with auto-checks --- echo "" echo "[cue itest] Apply with auto-checks (namespace)" -kubectl-yconverge --context="$CTX" -k cue/itest/example-namespace/ +kubectl-yconverge --context="$CTX" -k yconverge/itest/example-namespace/ echo "" echo "[cue itest] Apply with checks (configmap depends on namespace)" -kubectl-yconverge --context="$CTX" -k cue/itest/example-configmap/ +kubectl-yconverge --context="$CTX" -k yconverge/itest/example-configmap/ echo "" echo "[cue itest] Transitive dependency (depends on configmap which depends on namespace)" -kubectl-yconverge --context="$CTX" -k cue/itest/example-with-dependency/ +kubectl-yconverge --context="$CTX" -k yconverge/itest/example-with-dependency/ # --- indirection with namespace from referenced base --- echo "" echo "[cue itest] Indirection: yconverge.cue and namespace from referenced base" -kubectl-yconverge --context="$CTX" -k cue/itest/example-indirect/ +kubectl-yconverge --context="$CTX" -k yconverge/itest/example-indirect/ # --- idempotent re-converge --- echo "" echo "[cue itest] Idempotent re-apply" -kubectl-yconverge --context="$CTX" -k cue/itest/example-namespace/ -kubectl-yconverge --context="$CTX" -k cue/itest/example-configmap/ +kubectl-yconverge --context="$CTX" -k yconverge/itest/example-namespace/ +kubectl-yconverge --context="$CTX" -k yconverge/itest/example-configmap/ # --- converge-mode labels --- echo "" echo "[cue itest] Serverside-force label (other selectors match nothing)" -kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-serverside/ -kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-serverside/ +kubectl-yconverge --context="$CTX" --skip-checks -k yconverge/itest/example-serverside/ +kubectl-yconverge --context="$CTX" --skip-checks -k yconverge/itest/example-serverside/ _OUT=$(mktemp /tmp/yconverge-itest-out.XXXXXX) @@ -100,14 +100,14 @@ _OUT=$(mktemp /tmp/yconverge-itest-out.XXXXXX) echo "" echo "[cue itest] Indirection output must reference the base directory" -kubectl-yconverge --context="$CTX" -k cue/itest/example-indirect/ 2>&1 | tee "$_OUT" +kubectl-yconverge --context="$CTX" -k yconverge/itest/example-indirect/ 2>&1 | tee "$_OUT" grep -q "example-configmap/yconverge.cue" "$_OUT" # --- negative: --skip-checks suppresses check invocation --- echo "" echo "[cue itest] --skip-checks must not produce [yconverge] output" -kubectl-yconverge --context="$CTX" --skip-checks -k cue/itest/example-namespace/ 2>&1 | tee "$_OUT" +kubectl-yconverge --context="$CTX" --skip-checks -k yconverge/itest/example-namespace/ 2>&1 | tee "$_OUT" ! grep -q "\[yconverge\]" "$_OUT" # --- negative: broken yconverge.cue must fail --- From ba2ddc74e080362a800ae6fa241673a31f16f290 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Sun, 5 Apr 2026 13:28:54 +0000 Subject: [PATCH 27/36] Rename converge package to verify: verify.#Step yconverge/converge/ -> yconverge/verify/ package converge -> package verify Co-Authored-By: Claude Opus 4.6 (1M context) --- k3s/00-namespace-ystack/yconverge.cue | 4 ++-- k3s/01-namespace-blobs/yconverge.cue | 4 ++-- k3s/02-namespace-kafka/yconverge.cue | 4 ++-- k3s/03-namespace-monitoring/yconverge.cue | 4 ++-- k3s/09-y-kustomize-secrets-init/yconverge.cue | 4 ++-- k3s/10-gateway-api/yconverge.cue | 4 ++-- k3s/11-monitoring-operator/yconverge.cue | 4 ++-- k3s/20-gateway/yconverge.cue | 4 ++-- k3s/29-y-kustomize/yconverge.cue | 4 ++-- k3s/30-blobs-minio-disabled/yconverge.cue | 4 ++-- k3s/30-blobs-ystack/yconverge.cue | 4 ++-- k3s/30-blobs/yconverge.cue | 4 ++-- k3s/40-kafka-ystack/yconverge.cue | 4 ++-- k3s/40-kafka/yconverge.cue | 4 ++-- k3s/50-monitoring/yconverge.cue | 4 ++-- k3s/60-builds-registry/yconverge.cue | 4 ++-- k3s/61-prod-registry/yconverge.cue | 4 ++-- k3s/62-buildkit/yconverge.cue | 4 ++-- yconverge/itest/example-configmap/yconverge.cue | 4 ++-- yconverge/itest/example-disabled/yconverge.cue | 4 ++-- yconverge/itest/example-namespace/yconverge.cue | 4 ++-- yconverge/itest/example-with-dependency/yconverge.cue | 4 ++-- yconverge/{converge => verify}/schema.cue | 2 +- 23 files changed, 45 insertions(+), 45 deletions(-) rename yconverge/{converge => verify}/schema.cue (99%) diff --git a/k3s/00-namespace-ystack/yconverge.cue b/k3s/00-namespace-ystack/yconverge.cue index f677cdf..e78dc7d 100644 --- a/k3s/00-namespace-ystack/yconverge.cue +++ b/k3s/00-namespace-ystack/yconverge.cue @@ -1,7 +1,7 @@ package namespace_ystack -import "yolean.se/ystack/yconverge/converge" +import "yolean.se/ystack/yconverge/verify" -step: converge.#Step & { +step: verify.#Step & { checks: [] } diff --git a/k3s/01-namespace-blobs/yconverge.cue b/k3s/01-namespace-blobs/yconverge.cue index 4c1ddc4..2be32ca 100644 --- a/k3s/01-namespace-blobs/yconverge.cue +++ b/k3s/01-namespace-blobs/yconverge.cue @@ -1,7 +1,7 @@ package namespace_blobs -import "yolean.se/ystack/yconverge/converge" +import "yolean.se/ystack/yconverge/verify" -step: converge.#Step & { +step: verify.#Step & { checks: [] } diff --git a/k3s/02-namespace-kafka/yconverge.cue b/k3s/02-namespace-kafka/yconverge.cue index 0b13a30..5ee5cc2 100644 --- a/k3s/02-namespace-kafka/yconverge.cue +++ b/k3s/02-namespace-kafka/yconverge.cue @@ -1,7 +1,7 @@ package namespace_kafka -import "yolean.se/ystack/yconverge/converge" +import "yolean.se/ystack/yconverge/verify" -step: converge.#Step & { +step: verify.#Step & { checks: [] } diff --git a/k3s/03-namespace-monitoring/yconverge.cue b/k3s/03-namespace-monitoring/yconverge.cue index bf98010..dfe009c 100644 --- a/k3s/03-namespace-monitoring/yconverge.cue +++ b/k3s/03-namespace-monitoring/yconverge.cue @@ -1,7 +1,7 @@ package namespace_monitoring -import "yolean.se/ystack/yconverge/converge" +import "yolean.se/ystack/yconverge/verify" -step: converge.#Step & { +step: verify.#Step & { checks: [] } diff --git a/k3s/09-y-kustomize-secrets-init/yconverge.cue b/k3s/09-y-kustomize-secrets-init/yconverge.cue index ce639da..bb62908 100644 --- a/k3s/09-y-kustomize-secrets-init/yconverge.cue +++ b/k3s/09-y-kustomize-secrets-init/yconverge.cue @@ -1,12 +1,12 @@ package y_kustomize_secrets_init import ( - "yolean.se/ystack/yconverge/converge" + "yolean.se/ystack/yconverge/verify" "yolean.se/ystack/k3s/00-namespace-ystack:namespace_ystack" ) _dep_ns: namespace_ystack.step -step: converge.#Step & { +step: verify.#Step & { checks: [] } diff --git a/k3s/10-gateway-api/yconverge.cue b/k3s/10-gateway-api/yconverge.cue index 8e57bee..6c1daa6 100644 --- a/k3s/10-gateway-api/yconverge.cue +++ b/k3s/10-gateway-api/yconverge.cue @@ -1,13 +1,13 @@ package gateway_api import ( - "yolean.se/ystack/yconverge/converge" + "yolean.se/ystack/yconverge/verify" "yolean.se/ystack/k3s/00-namespace-ystack:namespace_ystack" ) _dep_ns: namespace_ystack.step -step: converge.#Step & { +step: verify.#Step & { checks: [{ kind: "exec" command: "for i in $(seq 1 30); do kubectl --context=$CONTEXT wait --for=condition=Established --timeout=2s crd/gateways.gateway.networking.k8s.io 2>/dev/null && break; sleep 2; done && kubectl --context=$CONTEXT wait --for=condition=Established --timeout=5s crd/gateways.gateway.networking.k8s.io" diff --git a/k3s/11-monitoring-operator/yconverge.cue b/k3s/11-monitoring-operator/yconverge.cue index 62dba43..5cd6a67 100644 --- a/k3s/11-monitoring-operator/yconverge.cue +++ b/k3s/11-monitoring-operator/yconverge.cue @@ -1,13 +1,13 @@ package monitoring_operator import ( - "yolean.se/ystack/yconverge/converge" + "yolean.se/ystack/yconverge/verify" "yolean.se/ystack/k3s/03-namespace-monitoring:namespace_monitoring" ) _dep_ns: namespace_monitoring.step -step: converge.#Step & { +step: verify.#Step & { checks: [{ kind: "rollout" resource: "deploy/prometheus-operator" diff --git a/k3s/20-gateway/yconverge.cue b/k3s/20-gateway/yconverge.cue index 10505ef..2f98541 100644 --- a/k3s/20-gateway/yconverge.cue +++ b/k3s/20-gateway/yconverge.cue @@ -1,13 +1,13 @@ package gateway import ( - "yolean.se/ystack/yconverge/converge" + "yolean.se/ystack/yconverge/verify" "yolean.se/ystack/k3s/10-gateway-api:gateway_api" ) _dep_crds: gateway_api.step -step: converge.#Step & { +step: verify.#Step & { checks: [ { kind: "exec" diff --git a/k3s/29-y-kustomize/yconverge.cue b/k3s/29-y-kustomize/yconverge.cue index 72e4867..f51f685 100644 --- a/k3s/29-y-kustomize/yconverge.cue +++ b/k3s/29-y-kustomize/yconverge.cue @@ -1,7 +1,7 @@ package y_kustomize import ( - "yolean.se/ystack/yconverge/converge" + "yolean.se/ystack/yconverge/verify" "yolean.se/ystack/k3s/09-y-kustomize-secrets-init:y_kustomize_secrets_init" "yolean.se/ystack/k3s/20-gateway:gateway" ) @@ -9,7 +9,7 @@ import ( _dep_secrets: y_kustomize_secrets_init.step _dep_gateway: gateway.step -step: converge.#Step & { +step: verify.#Step & { checks: [{ kind: "rollout" resource: "deploy/y-kustomize" diff --git a/k3s/30-blobs-minio-disabled/yconverge.cue b/k3s/30-blobs-minio-disabled/yconverge.cue index 1a83ad3..f8ba675 100644 --- a/k3s/30-blobs-minio-disabled/yconverge.cue +++ b/k3s/30-blobs-minio-disabled/yconverge.cue @@ -1,7 +1,7 @@ package blobs_minio_disabled -import "yolean.se/ystack/yconverge/converge" +import "yolean.se/ystack/yconverge/verify" -step: converge.#Step & { +step: verify.#Step & { checks: [] } diff --git a/k3s/30-blobs-ystack/yconverge.cue b/k3s/30-blobs-ystack/yconverge.cue index 7b2ee31..75bed63 100644 --- a/k3s/30-blobs-ystack/yconverge.cue +++ b/k3s/30-blobs-ystack/yconverge.cue @@ -1,7 +1,7 @@ package blobs_ystack import ( - "yolean.se/ystack/yconverge/converge" + "yolean.se/ystack/yconverge/verify" "yolean.se/ystack/k3s/01-namespace-blobs:namespace_blobs" "yolean.se/ystack/k3s/29-y-kustomize:y_kustomize" ) @@ -9,7 +9,7 @@ import ( _dep_ns: namespace_blobs.step _dep_kustomize: y_kustomize.step -step: converge.#Step & { +step: verify.#Step & { checks: [{ kind: "exec" command: "kubectl --context=$CONTEXT -n ystack rollout restart deploy/y-kustomize && kubectl --context=$CONTEXT -n ystack rollout status deploy/y-kustomize --timeout=60s" diff --git a/k3s/30-blobs/yconverge.cue b/k3s/30-blobs/yconverge.cue index 6e29744..fc31b65 100644 --- a/k3s/30-blobs/yconverge.cue +++ b/k3s/30-blobs/yconverge.cue @@ -1,13 +1,13 @@ package blobs import ( - "yolean.se/ystack/yconverge/converge" + "yolean.se/ystack/yconverge/verify" "yolean.se/ystack/k3s/30-blobs-ystack:blobs_ystack" ) _dep_ystack: blobs_ystack.step -step: converge.#Step & { +step: verify.#Step & { checks: [{ kind: "rollout" resource: "deploy/versitygw" diff --git a/k3s/40-kafka-ystack/yconverge.cue b/k3s/40-kafka-ystack/yconverge.cue index c8e3462..abefc9b 100644 --- a/k3s/40-kafka-ystack/yconverge.cue +++ b/k3s/40-kafka-ystack/yconverge.cue @@ -1,7 +1,7 @@ package kafka_ystack import ( - "yolean.se/ystack/yconverge/converge" + "yolean.se/ystack/yconverge/verify" "yolean.se/ystack/k3s/02-namespace-kafka:namespace_kafka" "yolean.se/ystack/k3s/29-y-kustomize:y_kustomize" ) @@ -9,7 +9,7 @@ import ( _dep_ns: namespace_kafka.step _dep_kustomize: y_kustomize.step -step: converge.#Step & { +step: verify.#Step & { checks: [ { kind: "exec" diff --git a/k3s/40-kafka/yconverge.cue b/k3s/40-kafka/yconverge.cue index e70ba25..bbf63a6 100644 --- a/k3s/40-kafka/yconverge.cue +++ b/k3s/40-kafka/yconverge.cue @@ -1,13 +1,13 @@ package kafka import ( - "yolean.se/ystack/yconverge/converge" + "yolean.se/ystack/yconverge/verify" "yolean.se/ystack/k3s/40-kafka-ystack:kafka_ystack" ) _dep_ystack: kafka_ystack.step -step: converge.#Step & { +step: verify.#Step & { checks: [ { kind: "rollout" diff --git a/k3s/50-monitoring/yconverge.cue b/k3s/50-monitoring/yconverge.cue index 8c8cc22..9b8a3a9 100644 --- a/k3s/50-monitoring/yconverge.cue +++ b/k3s/50-monitoring/yconverge.cue @@ -1,13 +1,13 @@ package monitoring import ( - "yolean.se/ystack/yconverge/converge" + "yolean.se/ystack/yconverge/verify" "yolean.se/ystack/k3s/11-monitoring-operator:monitoring_operator" ) _dep_operator: monitoring_operator.step -step: converge.#Step & { +step: verify.#Step & { checks: [{ kind: "rollout" resource: "deploy/kube-state-metrics" diff --git a/k3s/60-builds-registry/yconverge.cue b/k3s/60-builds-registry/yconverge.cue index 8dc3ea1..4b75a86 100644 --- a/k3s/60-builds-registry/yconverge.cue +++ b/k3s/60-builds-registry/yconverge.cue @@ -1,7 +1,7 @@ package builds_registry import ( - "yolean.se/ystack/yconverge/converge" + "yolean.se/ystack/yconverge/verify" "yolean.se/ystack/k3s/30-blobs:blobs" "yolean.se/ystack/k3s/40-kafka-ystack:kafka_ystack" "yolean.se/ystack/k3s/29-y-kustomize:y_kustomize" @@ -11,7 +11,7 @@ _dep_blobs: blobs.step _dep_kafka: kafka_ystack.step _dep_kustomize: y_kustomize.step -step: converge.#Step & { +step: verify.#Step & { checks: [ { kind: "rollout" diff --git a/k3s/61-prod-registry/yconverge.cue b/k3s/61-prod-registry/yconverge.cue index 798f8fa..5285b07 100644 --- a/k3s/61-prod-registry/yconverge.cue +++ b/k3s/61-prod-registry/yconverge.cue @@ -1,12 +1,12 @@ package prod_registry import ( - "yolean.se/ystack/yconverge/converge" + "yolean.se/ystack/yconverge/verify" "yolean.se/ystack/k3s/00-namespace-ystack:namespace_ystack" ) _dep_ns: namespace_ystack.step -step: converge.#Step & { +step: verify.#Step & { checks: [] } diff --git a/k3s/62-buildkit/yconverge.cue b/k3s/62-buildkit/yconverge.cue index bf29fc8..f870963 100644 --- a/k3s/62-buildkit/yconverge.cue +++ b/k3s/62-buildkit/yconverge.cue @@ -1,13 +1,13 @@ package buildkit import ( - "yolean.se/ystack/yconverge/converge" + "yolean.se/ystack/yconverge/verify" "yolean.se/ystack/k3s/60-builds-registry:builds_registry" ) _dep_registry: builds_registry.step -step: converge.#Step & { +step: verify.#Step & { checks: [{ kind: "exec" command: "kubectl --context=$CONTEXT -n ystack get statefulset buildkitd" diff --git a/yconverge/itest/example-configmap/yconverge.cue b/yconverge/itest/example-configmap/yconverge.cue index 4f1557f..be15540 100644 --- a/yconverge/itest/example-configmap/yconverge.cue +++ b/yconverge/itest/example-configmap/yconverge.cue @@ -1,13 +1,13 @@ package example_configmap import ( - "yolean.se/ystack/yconverge/converge" + "yolean.se/ystack/yconverge/verify" "yolean.se/ystack/yconverge/itest/example-namespace:example_namespace" ) _dep_ns: example_namespace.step -step: converge.#Step & { +step: verify.#Step & { checks: [{ kind: "exec" command: "kubectl --context=$CONTEXT -n itest get configmap itest-config" diff --git a/yconverge/itest/example-disabled/yconverge.cue b/yconverge/itest/example-disabled/yconverge.cue index f1a1368..8de2101 100644 --- a/yconverge/itest/example-disabled/yconverge.cue +++ b/yconverge/itest/example-disabled/yconverge.cue @@ -1,8 +1,8 @@ package example_disabled -import "yolean.se/ystack/yconverge/converge" +import "yolean.se/ystack/yconverge/verify" -step: converge.#Step & { +step: verify.#Step & { checks: [{ kind: "exec" command: "false" diff --git a/yconverge/itest/example-namespace/yconverge.cue b/yconverge/itest/example-namespace/yconverge.cue index 72875e9..cd04290 100644 --- a/yconverge/itest/example-namespace/yconverge.cue +++ b/yconverge/itest/example-namespace/yconverge.cue @@ -1,8 +1,8 @@ package example_namespace -import "yolean.se/ystack/yconverge/converge" +import "yolean.se/ystack/yconverge/verify" -step: converge.#Step & { +step: verify.#Step & { checks: [{ kind: "wait" resource: "ns/itest" diff --git a/yconverge/itest/example-with-dependency/yconverge.cue b/yconverge/itest/example-with-dependency/yconverge.cue index fa8684b..c31ead3 100644 --- a/yconverge/itest/example-with-dependency/yconverge.cue +++ b/yconverge/itest/example-with-dependency/yconverge.cue @@ -1,13 +1,13 @@ package example_with_dependency import ( - "yolean.se/ystack/yconverge/converge" + "yolean.se/ystack/yconverge/verify" "yolean.se/ystack/yconverge/itest/example-configmap:example_configmap" ) _dep_config: example_configmap.step -step: converge.#Step & { +step: verify.#Step & { checks: [{ kind: "exec" command: "kubectl --context=$CONTEXT -n itest get configmap itest-dependent" diff --git a/yconverge/converge/schema.cue b/yconverge/verify/schema.cue similarity index 99% rename from yconverge/converge/schema.cue rename to yconverge/verify/schema.cue index d7c3539..febdbb6 100644 --- a/yconverge/converge/schema.cue +++ b/yconverge/verify/schema.cue @@ -1,4 +1,4 @@ -package converge +package verify // A convergence step: apply a kustomize base, then verify. // The yconverge.cue file must be next to a kustomization.yaml. From fade80044622611f6ea12149e1a2a8c71519b3ff Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Sun, 5 Apr 2026 13:57:58 +0000 Subject: [PATCH 28/36] Add --keep flag to itest, reject unknown flags Co-Authored-By: Claude Opus 4.6 (1M context) --- yconverge/itest/test.sh | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/yconverge/itest/test.sh b/yconverge/itest/test.sh index 3b53c56..d99fdf4 100755 --- a/yconverge/itest/test.sh +++ b/yconverge/itest/test.sh @@ -6,15 +6,30 @@ set -eo pipefail Integration tests for the yconverge framework. Uses kwok (registry.k8s.io/kwok/cluster) as a lightweight test cluster. +Flags: + --keep keep the kwok cluster running after tests + Requires: docker, kubectl, y-cue, kubectl-yconverge ' && exit 0 +KEEP=false +while [ $# -gt 0 ]; do + case "$1" in + --keep) KEEP=true; shift ;; + *) echo "Unknown flag: $1" >&2; exit 1 ;; + esac +done + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" YSTACK_HOME="$(cd "$SCRIPT_DIR/../.." && pwd)" CONTAINER_NAME="yconverge-itest-$$" CTX="yconverge-itest" cleanup() { + if [ "$KEEP" = "true" ]; then + echo "[cue itest] KEEP=true, cluster kept: kubectl --context=$CTX get ns" + return + fi echo "[cue itest] Cleaning up ..." docker rm -f "$CONTAINER_NAME" 2>/dev/null || true # y-script-lint:disable=or-true # best-effort cleanup kubectl config delete-context "$CTX" 2>/dev/null || true # y-script-lint:disable=or-true # best-effort cleanup From f465252c9f67c439ed87a744b7dca6b59fdcc4bb Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Sun, 5 Apr 2026 14:01:15 +0000 Subject: [PATCH 29/36] Use ephemeral kubeconfig for itest, stable path with --keep --keep uses /tmp/ystack-yconverge-itest (stable, reusable). Without --keep uses a temp file deleted on cleanup. Prints KUBECONFIG path with --keep for manual inspection. Co-Authored-By: Claude Opus 4.6 (1M context) --- yconverge/itest/test.sh | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/yconverge/itest/test.sh b/yconverge/itest/test.sh index d99fdf4..494781f 100755 --- a/yconverge/itest/test.sh +++ b/yconverge/itest/test.sh @@ -22,17 +22,26 @@ done SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" YSTACK_HOME="$(cd "$SCRIPT_DIR/../.." && pwd)" -CONTAINER_NAME="yconverge-itest-$$" CTX="yconverge-itest" +if [ "$KEEP" = "true" ]; then + CONTAINER_NAME="yconverge-itest" + ITEST_KUBECONFIG="/tmp/ystack-yconverge-itest" +else + CONTAINER_NAME="yconverge-itest-$$" + ITEST_KUBECONFIG=$(mktemp /tmp/ystack-yconverge-itest.XXXXXX) +fi +export KUBECONFIG="$ITEST_KUBECONFIG" + cleanup() { if [ "$KEEP" = "true" ]; then - echo "[cue itest] KEEP=true, cluster kept: kubectl --context=$CTX get ns" + echo "[cue itest] KEEP=true, cluster kept:" + echo " KUBECONFIG=$ITEST_KUBECONFIG kubectl --context=$CTX get ns" return fi echo "[cue itest] Cleaning up ..." docker rm -f "$CONTAINER_NAME" 2>/dev/null || true # y-script-lint:disable=or-true # best-effort cleanup - kubectl config delete-context "$CTX" 2>/dev/null || true # y-script-lint:disable=or-true # best-effort cleanup + rm -f "$ITEST_KUBECONFIG" } trap cleanup EXIT From 46b38c1e35eb5b69cdb92f212051bda539459e33 Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Sun, 5 Apr 2026 14:21:33 +0000 Subject: [PATCH 30/36] Fix kubeconfig for kubie compatibility Set current-context, create a named user entry, and reference it from the context. Fixes kubie which requires non-null users list and a current-context. Co-Authored-By: Claude Opus 4.6 (1M context) --- yconverge/itest/test.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/yconverge/itest/test.sh b/yconverge/itest/test.sh index 494781f..7bcc1f2 100755 --- a/yconverge/itest/test.sh +++ b/yconverge/itest/test.sh @@ -62,6 +62,9 @@ done kubectl config set-cluster "$CTX" --server="http://127.0.0.1:$PORT" >/dev/null kubectl config set-context "$CTX" --cluster="$CTX" >/dev/null +kubectl config set-credentials "$CTX" >/dev/null +kubectl config set-context "$CTX" --user="$CTX" >/dev/null +kubectl config use-context "$CTX" >/dev/null kubectl --context="$CTX" get ns default >/dev/null 2>&1 \ && echo "[cue itest] kwok cluster ready at port $PORT" \ || { echo "[cue itest] FATAL: kwok cluster not reachable"; exit 1; } From 02356da11ecb364e842db9b3f0f591b3370f114b Mon Sep 17 00:00:00 2001 From: Yolean k8s-qa Date: Sun, 5 Apr 2026 14:28:26 +0000 Subject: [PATCH 31/36] Add --teardown to itest for cleaning up kept clusters Co-Authored-By: Claude Opus 4.6 (1M context) --- yconverge/itest/test.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/yconverge/itest/test.sh b/yconverge/itest/test.sh index 7bcc1f2..53a61a6 100755 --- a/yconverge/itest/test.sh +++ b/yconverge/itest/test.sh @@ -7,19 +7,30 @@ Integration tests for the yconverge framework. Uses kwok (registry.k8s.io/kwok/cluster) as a lightweight test cluster. Flags: - --keep keep the kwok cluster running after tests + --keep keep the kwok cluster running after tests + --teardown remove a kept cluster and exit Requires: docker, kubectl, y-cue, kubectl-yconverge ' && exit 0 KEEP=false +TEARDOWN=false while [ $# -gt 0 ]; do case "$1" in --keep) KEEP=true; shift ;; + --teardown) TEARDOWN=true; shift ;; *) echo "Unknown flag: $1" >&2; exit 1 ;; esac done +if [ "$TEARDOWN" = "true" ]; then + echo "[cue itest] Tearing down kept cluster ..." + docker rm -f yconverge-itest 2>/dev/null || true # y-script-lint:disable=or-true # may not exist + rm -f /tmp/ystack-yconverge-itest + echo "[cue itest] Done" + exit 0 +fi + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" YSTACK_HOME="$(cd "$SCRIPT_DIR/../.." && pwd)" CTX="yconverge-itest" From c5f42498c55e719a22459d0072bbf1765bbe5719 Mon Sep 17 00:00:00 2001 From: Staffan Olsson Date: Wed, 8 Apr 2026 06:54:28 +0200 Subject: [PATCH 32/36] esbuild 0.25.11->0.28.0 --- bin/y-bin.runner.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bin/y-bin.runner.yaml b/bin/y-bin.runner.yaml index 093c626..2d377fc 100755 --- a/bin/y-bin.runner.yaml +++ b/bin/y-bin.runner.yaml @@ -110,14 +110,14 @@ crane: path: crane esbuild: - version: 0.25.11 + version: 0.28.0 templates: download: https://registry.npmjs.org/@esbuild/${os}-${xarm}64/-/${os}-${xarm}64-${version}.tgz sha256: - darwin_amd64: 6c685589690123a0a2b6424f82cfe21973e81923c0c61246bc48c69911f24dba - darwin_arm64: 42ea27b04af4aaf177f5a6fa5cb6dda85d21ecd2fa45adb865cc3430eb985e2e - linux_amd64: e9e893cfe6a327b97b8d749a2d477d7379ef87eb576b88e03e91b7d07ec935e0 - linux_arm64: 5bb8193032b55494841f27aa5bec41e6e1c717d6d90ae5651a4bd9ac01ebc930 + darwin_amd64: 58716de460d4e258a56191c1bb47c7341c1ed61d8d4d92b8991c85907e117667 + darwin_arm64: 6ef1261e245caed64e4d5195572e3468f18a5df4b65df5e14f9ae10408ad5502 + linux_amd64: e94bf1c7f44197b22cf6a787578eca9af805aa9624104488252de2a765c6a4f0 + linux_arm64: 9fdc4e2d6fac8e55b7ee3a69617d1fd4856d19eab72f8a150c5545c9f666973a archive: tool: tar path: package/bin/esbuild From 63e01fea63773c20d9e85d20a96a7f1f49341dd7 Mon Sep 17 00:00:00 2001 From: Staffan Olsson Date: Thu, 9 Apr 2026 16:41:17 +0200 Subject: [PATCH 33/36] see https://github.com/solsson/turbo/pull/1 --- bin/y-bin.runner.yaml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/bin/y-bin.runner.yaml b/bin/y-bin.runner.yaml index 2d377fc..af7d4b5 100755 --- a/bin/y-bin.runner.yaml +++ b/bin/y-bin.runner.yaml @@ -123,17 +123,14 @@ esbuild: path: package/bin/esbuild turbo: - version: 2.8.17 + version: 2.9.5-hashdepends.1 templates: - download: https://registry.npmjs.org/turbo-${os}-${arm}64/-/turbo-${os}-${arm}64-${version}.tgz + download: https://github.com/solsson/turbo/releases/download/v${version}/turbo-${os}-${arch} sha256: - darwin_amd64: c1653788f3e82aac33c38a2cf0be8bb3c94069f50bf742826d17ab0fbcabd430 - darwin_arm64: 7bbcb3a0218702bfd43916828abcd42a892777f751c2c7b40a02f6730cf789ac - linux_amd64: dcb1bac57f5745503defe968dc23780b2ad986615c2a4c9a44439061eb10753a - linux_arm64: 8b20bfcd34adfbe8fb670653571b87947c882ee850c56969ebf6cd7d63a8192b - archive: - tool: tar - path: turbo-${os}-${xarm}64/bin/turbo + darwin_amd64: 45ca441c683ec728d35056406de588b3d40dfbee0d2c6f2e1b5cf36c9b826843 + darwin_arm64: c866d3f6ac4dc89233775cab233bdf886c0bc8f027cdeba96a897b0f90c7a5f8 + linux_amd64: ac3b3ea6db7bf1b59ae7ac0024a0af649fc5543fac70aae2e4785ed8a4f0a27b + linux_arm64: c5f746d9a1a1628900ac464bb4af989c60a0e888898f68474ee58548fce4b690 yarn: version: 0.2.1 From f95811fdd4c3d1a7dce2412c8976370f7911a9e6 Mon Sep 17 00:00:00 2001 From: Staffan Olsson Date: Sun, 5 Apr 2026 16:07:41 +0200 Subject: [PATCH 34/36] yconverge: basic prod/qa reuse example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit yconverge: verify exact criteria before swallowing errors _apply_if_any tolerated any *"not found"* substring, which silently masked real apply failures like `namespaces "X" not found`. Replaced with _kubectl_tolerant(allowed_patterns, kubectl args) that tolerates only an explicit |-separated list. Each call site names the exact expected errors: - create --save-config: AlreadyExists, no objects passed to create - apply variants: no objects passed to apply - delete --selector: strict (kubectl exits 0 on empty match) Also stripped blanket 2>/dev/null masks from y-yq calls, kubectl config view, and y-yconverge-deps invocation. Restructured _find_cue and _find_yconverge_dir so "no cue file" is empty output + exit 0 while real parse errors propagate via set -e. itest: cover cluster-prod/db, create namespace in test driver Adds a cluster-prod/db step to the itest, creating the db namespace up front since bases intentionally don't carry a Namespace (keeps delete -k reversible). Also tightens docker rm cleanup to verify "No such container" before swallowing. db-service: add ports and clusterIP so kustomize renders a valid headless Service. yconverge: label internal kubectl output so users can attribute it Every internal step (create, delete replace-mode, 3 apply variants) now runs through _kubectl_step which prefixes each output line with " [yconverge] : " — matching the existing "[yconverge] found" and "[yconverge] check:" convention. Expected "nothing to do" stdout (e.g. delete's "No resources found") is silenced, so a clean re-apply only shows lines the user cares about. Renames _kubectl_tolerant → _kubectl_step and adds a third arg for |-separated success-stdout substrings to suppress. itest: --skip-checks negative test now greps for "[yconverge] check:" and "[yconverge] found" specifically, since plain apply output also carries the "[yconverge]" prefix now. yconverge: pass meaningful kubectl output through raw Drops the " [yconverge] : " prefix on internal kubectl step output — users already recognize kubectl's own format. Expected "nothing to do" outputs (delete's "No resources found") are still silenced via the _empty_ok arg, so a clean re-apply shows only the raw lines the user cares about. The label arg is dropped. Reverts the --skip-checks negative test back to grepping for any "[yconverge]" since kubectl step output no longer carries the prefix. --- bin/kubectl-yconverge | 94 ++++++++++++------- bin/y-yconverge-deps | 19 ++-- .../itest/cluster-prod/db/kustomization.yaml | 8 ++ .../itest/cluster-qa/db/kustomization.yaml | 8 ++ .../itest/example-db/base/db-service.yaml | 9 ++ .../itest/example-db/base/db-statefulset.yaml | 17 ++++ .../itest/example-db/base/kustomization.yaml | 9 ++ .../example-db/distributed/kustomization.yaml | 12 +++ .../example-db/namespace/db-namespace.yaml | 4 + .../example-db/namespace/kustomization.yaml | 6 ++ .../example-db/single/kustomization.yaml | 8 ++ .../itest/example-db/single/yconverge.cue | 12 +++ yconverge/itest/test.sh | 24 ++++- 13 files changed, 183 insertions(+), 47 deletions(-) create mode 100644 yconverge/itest/cluster-prod/db/kustomization.yaml create mode 100644 yconverge/itest/cluster-qa/db/kustomization.yaml create mode 100644 yconverge/itest/example-db/base/db-service.yaml create mode 100644 yconverge/itest/example-db/base/db-statefulset.yaml create mode 100644 yconverge/itest/example-db/base/kustomization.yaml create mode 100644 yconverge/itest/example-db/distributed/kustomization.yaml create mode 100644 yconverge/itest/example-db/namespace/db-namespace.yaml create mode 100644 yconverge/itest/example-db/namespace/kustomization.yaml create mode 100644 yconverge/itest/example-db/single/kustomization.yaml create mode 100644 yconverge/itest/example-db/single/yconverge.cue diff --git a/bin/kubectl-yconverge b/bin/kubectl-yconverge index 5e5285c..48223e5 100755 --- a/bin/kubectl-yconverge +++ b/bin/kubectl-yconverge @@ -72,8 +72,8 @@ export CONTEXT # Inner calls (during resolution) skip this and go straight to apply + checks. if [ -z "$_YCONVERGE_RESOLVING" ] && [ -n "$KUSTOMIZE_DIR" ] && [ "$SKIP_CHECKS" = "false" ] && [ "$DIFF" = "false" ]; then - deps=$(y-yconverge-deps "$KUSTOMIZE_DIR" 2>/dev/null) || true # y-script-lint:disable=or-true # no deps script or no yconverge.cue - dep_count=$(printf '%s\n' "$deps" | grep -c . 2>/dev/null) || true # y-script-lint:disable=or-true # empty + deps=$(y-yconverge-deps "$KUSTOMIZE_DIR") + dep_count=$(printf '%s\n' "$deps" | grep -c . 2>/dev/null) || true # y-script-lint:disable=or-true # grep -c . exit 1 = zero matches if [ "$dep_count" -gt 1 ] 2>/dev/null; then echo "=== Converge plan (context=$CONTEXT) ===" echo "Steps ($dep_count):" @@ -96,20 +96,18 @@ _find_yconverge_dir() { echo "$dir" return 0 fi - if [ -f "$dir/kustomization.yaml" ]; then - resources=$(y-yq '.resources // [] | .[] | select(test("^[^h]") and test("^(http|github)") | not)' "$dir/kustomization.yaml" 2>/dev/null) - count=$(printf '%s\n' "$resources" | grep -c . 2>/dev/null) - if [ "$count" = "1" ] && [ -d "$dir/$resources" ] && [ -f "$dir/$resources/yconverge.cue" ]; then - echo "$dir/$resources" - return 0 - fi + [ -f "$dir/kustomization.yaml" ] || return 0 + resources=$(y-yq '.resources // [] | .[] | select(test("^[^h]") and test("^(http|github)") | not)' "$dir/kustomization.yaml") + count=$(printf '%s\n' "$resources" | grep -c . 2>/dev/null) || true # y-script-lint:disable=or-true # grep -c . exit 1 = zero matches + if [ "$count" = "1" ] && [ -d "$dir/$resources" ] && [ -f "$dir/$resources/yconverge.cue" ]; then + echo "$dir/$resources" fi - return 1 + return 0 } yconverge_dir="" if [ -n "$KUSTOMIZE_DIR" ] && [ "$SKIP_CHECKS" = "false" ] && [ "$DRY_RUN" = "false" ] && [ "$DIFF" = "false" ]; then - yconverge_dir=$(_find_yconverge_dir "$KUSTOMIZE_DIR") || true # y-script-lint:disable=or-true # no yconverge.cue is ok + yconverge_dir=$(_find_yconverge_dir "$KUSTOMIZE_DIR") fi if [ -n "$yconverge_dir" ]; then @@ -122,8 +120,9 @@ fi # --- resolve namespace guess --- # Priority: 1. -n CLI arg -# 2. referenced base namespace (when indirection found yconverge.cue) -# 3. kustomization namespace: +# 2. outer kustomization namespace: (the rendered namespace kustomize uses) +# 3. referenced base namespace (fallback when indirection found yconverge.cue +# and the outer kustomization did not set its own namespace) # 4. context default NS_GUESS="" _prev="" @@ -134,16 +133,16 @@ for arg in "$@"; do fi _prev="$arg" done +if [ -z "$NS_GUESS" ] && [ -n "$KUSTOMIZE_DIR" ] && [ -f "$KUSTOMIZE_DIR/kustomization.yaml" ]; then + NS_GUESS=$(y-yq '.namespace // ""' "$KUSTOMIZE_DIR/kustomization.yaml") +fi if [ -z "$NS_GUESS" ] && [ -n "$yconverge_dir" ] && [ -n "$KUSTOMIZE_DIR" ] && [ "$yconverge_dir" != "$KUSTOMIZE_DIR" ] && [ "$yconverge_dir" != "./$KUSTOMIZE_DIR" ]; then _ref_kust="$yconverge_dir/kustomization.yaml" [ ! -f "$_ref_kust" ] && _ref_kust="$yconverge_dir/kustomization.yml" - [ -f "$_ref_kust" ] && NS_GUESS=$(y-yq '.namespace // ""' "$_ref_kust" 2>/dev/null) -fi -if [ -z "$NS_GUESS" ] && [ -n "$KUSTOMIZE_DIR" ] && [ -f "$KUSTOMIZE_DIR/kustomization.yaml" ]; then - NS_GUESS=$(y-yq '.namespace // ""' "$KUSTOMIZE_DIR/kustomization.yaml" 2>/dev/null) + [ -f "$_ref_kust" ] && NS_GUESS=$(y-yq '.namespace // ""' "$_ref_kust") fi if [ -z "$NS_GUESS" ]; then - NS_GUESS=$(kubectl config view --minify --context="$CONTEXT" -o jsonpath='{.contexts[0].context.namespace}' 2>/dev/null) || true # y-script-lint:disable=or-true # context may not have default namespace + NS_GUESS=$(kubectl config view --minify --context="$CONTEXT" -o jsonpath='{.contexts[0].context.namespace}') fi [ -z "$NS_GUESS" ] && NS_GUESS="default" @@ -157,25 +156,50 @@ fi DRY_RUN_FLAG="" [ "$DRY_RUN" = "true" ] && DRY_RUN_FLAG="--dry-run=server" -kubectl $ctx create --save-config $DRY_RUN_FLAG --selector=yolean.se/converge-mode=create "$@" 2>/dev/null || true # y-script-lint:disable=or-true # idempotent -if [ "$DRY_RUN" = "false" ]; then - kubectl $ctx delete --selector=yolean.se/converge-mode=replace "$@" 2>/dev/null || true # y-script-lint:disable=or-true # ignore not-found -fi - -_apply_if_any() { - output=$(kubectl "$@" 2>&1) || { - case "$output" in - *"no objects passed to apply"*) return 0 ;; - *"not found"*) return 0 ;; - *) printf '%s\n' "$output" >&2; return 1 ;; - esac +# Run one internal kubectl step, passing meaningful output through raw. +# $1 |-separated error substrings to tolerate silently (exit nonzero but expected) +# $2 |-separated stdout substrings that mean "nothing to do" (exit zero but uninteresting) +# $3... kubectl args +# Any other failure is fatal and shown raw on stderr. Any other success output is passed through. +_kubectl_step() { + _err_ok="$1" + _empty_ok="$2" + shift 2 + _out=$(kubectl "$@" 2>&1) || { + _old_ifs="$IFS"; IFS='|' + for _pat in $_err_ok; do + case "$_out" in *"$_pat"*) IFS="$_old_ifs"; return 0 ;; esac + done + IFS="$_old_ifs" + printf '%s\n' "$_out" >&2 + return 1 } - [ -n "$output" ] && printf '%s\n' "$output" + [ -z "$_out" ] && return 0 + _old_ifs="$IFS"; IFS='|' + for _pat in $_empty_ok; do + case "$_out" in *"$_pat"*) IFS="$_old_ifs"; return 0 ;; esac + done + IFS="$_old_ifs" + printf '%s\n' "$_out" } -_apply_if_any $ctx apply --server-side --force-conflicts $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside-force "$@" -_apply_if_any $ctx apply --server-side $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside "$@" -_apply_if_any $ctx apply $DRY_RUN_FLAG --selector='yolean.se/converge-mode!=create,yolean.se/converge-mode!=serverside,yolean.se/converge-mode!=serverside-force' "$@" +# create --save-config for converge-mode=create resources: AlreadyExists on re-run +# is the whole point of "idempotent"; no-match on selector is also expected. +_kubectl_step 'AlreadyExists|no objects passed to create' '' \ + $ctx create --save-config $DRY_RUN_FLAG --selector=yolean.se/converge-mode=create "$@" + +if [ "$DRY_RUN" = "false" ]; then + # delete with --selector and no matches exits 0 with "No resources found" — silent. + _kubectl_step '' 'No resources found' \ + $ctx delete --selector=yolean.se/converge-mode=replace "$@" +fi + +_kubectl_step 'no objects passed to apply' '' \ + $ctx apply --server-side --force-conflicts $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside-force "$@" +_kubectl_step 'no objects passed to apply' '' \ + $ctx apply --server-side $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside "$@" +_kubectl_step 'no objects passed to apply' '' \ + $ctx apply $DRY_RUN_FLAG --selector='yolean.se/converge-mode!=create,yolean.se/converge-mode!=serverside,yolean.se/converge-mode!=serverside-force' "$@" # --- yconverge.cue: post-apply checks --- @@ -184,7 +208,7 @@ if [ -n "$yconverge_dir" ]; then checks_json="$1" label="$2" [ -z "$checks_json" ] || [ "$checks_json" = "[]" ] && return 0 - count=$(echo "$checks_json" | y-yq '. | length' - 2>/dev/null) + count=$(echo "$checks_json" | y-yq '. | length' -) [ "$count" = "0" ] && return 0 i=0 while [ "$i" -lt "$count" ]; do diff --git a/bin/y-yconverge-deps b/bin/y-yconverge-deps index de530eb..606879b 100755 --- a/bin/y-yconverge-deps +++ b/bin/y-yconverge-deps @@ -23,16 +23,14 @@ _find_cue() { echo "$d" return 0 fi - if [ -f "$d/kustomization.yaml" ]; then - local resources count - resources=$(y-yq '.resources // [] | .[] | select(test("^[^h]") and test("^(http|github)") | not)' "$d/kustomization.yaml" 2>/dev/null) || true # y-script-lint:disable=or-true # no local resources - count=$(printf '%s\n' "$resources" | grep -c . 2>/dev/null) || true # y-script-lint:disable=or-true # empty - if [ "$count" = "1" ] && [ -f "$d/$resources/yconverge.cue" ]; then - echo "$d/$resources" - return 0 - fi + [ -f "$d/kustomization.yaml" ] || return 0 + local resources count + resources=$(y-yq '.resources // [] | .[] | select(test("^[^h]") and test("^(http|github)") | not)' "$d/kustomization.yaml") + count=$(printf '%s\n' "$resources" | grep -c . 2>/dev/null) || true # y-script-lint:disable=or-true # grep -c . exit 1 = zero matches + if [ "$count" = "1" ] && [ -f "$d/$resources/yconverge.cue" ]; then + echo "$d/$resources" fi - return 1 + return 0 } _imports() { @@ -48,7 +46,8 @@ _resolve() { if grep -qx "$path" "$_VISITED" 2>/dev/null; then return 0; fi - cue_dir=$(_find_cue "$path") || return 0 # y-script-lint:disable=or-true # no yconverge.cue + cue_dir=$(_find_cue "$path") + [ -z "$cue_dir" ] && return 0 cue_file="$cue_dir/yconverge.cue" for dep in $(_imports "$cue_file"); do diff --git a/yconverge/itest/cluster-prod/db/kustomization.yaml b/yconverge/itest/cluster-prod/db/kustomization.yaml new file mode 100644 index 0000000..064b7fa --- /dev/null +++ b/yconverge/itest/cluster-prod/db/kustomization.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://json.schemastore.org/kustomization.json +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: db + +resources: +- ../../example-db/distributed diff --git a/yconverge/itest/cluster-qa/db/kustomization.yaml b/yconverge/itest/cluster-qa/db/kustomization.yaml new file mode 100644 index 0000000..e7e809f --- /dev/null +++ b/yconverge/itest/cluster-qa/db/kustomization.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://json.schemastore.org/kustomization.json +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: db + +resources: +- ../../example-db/single diff --git a/yconverge/itest/example-db/base/db-service.yaml b/yconverge/itest/example-db/base/db-service.yaml new file mode 100644 index 0000000..a1b08a4 --- /dev/null +++ b/yconverge/itest/example-db/base/db-service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +metadata: + name: db +spec: + selector: + app: database + ports: [] + clusterIP: None diff --git a/yconverge/itest/example-db/base/db-statefulset.yaml b/yconverge/itest/example-db/base/db-statefulset.yaml new file mode 100644 index 0000000..13910d8 --- /dev/null +++ b/yconverge/itest/example-db/base/db-statefulset.yaml @@ -0,0 +1,17 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: database +spec: + selector: + matchLabels: + app: database + serviceName: "db" + template: + metadata: + labels: + app: database + spec: + containers: + - name: server + image: ghcr.io/yolean/static-web-server:2.41.0@sha256:34bb160fd62d2145dabd0598f36352653ec58cf80a8d58c8cd2617097d34564d diff --git a/yconverge/itest/example-db/base/kustomization.yaml b/yconverge/itest/example-db/base/kustomization.yaml new file mode 100644 index 0000000..62864bc --- /dev/null +++ b/yconverge/itest/example-db/base/kustomization.yaml @@ -0,0 +1,9 @@ +# yaml-language-server: $schema=https://json.schemastore.org/kustomization.json +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: ONLY_apply_through_cluster_variant + +resources: +- db-service.yaml +- db-statefulset.yaml diff --git a/yconverge/itest/example-db/distributed/kustomization.yaml b/yconverge/itest/example-db/distributed/kustomization.yaml new file mode 100644 index 0000000..0a06bfe --- /dev/null +++ b/yconverge/itest/example-db/distributed/kustomization.yaml @@ -0,0 +1,12 @@ +# yaml-language-server: $schema=https://json.schemastore.org/kustomization.json +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: ONLY_apply_through_cluster_variant + +resources: +- ../base + +replicas: +- name: database + count: 3 diff --git a/yconverge/itest/example-db/namespace/db-namespace.yaml b/yconverge/itest/example-db/namespace/db-namespace.yaml new file mode 100644 index 0000000..bab604e --- /dev/null +++ b/yconverge/itest/example-db/namespace/db-namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: db diff --git a/yconverge/itest/example-db/namespace/kustomization.yaml b/yconverge/itest/example-db/namespace/kustomization.yaml new file mode 100644 index 0000000..e810266 --- /dev/null +++ b/yconverge/itest/example-db/namespace/kustomization.yaml @@ -0,0 +1,6 @@ +# yaml-language-server: $schema=https://json.schemastore.org/kustomization.json +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: +- db-namespace.yaml diff --git a/yconverge/itest/example-db/single/kustomization.yaml b/yconverge/itest/example-db/single/kustomization.yaml new file mode 100644 index 0000000..99b63e7 --- /dev/null +++ b/yconverge/itest/example-db/single/kustomization.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://json.schemastore.org/kustomization.json +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: ONLY_apply_through_cluster_variant + +resources: +- ../base diff --git a/yconverge/itest/example-db/single/yconverge.cue b/yconverge/itest/example-db/single/yconverge.cue new file mode 100644 index 0000000..46a9a5f --- /dev/null +++ b/yconverge/itest/example-db/single/yconverge.cue @@ -0,0 +1,12 @@ +package example_db_single + +import "yolean.se/ystack/yconverge/verify" + +step: verify.#Step & { + checks: [{ + kind: "wait" + resource: "statefulset/database" + for: "jsonpath={.status.currentReplicas}=1" + timeout: "30s" + }] +} diff --git a/yconverge/itest/test.sh b/yconverge/itest/test.sh index 53a61a6..6431f64 100755 --- a/yconverge/itest/test.sh +++ b/yconverge/itest/test.sh @@ -23,9 +23,20 @@ while [ $# -gt 0 ]; do esac done +# Remove a docker container, tolerating only the "not there" case. +_docker_rm_tolerant() { + _name="$1" + if ! _out=$(docker rm -f "$_name" 2>&1); then + case "$_out" in + *"No such container"*) ;; + *) echo "[cue itest] warn: docker rm $_name: $_out" >&2 ;; + esac + fi +} + if [ "$TEARDOWN" = "true" ]; then echo "[cue itest] Tearing down kept cluster ..." - docker rm -f yconverge-itest 2>/dev/null || true # y-script-lint:disable=or-true # may not exist + _docker_rm_tolerant yconverge-itest rm -f /tmp/ystack-yconverge-itest echo "[cue itest] Done" exit 0 @@ -51,7 +62,7 @@ cleanup() { return fi echo "[cue itest] Cleaning up ..." - docker rm -f "$CONTAINER_NAME" 2>/dev/null || true # y-script-lint:disable=or-true # best-effort cleanup + _docker_rm_tolerant "$CONTAINER_NAME" rm -f "$ITEST_KUBECONFIG" } trap cleanup EXIT @@ -97,6 +108,7 @@ y-cue vet ./yconverge/itest/example-namespace/ y-cue vet ./yconverge/itest/example-configmap/ y-cue vet ./yconverge/itest/example-with-dependency/ y-cue vet ./yconverge/itest/example-disabled/ +y-cue vet ./yconverge/itest/example-db/single/ # --- apply with auto-checks --- @@ -177,5 +189,13 @@ rm -rf /tmp/yconverge-itest-broken rm -f "$_OUT" +# --- prod/qa kustomize example --- + +# never include namespaces in actual bases as it makes delete -k irreversibe in many cases +kubectl yconverge --context="$CTX" -k yconverge/itest/example-db/namespace/ +kubectl yconverge --context="$CTX" -k yconverge/itest/cluster-prod/db/ + +kubectl yconverge --context="$CTX" -k yconverge/itest/cluster-qa/db/ + echo "" echo "[cue itest] All tests passed" From 8b6825687e7ce3cc071ff1266669d40bc60d4a47 Mon Sep 17 00:00:00 2001 From: Staffan Olsson Date: Wed, 15 Apr 2026 17:59:11 +0200 Subject: [PATCH 35/36] yconverge: mode selector, inlined dep walker, shared checks, replace dry-run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flag redesign — modes are mutually exclusive with up-front validation: default (apply), --diff=true, --checks-only, --print-deps. Modifiers --dry-run=server|none and --skip-checks are gated to apply mode. --dry-run=client is rejected (kubectl's own --server-side limitation); --dry-run=true no longer silently maps to server. Help prints without --context for kubectl yconverge, help, --help, -h, or no args. Inlined y-yconverge-deps into kubectl-yconverge as _find_cue_dir / _find_imports / _resolve_deps; _VISITED moved from a tempfile to a shell variable. The duplicate indirection logic is gone — a single source of truth for "find yconverge.cue via one-level single-directory indirection, ignoring file resources like a sibling PDB." replace-mode dry-run: delete step now forwards --dry-run=$DRY_RUN so kubectl itself simulates and prints "(server dry run)" without mutating. Previously the delete was skipped entirely under dry-run, hiding intent and leaving "does dry-run delete anything?" untestable. New CUE examples demonstrating shared checks: example-db/checks/checks.cue — #DbChecks: { replicas, list } template example-db/single/yconverge.cue — imports, unifies replicas: 1, adds an exec check asserting no PDB requires >1 replica example-db/distributed/yconverge.cue — imports, unifies replicas: 3 cluster-prod/db gains an ad-hoc pdb.yaml (minAvailable: 2) alongside the distributed base — exercises the "ignore file resources when counting directory resources for indirection" rule and also gives the single-replica check something to fail against when running prod→qa without the recovery delete. Tests added in yconverge/itest/test.sh: - example-replace/ with a converge-mode=replace Job, verifying --dry-run=server prints "(server dry run)" and preserves the Job metadata.uid (i.e. delete is provably non-mutating). Framework correctness fixes uncovered while wiring the above: - exec-kind check retry loop now honors the schema's timeout field and returns non-zero on final failure (previously it retried 15×2s and silently claimed success). - NS_GUESS is exported so exec check commands can target the resolved namespace without hard-coding it. Co-Authored-By: Claude Opus 4.6 (1M context) --- bin/kubectl-yconverge | 279 +++++++++++++----- bin/y-yconverge-deps | 62 ---- .../itest/cluster-prod/db/kustomization.yaml | 1 + yconverge/itest/cluster-prod/db/pdb.yaml | 9 + yconverge/itest/example-db/checks/checks.cue | 13 + .../example-db/distributed/yconverge.cue | 12 + .../itest/example-db/single/yconverge.cue | 20 +- yconverge/itest/example-replace/job.yaml | 13 + .../itest/example-replace/kustomization.yaml | 8 + yconverge/itest/test.sh | 36 +++ 10 files changed, 309 insertions(+), 144 deletions(-) delete mode 100755 bin/y-yconverge-deps create mode 100644 yconverge/itest/cluster-prod/db/pdb.yaml create mode 100644 yconverge/itest/example-db/checks/checks.cue create mode 100644 yconverge/itest/example-db/distributed/yconverge.cue create mode 100644 yconverge/itest/example-replace/job.yaml create mode 100644 yconverge/itest/example-replace/kustomization.yaml diff --git a/bin/kubectl-yconverge b/bin/kubectl-yconverge index 48223e5..8943f6e 100755 --- a/bin/kubectl-yconverge +++ b/bin/kubectl-yconverge @@ -2,54 +2,99 @@ [ -z "$DEBUG" ] || set -x set -e -[ "$1" = "help" ] && echo ' -Idempotent apply with support for converge-mode annotations. +_print_help() { + cat <<'HELP' +Idempotent apply with CUE-backed checks. -Supported values for the yolean.se/converge-mode label: +Usage: + kubectl yconverge --context= [flags] -k + kubectl yconverge help | --help + +Modes (mutually exclusive; default is apply): + --diff=true run kubectl diff, no apply, no checks + --checks-only run yconverge.cue checks against current state, no apply + --print-deps print dependency order from yconverge.cue imports, exit + +Apply-mode modifiers: + --dry-run=MODE forward to kubectl apply/delete (server|none) + (client is rejected: incompatible with --server-side) + --skip-checks skip yconverge.cue check invocation after apply + +Converge modes (label yolean.se/converge-mode on a resource): (none) standard kubectl apply create kubectl create --save-config (skip if exists) replace kubectl delete + apply (for immutable resources like Jobs) serverside kubectl apply --server-side serverside-force kubectl apply --server-side --force-conflicts -Flags (must come before kustomize args): - --context=NAME required - --diff=true run kubectl diff instead of apply - --dry-run=true pass --dry-run=server to kubectl apply - --skip-checks skip yconverge.cue check invocation - -If the -k directory contains a yconverge.cue file (or one is found -one level of resources: indirection away): +If the -k directory contains a yconverge.cue file (or one is found one +level of resources: indirection away): - Dependencies from CUE imports are resolved and converged first - - Checks run after apply + - Checks run after apply (unless --skip-checks) Honors KUBECONFIG if set. -' && exit 0 +HELP +} -DIFF=false -DRY_RUN=false -SKIP_CHECKS=false +case "${1:-}" in + ""|help|--help|-h) + _print_help + exit 0 + ;; +esac + +_die() { echo "Error: $1" >&2; exit 1; } -ctx=$1 -case $ctx in +# --- arg parsing --- + +ctx="$1" +case "$ctx" in "--context="*) shift 1 ;; - *) echo "Error: first arg must be --context=" >&2 && exit 1 ;; + *) _die "first arg must be --context= (try --help)" ;; esac +CONTEXT="${ctx#--context=}" +export CONTEXT + +MODE="apply" +DRY_RUN="" +SKIP_CHECKS=false + +_set_mode() { + [ "$MODE" = "apply" ] || _die "$1 conflicts with $MODE mode" + MODE="$1" +} while true; do case "${1:-}" in - --diff=true) DIFF=true; shift ;; - --dry-run=true) DRY_RUN=true; shift ;; + --diff=true) _set_mode diff; shift ;; + --checks-only) _set_mode checks-only; shift ;; + --print-deps) _set_mode print-deps; shift ;; + --dry-run=*) DRY_RUN="${1#--dry-run=}"; shift ;; --skip-checks) SKIP_CHECKS=true; shift ;; + --help|-h) _print_help; exit 0 ;; *) break ;; esac done -# Extract -k directory from remaining args +case "$DRY_RUN" in + ""|server|none) ;; + client) _die "--dry-run=client is not supported: yconverge uses server-side apply, and kubectl rejects --dry-run=client with --server-side. Use --dry-run=server instead." ;; + *) _die "--dry-run must be one of: server, none" ;; +esac + +if [ -n "$DRY_RUN" ] && [ "$MODE" != "apply" ]; then + _die "--dry-run is only valid in apply mode (got --$MODE)" +fi +if [ "$SKIP_CHECKS" = "true" ] && [ "$MODE" != "apply" ]; then + _die "--skip-checks is only valid in apply mode (got --$MODE)" +fi + +# --- extract -k directory from remaining args --- + KUSTOMIZE_DIR="" for arg in "$@"; do case "$arg" in - -l|--selector) echo "Error: yconverge can not be combined with other selectors" >&2 && exit 1 ;; + -l|--selector) _die "yconverge can not be combined with other selectors" ;; esac done _prev="" @@ -59,55 +104,132 @@ for arg in "$@"; do break fi case "$arg" in - -k) _prev="-k" ;; + -k) _prev="-k" ;; -k*) KUSTOMIZE_DIR="${arg#-k}"; KUSTOMIZE_DIR="${KUSTOMIZE_DIR%/}"; break ;; esac done -CONTEXT="${ctx#--context=}" -export CONTEXT +# --- mode args to propagate on recursive calls --- + +MODE_ARGS="" +case "$MODE" in + diff) MODE_ARGS="--diff=true" ;; + checks-only) MODE_ARGS="--checks-only" ;; + print-deps) MODE_ARGS="--print-deps" ;; +esac +[ -n "$DRY_RUN" ] && MODE_ARGS="$MODE_ARGS --dry-run=$DRY_RUN" +[ "$SKIP_CHECKS" = "true" ] && MODE_ARGS="$MODE_ARGS --skip-checks" + +# --- diff mode: pass through and exit --- + +if [ "$MODE" = "diff" ]; then + kubectl $ctx diff "$@" + exit $? +fi + +# --- yconverge.cue lookup: finds a yconverge.cue file, with 1-level indirection +# through a kustomization.yaml that references exactly one local directory. --- + +_find_cue_dir() { + d="$1" + if [ -f "$d/yconverge.cue" ]; then + echo "$d" + return 0 + fi + [ -f "$d/kustomization.yaml" ] || return 0 + _resources=$(y-yq '.resources // [] | .[] | select(test("^[^h]") and test("^(http|github)") | not)' "$d/kustomization.yaml") + _base_dir="" + _dir_count=0 + _old_ifs="$IFS"; IFS=' +' + for _r in $_resources; do + if [ -d "$d/$_r" ]; then + _dir_count=$((_dir_count + 1)) + [ "$_dir_count" = "1" ] && _base_dir="$_r" + fi + done + IFS="$_old_ifs" + if [ "$_dir_count" = "1" ] && [ -f "$d/$_base_dir/yconverge.cue" ]; then + echo "$d/$_base_dir" + fi + return 0 +} + +# --- dependency graph walk via CUE imports --- +# Emits paths in topological order (deps first, target last). _DEP_VISITED +# holds already-resolved paths, newline-separated, to avoid re-walks/cycles. + +_DEP_VISITED="" + +_find_imports() { + grep '"yolean.se/ystack/' "$1" 2>/dev/null \ + | grep -v '"yolean.se/ystack/yconverge/verify"' \ + | sed 's|.*"yolean.se/ystack/\([^":]*\).*|\1|' \ + || true # y-script-lint:disable=or-true # no imports is valid +} + +_resolve_deps() { + # POSIX sh has no `local`, so recursive calls share named variables. + # Reference $1 (positional arg, call-scoped) for the path throughout, and + # only read _cue_dir before recursing (its subsequent clobbering is harmless). + case " +$_DEP_VISITED +" in + *" +${1%/} +"*) return 0 ;; + esac + _cue_dir=$(_find_cue_dir "${1%/}") + [ -z "$_cue_dir" ] && return 0 + for _dep in $(_find_imports "$_cue_dir/yconverge.cue"); do + _resolve_deps "$_dep" + done + _DEP_VISITED="$_DEP_VISITED +${1%/}" + echo "${1%/}" +} # --- dependency resolution --- -# On first invocation, resolve the dependency tree and call self for each step. -# Inner calls (during resolution) skip this and go straight to apply + checks. +# On first (top-level) invocation, resolve the full dep graph. For print-deps +# mode, print and exit. For multi-step graphs, iterate calling self per step +# and let each run its own apply + checks. -if [ -z "$_YCONVERGE_RESOLVING" ] && [ -n "$KUSTOMIZE_DIR" ] && [ "$SKIP_CHECKS" = "false" ] && [ "$DIFF" = "false" ]; then - deps=$(y-yconverge-deps "$KUSTOMIZE_DIR") +if [ -z "$_YCONVERGE_RESOLVING" ] && [ -n "$KUSTOMIZE_DIR" ]; then + deps=$(_resolve_deps "$KUSTOMIZE_DIR") dep_count=$(printf '%s\n' "$deps" | grep -c . 2>/dev/null) || true # y-script-lint:disable=or-true # grep -c . exit 1 = zero matches + + if [ "$MODE" = "print-deps" ]; then + printf '%s\n' "$deps" + exit 0 + fi + if [ "$dep_count" -gt 1 ] 2>/dev/null; then - echo "=== Converge plan (context=$CONTEXT) ===" + echo "=== Converge plan (context=$CONTEXT, mode=$MODE) ===" echo "Steps ($dep_count):" for d in $deps; do echo " $d"; done echo "===" export _YCONVERGE_RESOLVING=1 for d in $deps; do echo ">>> $d" - kubectl-yconverge $ctx -k "$d/" + kubectl-yconverge $ctx $MODE_ARGS -k "$d/" done exit 0 fi fi -# --- yconverge.cue lookup --- - -_find_yconverge_dir() { - dir="$1" - if [ -f "$dir/yconverge.cue" ]; then - echo "$dir" - return 0 - fi - [ -f "$dir/kustomization.yaml" ] || return 0 - resources=$(y-yq '.resources // [] | .[] | select(test("^[^h]") and test("^(http|github)") | not)' "$dir/kustomization.yaml") - count=$(printf '%s\n' "$resources" | grep -c . 2>/dev/null) || true # y-script-lint:disable=or-true # grep -c . exit 1 = zero matches - if [ "$count" = "1" ] && [ -d "$dir/$resources" ] && [ -f "$dir/$resources/yconverge.cue" ]; then - echo "$dir/$resources" - fi - return 0 -} +# --- single-step path: find yconverge.cue for this target, resolve namespace --- yconverge_dir="" -if [ -n "$KUSTOMIZE_DIR" ] && [ "$SKIP_CHECKS" = "false" ] && [ "$DRY_RUN" = "false" ] && [ "$DIFF" = "false" ]; then - yconverge_dir=$(_find_yconverge_dir "$KUSTOMIZE_DIR") +if [ -n "$KUSTOMIZE_DIR" ]; then + case "$MODE" in + apply) + [ "$SKIP_CHECKS" = "false" ] && yconverge_dir=$(_find_cue_dir "$KUSTOMIZE_DIR") + ;; + checks-only) + yconverge_dir=$(_find_cue_dir "$KUSTOMIZE_DIR") + [ -z "$yconverge_dir" ] && _die "--checks-only: no yconverge.cue found for $KUSTOMIZE_DIR" + ;; + esac fi if [ -n "$yconverge_dir" ]; then @@ -145,16 +267,9 @@ if [ -z "$NS_GUESS" ]; then NS_GUESS=$(kubectl config view --minify --context="$CONTEXT" -o jsonpath='{.contexts[0].context.namespace}') fi [ -z "$NS_GUESS" ] && NS_GUESS="default" +export NS_GUESS -# --- apply --- - -if [ "$DIFF" = "true" ]; then - kubectl $ctx diff "$@" - exit $? -fi - -DRY_RUN_FLAG="" -[ "$DRY_RUN" = "true" ] && DRY_RUN_FLAG="--dry-run=server" +# --- apply (skipped in checks-only mode) --- # Run one internal kubectl step, passing meaningful output through raw. # $1 |-separated error substrings to tolerate silently (exit nonzero but expected) @@ -183,23 +298,25 @@ _kubectl_step() { printf '%s\n' "$_out" } -# create --save-config for converge-mode=create resources: AlreadyExists on re-run -# is the whole point of "idempotent"; no-match on selector is also expected. -_kubectl_step 'AlreadyExists|no objects passed to create' '' \ - $ctx create --save-config $DRY_RUN_FLAG --selector=yolean.se/converge-mode=create "$@" +if [ "$MODE" = "apply" ]; then + DRY_RUN_FLAG="" + [ -n "$DRY_RUN" ] && DRY_RUN_FLAG="--dry-run=$DRY_RUN" + + _kubectl_step 'AlreadyExists|no objects passed to create' '' \ + $ctx create --save-config $DRY_RUN_FLAG --selector=yolean.se/converge-mode=create "$@" -if [ "$DRY_RUN" = "false" ]; then - # delete with --selector and no matches exits 0 with "No resources found" — silent. + # delete for replace-mode resources: under dry-run, kubectl itself simulates + # and prints "(dry run)" without actually deleting. _kubectl_step '' 'No resources found' \ - $ctx delete --selector=yolean.se/converge-mode=replace "$@" -fi + $ctx delete $DRY_RUN_FLAG --selector=yolean.se/converge-mode=replace "$@" -_kubectl_step 'no objects passed to apply' '' \ - $ctx apply --server-side --force-conflicts $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside-force "$@" -_kubectl_step 'no objects passed to apply' '' \ - $ctx apply --server-side $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside "$@" -_kubectl_step 'no objects passed to apply' '' \ - $ctx apply $DRY_RUN_FLAG --selector='yolean.se/converge-mode!=create,yolean.se/converge-mode!=serverside,yolean.se/converge-mode!=serverside-force' "$@" + _kubectl_step 'no objects passed to apply' '' \ + $ctx apply --server-side --force-conflicts $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside-force "$@" + _kubectl_step 'no objects passed to apply' '' \ + $ctx apply --server-side $DRY_RUN_FLAG --selector=yolean.se/converge-mode=serverside "$@" + _kubectl_step 'no objects passed to apply' '' \ + $ctx apply $DRY_RUN_FLAG --selector='yolean.se/converge-mode!=create,yolean.se/converge-mode!=serverside,yolean.se/converge-mode!=serverside-force' "$@" +fi # --- yconverge.cue: post-apply checks --- @@ -233,9 +350,21 @@ if [ -n "$yconverge_dir" ]; then ;; exec) echo " [yconverge] $label $desc" - for _retry_i in $(seq 1 15); do - sh -c "$command" && break || sleep 2 # y-script-lint:disable=or-true # retry loop + _timeout_s=${timeout%s} + _deadline=$(($(date +%s) + _timeout_s)) + _exec_ok=0 + while :; do + if sh -c "$command"; then + _exec_ok=1 + break + fi + [ "$(date +%s)" -ge "$_deadline" ] && break + sleep 2 done + if [ "$_exec_ok" = "0" ]; then + echo " [yconverge] ERROR: exec check failed after ${timeout}: $desc" >&2 + return 1 + fi ;; esac i=$((i + 1)) diff --git a/bin/y-yconverge-deps b/bin/y-yconverge-deps deleted file mode 100755 index 606879b..0000000 --- a/bin/y-yconverge-deps +++ /dev/null @@ -1,62 +0,0 @@ -#!/bin/bash -[ -z "$DEBUG" ] || set -x -set -eo pipefail - -[ "$1" = "help" ] && echo ' -Resolve the dependency tree for a yconverge.cue file. -Outputs kustomization paths in topological order (dependencies first). - -Usage: y-yconverge-deps -' && exit 0 - -YSTACK_HOME="${YSTACK_HOME:-$(cd "$(dirname "$0")/.." && pwd)}" -MODULE_PREFIX="yolean.se/ystack/" - -dir="${1%/}" -[ -z "$dir" ] && echo "Usage: y-yconverge-deps " >&2 && exit 1 - -_VISITED=$(mktemp /tmp/yconverge-deps.XXXXXX) - -_find_cue() { - local d="$1" - if [ -f "$d/yconverge.cue" ]; then - echo "$d" - return 0 - fi - [ -f "$d/kustomization.yaml" ] || return 0 - local resources count - resources=$(y-yq '.resources // [] | .[] | select(test("^[^h]") and test("^(http|github)") | not)' "$d/kustomization.yaml") - count=$(printf '%s\n' "$resources" | grep -c . 2>/dev/null) || true # y-script-lint:disable=or-true # grep -c . exit 1 = zero matches - if [ "$count" = "1" ] && [ -f "$d/$resources/yconverge.cue" ]; then - echo "$d/$resources" - fi - return 0 -} - -_imports() { - grep "\"${MODULE_PREFIX}" "$1" 2>/dev/null \ - | grep -v '/yconverge/cue"' \ - | sed "s|.*\"${MODULE_PREFIX}\([^\":]*\).*|\1|" \ - || true # y-script-lint:disable=or-true # no deps is valid -} - -_resolve() { - local path="$1" - local cue_file dep cue_dir - - if grep -qx "$path" "$_VISITED" 2>/dev/null; then return 0; fi - - cue_dir=$(_find_cue "$path") - [ -z "$cue_dir" ] && return 0 - cue_file="$cue_dir/yconverge.cue" - - for dep in $(_imports "$cue_file"); do - _resolve "$dep" - done - - echo "$path" >> "$_VISITED" - echo "$path" -} - -_resolve "$dir" -rm -f "$_VISITED" diff --git a/yconverge/itest/cluster-prod/db/kustomization.yaml b/yconverge/itest/cluster-prod/db/kustomization.yaml index 064b7fa..575a140 100644 --- a/yconverge/itest/cluster-prod/db/kustomization.yaml +++ b/yconverge/itest/cluster-prod/db/kustomization.yaml @@ -6,3 +6,4 @@ namespace: db resources: - ../../example-db/distributed +- pdb.yaml diff --git a/yconverge/itest/cluster-prod/db/pdb.yaml b/yconverge/itest/cluster-prod/db/pdb.yaml new file mode 100644 index 0000000..3a66a37 --- /dev/null +++ b/yconverge/itest/cluster-prod/db/pdb.yaml @@ -0,0 +1,9 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: database +spec: + minAvailable: 2 + selector: + matchLabels: + app: database diff --git a/yconverge/itest/example-db/checks/checks.cue b/yconverge/itest/example-db/checks/checks.cue new file mode 100644 index 0000000..ede9a72 --- /dev/null +++ b/yconverge/itest/example-db/checks/checks.cue @@ -0,0 +1,13 @@ +package checks + +// Parameterized check set for the database statefulset. +// Variants (single, distributed) import and unify with their own replica count. +#DbChecks: { + replicas: int + list: [{ + kind: "wait" + resource: "statefulset/database" + for: "jsonpath={.status.currentReplicas}=\(replicas)" + timeout: "30s" + }] +} diff --git a/yconverge/itest/example-db/distributed/yconverge.cue b/yconverge/itest/example-db/distributed/yconverge.cue new file mode 100644 index 0000000..ac122c9 --- /dev/null +++ b/yconverge/itest/example-db/distributed/yconverge.cue @@ -0,0 +1,12 @@ +package example_db_distributed + +import ( + "yolean.se/ystack/yconverge/verify" + "yolean.se/ystack/yconverge/itest/example-db/checks" +) + +_shared: checks.#DbChecks & {replicas: 3} + +step: verify.#Step & { + checks: _shared.list +} diff --git a/yconverge/itest/example-db/single/yconverge.cue b/yconverge/itest/example-db/single/yconverge.cue index 46a9a5f..d2df330 100644 --- a/yconverge/itest/example-db/single/yconverge.cue +++ b/yconverge/itest/example-db/single/yconverge.cue @@ -1,12 +1,18 @@ package example_db_single -import "yolean.se/ystack/yconverge/verify" +import ( + "list" + "yolean.se/ystack/yconverge/verify" + "yolean.se/ystack/yconverge/itest/example-db/checks" +) + +_shared: checks.#DbChecks & {replicas: 1} step: verify.#Step & { - checks: [{ - kind: "wait" - resource: "statefulset/database" - for: "jsonpath={.status.currentReplicas}=1" - timeout: "30s" - }] + checks: list.Concat([_shared.list, [{ + kind: "exec" + command: #"kubectl --context=$CONTEXT -n $NS_GUESS get pdb -o jsonpath='{.items[*].spec.minAvailable}' | tr ' ' '\n' | awk '$1 > 1 { exit 1 }'"# + description: "no PDB requires more than 1 replica (single-replica safety)" + timeout: "5s" + }]]) } diff --git a/yconverge/itest/example-replace/job.yaml b/yconverge/itest/example-replace/job.yaml new file mode 100644 index 0000000..63edc04 --- /dev/null +++ b/yconverge/itest/example-replace/job.yaml @@ -0,0 +1,13 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: example-replace-job + labels: + yolean.se/converge-mode: replace +spec: + template: + spec: + restartPolicy: Never + containers: + - name: noop + image: ghcr.io/yolean/static-web-server:2.41.0@sha256:34bb160fd62d2145dabd0598f36352653ec58cf80a8d58c8cd2617097d34564d diff --git a/yconverge/itest/example-replace/kustomization.yaml b/yconverge/itest/example-replace/kustomization.yaml new file mode 100644 index 0000000..37b594f --- /dev/null +++ b/yconverge/itest/example-replace/kustomization.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=https://json.schemastore.org/kustomization.json +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: default + +resources: +- job.yaml diff --git a/yconverge/itest/test.sh b/yconverge/itest/test.sh index 6431f64..7bec0dd 100755 --- a/yconverge/itest/test.sh +++ b/yconverge/itest/test.sh @@ -91,6 +91,23 @@ kubectl --context="$CTX" get ns default >/dev/null 2>&1 \ && echo "[cue itest] kwok cluster ready at port $PORT" \ || { echo "[cue itest] FATAL: kwok cluster not reachable"; exit 1; } +# kwok --manage-all-nodes=true only manages nodes that already exist. Without a +# node, pods stay Pending ("no nodes available to schedule pods") and StatefulSet +# status.currentReplicas never advances past the OrderedReady gate. Create one +# fake node so pod-ready stages fire and replica counts reflect spec. +kubectl --context="$CTX" apply -f - <<'YAML' >/dev/null +apiVersion: v1 +kind: Node +metadata: + name: kwok-node-0 + labels: + kubernetes.io/hostname: kwok-node-0 + type: kwok +status: + capacity: { cpu: "32", memory: 256Gi, pods: "110" } + allocatable: { cpu: "32", memory: 256Gi, pods: "110" } +YAML + export CONTEXT="$CTX" cd "$YSTACK_HOME" @@ -109,6 +126,7 @@ y-cue vet ./yconverge/itest/example-configmap/ y-cue vet ./yconverge/itest/example-with-dependency/ y-cue vet ./yconverge/itest/example-disabled/ y-cue vet ./yconverge/itest/example-db/single/ +y-cue vet ./yconverge/itest/example-db/distributed/ # --- apply with auto-checks --- @@ -144,6 +162,19 @@ echo "[cue itest] Serverside-force label (other selectors match nothing)" kubectl-yconverge --context="$CTX" --skip-checks -k yconverge/itest/example-serverside/ kubectl-yconverge --context="$CTX" --skip-checks -k yconverge/itest/example-serverside/ +echo "" +echo "[cue itest] replace-mode under --dry-run=server must not delete anything" +kubectl-yconverge --context="$CTX" --skip-checks -k yconverge/itest/example-replace/ +_REPLACE_UID_BEFORE=$(kubectl --context="$CTX" -n default get job example-replace-job -o jsonpath='{.metadata.uid}') +_REPLACE_DRY_OUT=$(mktemp /tmp/yconverge-itest-replace.XXXXXX) +kubectl-yconverge --context="$CTX" --skip-checks --dry-run=server -k yconverge/itest/example-replace/ 2>&1 | tee "$_REPLACE_DRY_OUT" +grep -q '(server dry run)' "$_REPLACE_DRY_OUT" +_REPLACE_UID_AFTER=$(kubectl --context="$CTX" -n default get job example-replace-job -o jsonpath='{.metadata.uid}') +[ "$_REPLACE_UID_BEFORE" = "$_REPLACE_UID_AFTER" ] \ + || { echo "[cue itest] FAIL: dry-run deleted/recreated the replace-mode Job (uid $_REPLACE_UID_BEFORE -> $_REPLACE_UID_AFTER)"; exit 1; } +kubectl --context="$CTX" -n default delete job example-replace-job >/dev/null +rm -f "$_REPLACE_DRY_OUT" + _OUT=$(mktemp /tmp/yconverge-itest-out.XXXXXX) # --- assert: indirection output shows referenced path --- @@ -195,6 +226,11 @@ rm -f "$_OUT" kubectl yconverge --context="$CTX" -k yconverge/itest/example-db/namespace/ kubectl yconverge --context="$CTX" -k yconverge/itest/cluster-prod/db/ +# cluster-qa/db asserts that no PDB requires more than 1 replica. Applying prod +# first left a PDB with minAvailable: 2 in the namespace, so remove it before +# running qa — recovery step, not a framework feature. +kubectl --context="$CTX" -n db delete pdb database + kubectl yconverge --context="$CTX" -k yconverge/itest/cluster-qa/db/ echo "" From f487023d0b08e3eabed009ceffca9c01bbda1406 Mon Sep 17 00:00:00 2001 From: Staffan Olsson Date: Thu, 16 Apr 2026 08:13:18 +0200 Subject: [PATCH 36/36] Rename CI workflow from "lint" to "checks" The workflow runs both script linting and yconverge integration tests, so "lint" was misleading. Renames the file and updates the workflow_call reference in images.yaml. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/{lint.yaml => checks.yaml} | 2 +- .github/workflows/images.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename .github/workflows/{lint.yaml => checks.yaml} (98%) diff --git a/.github/workflows/lint.yaml b/.github/workflows/checks.yaml similarity index 98% rename from .github/workflows/lint.yaml rename to .github/workflows/checks.yaml index 7876622..bd1718d 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/checks.yaml @@ -1,4 +1,4 @@ -name: lint +name: checks on: push: diff --git a/.github/workflows/images.yaml b/.github/workflows/images.yaml index 9719b3c..8326e04 100644 --- a/.github/workflows/images.yaml +++ b/.github/workflows/images.yaml @@ -6,10 +6,10 @@ on: - main jobs: - lint: - uses: ./.github/workflows/lint.yaml + checks: + uses: ./.github/workflows/checks.yaml docker: - needs: lint + needs: checks runs-on: ubuntu-latest permissions: packages: write