diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml new file mode 100644 index 00000000..0f23ccb0 --- /dev/null +++ b/.github/workflows/checks.yaml @@ -0,0 +1,82 @@ +name: checks + +on: + push: + branches: + - main + pull_request: + workflow_call: + +jobs: + script-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5.0.0 + - uses: actions/cache/restore@v5.0.5 + with: + key: script-lint-${{ github.ref_name }}- + restore-keys: | + script-lint-main- + path: ~/.cache/ystack + - name: Script lint + run: bin/y-script-lint --fail=degrade bin/ + env: + Y_SCRIPT_LINT_BRANCH: ${{ github.ref_name }} + - uses: actions/cache/save@v5.0.5 + with: + key: script-lint-${{ github.ref_name }}-${{ github.run_id }} + path: ~/.cache/ystack + + itest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5.0.0 + - uses: actions/cache/restore@v5.0.5 + with: + key: itest-${{ github.ref_name }}- + restore-keys: | + itest-main- + path: ~/.cache/ystack + - name: Integration tests (yconverge framework) + run: yconverge/itest/test.sh + env: + YSTACK_HOME: ${{ github.workspace }} + PATH: ${{ github.workspace }}/bin:/usr/local/bin:/usr/bin:/bin + - uses: actions/cache/save@v5.0.5 + with: + key: itest-${{ github.ref_name }}-${{ github.run_id }} + path: ~/.cache/ystack + + e2e-cluster: + # Opt-in via the `e2e-cluster` label on the PR -- runs the full + # qemu-based acceptance test (provision a real k3s cluster, + # converge ystack, validate). Heavyweight (~10-15 min); gates + # behind script-lint + itest so it only fires when the cheaper + # checks have passed. + needs: [script-lint, itest] + if: | + github.event_name == 'pull_request' && + contains(github.event.pull_request.labels.*.name, 'e2e-cluster') + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v5.0.0 + - name: Pre-flight (disk, docker) + run: | + df -h / + docker info | head -10 + - name: Set KUBECONFIG (ystack convention) + run: | + mkdir -p "$HOME/.kube" + echo "KUBECONFIG=$HOME/.kube/yolean" >> "$GITHUB_ENV" + - name: Run cluster acceptance + env: + # The script's first action is `exec env -i ...` which wipes + # the env to "mirror a fresh interactive terminal" on a dev + # laptop. CI's environment is already minimal; setting + # ENV_IS_CLEAN=true skips the trampoline so KUBECONFIG / + # PATH / YSTACK_HOME below take effect. + ENV_IS_CLEAN: "true" + YSTACK_HOME: ${{ github.workspace }} + PATH: ${{ github.workspace }}/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + run: e2e/agents-clusterautomation-acceptance-linux-amd64.sh diff --git a/.github/workflows/images.yaml b/.github/workflows/images.yaml index 9719b3cf..462e6d0e 100644 --- a/.github/workflows/images.yaml +++ b/.github/workflows/images.yaml @@ -16,23 +16,23 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5.0.0 - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4.0.0 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4.0.0 - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4.1.0 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push runner - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7.1.0 env: SOURCE_DATE_EPOCH: 0 BUILDKIT_PROGRESS: plain @@ -49,15 +49,11 @@ jobs: continue-on-error: false timeout-minutes: 45 - - uses: actions/setup-go@v5 - with: - go-version: 1.22 - - - uses: imjasonh/setup-crane@v0.3 + uses: imjasonh/setup-crane@v0.5 - name: Get registry image tag id: imageRegistryTag - uses: mikefarah/yq@v4.44.1 + uses: mikefarah/yq@v4.53.2 with: cmd: yq '.images[0].newTag | sub("(.*)@.*", "${1}")' registry/images/kustomization.yaml - @@ -68,7 +64,7 @@ jobs: - name: Get buildkit image tag id: imageBuildkitTag - uses: mikefarah/yq@v4.44.1 + uses: mikefarah/yq@v4.53.2 with: cmd: yq '.images[0].newTag | sub("(.*)@.*", "${1}")' buildkit/kustomization.yaml - @@ -79,7 +75,7 @@ jobs: - name: Get dockerd image tag id: imageDockerdTag - uses: mikefarah/yq@v4.44.1 + uses: mikefarah/yq@v4.53.2 with: cmd: yq '.images[0].newTag | sub("(.*)@.*", "${1}")' docker/kustomization.yaml - @@ -91,7 +87,7 @@ jobs: - name: Get gitea image tag id: imageGiteaTag - uses: mikefarah/yq@v4.44.1 + uses: mikefarah/yq@v4.53.2 with: cmd: yq '.images[0].newTag | sub("(.*)@.*", "${1}")' git-source/base/kustomization.yaml - @@ -102,7 +98,7 @@ jobs: - name: Get grafana image tag id: imageGrafanaTag - uses: mikefarah/yq@v4.44.1 + uses: mikefarah/yq@v4.53.2 with: cmd: yq '.images[0].newTag | sub("(.*)@.*", "${1}")' monitoring/grafana/kustomization.yaml - @@ -113,7 +109,7 @@ jobs: - name: Get grafana-image-renderer image tag id: imageGrafanaImageRendererTag - uses: mikefarah/yq@v4.44.1 + uses: mikefarah/yq@v4.53.2 with: cmd: yq '.images[1].newTag | sub("(.*)@.*", "${1}")' monitoring/grafana/kustomization.yaml - @@ -124,7 +120,7 @@ jobs: - name: Get redpanda image tag id: imageRedpandaTag - uses: mikefarah/yq@v4.44.1 + uses: mikefarah/yq@v4.53.2 with: cmd: yq '.images[0].newTag | sub("(.*)@.*", "${1}")' kafka/redpanda-image/kustomization.yaml - @@ -132,10 +128,21 @@ jobs: run: | TAG_REDPANDA=${{ steps.imageRedpandaTag.outputs.result }} crane cp docker.redpanda.com/redpandadata/redpanda:$TAG_REDPANDA ghcr.io/yolean/redpanda:$TAG_REDPANDA + - + name: Get versitygw image tag + id: imageVersitygwTag + uses: mikefarah/yq@v4.53.2 + with: + cmd: yq '.spec.template.spec.containers[0].image | sub("[^:]+:(.+)@.*", "${1}")' blobs-versitygw/standalone/deployment.yaml + - + name: Mirror versitygw image from hub + run: | + TAG_VERSITYGW=${{ steps.imageVersitygwTag.outputs.result }} + crane cp docker.io/versity/versitygw:$TAG_VERSITYGW ghcr.io/yolean/versitygw:$TAG_VERSITYGW - name: Get static-web-server image tag id: imageStaticWebServerTag - uses: mikefarah/yq@v4.44.1 + uses: mikefarah/yq@v4.53.2 with: cmd: yq '."static-web-server".version' bin/y-bin.optional.yaml - diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml deleted file mode 100644 index 144ef918..00000000 --- a/.github/workflows/lint.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: lint - -on: - push: - branches: - - main - pull_request: - workflow_call: - -jobs: - script-lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/cache/restore@v4 - with: - key: script-lint-${{ github.ref_name }}- - restore-keys: | - script-lint-main- - path: ~/.cache/ystack - - name: Script lint - run: bin/y-script-lint --fail=degrade bin/ - env: - Y_SCRIPT_LINT_BRANCH: ${{ github.ref_name }} - - uses: actions/cache/save@v4 - with: - key: script-lint-${{ github.ref_name }}-${{ github.run_id }} - path: ~/.cache/ystack diff --git a/bin/.gitignore b/bin/.gitignore index 6832f91c..99149a93 100644 --- a/bin/.gitignore +++ b/bin/.gitignore @@ -6,6 +6,7 @@ buildctl buildx bun bunyan +cluster contain container-structure-test crane diff --git a/bin/acceptance-y-kustomize-local b/bin/acceptance-y-kustomize-local new file mode 100755 index 00000000..1e082f62 --- /dev/null +++ b/bin/acceptance-y-kustomize-local @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +[ -z "$DEBUG" ] || set -x +set -eo pipefail + +YHELP='acceptance-y-kustomize-local - standalone test for y-kustomize/y-cluster-serve.yaml + +Usage: acceptance-y-kustomize-local + +Boots `y-cluster serve -c y-kustomize/` against a temp state dir, +probes the four routes the host-local serve is expected to expose, +and stops the serve. No qemu, no docker, no kubectl -- exercises +only the host-local kustomize-build + HTTP serve pipeline. + +The four routes mirror the source list in y-kustomize/y-cluster-serve.yaml: + /v1/blobs/setup-bucket-job/base-for-annotations.yaml + /v1/blobs/setup-bucket-prep/base-for-annotations.yaml + /v1/kafka/setup-topic-job/base-for-annotations.yaml + /v1/kafka/setup-topic-prep/base-for-annotations.yaml + +Exit codes: + 0 All four routes serve valid YAML + 1 One or more routes failed (404, malformed YAML, serve crash) + +Dependencies: + y-cluster (in PATH; auto-resolved via bin/y-cluster wrapper) + curl +' + +case "${1:-}" in + help|--help|-h) echo "$YHELP"; exit 0 ;; + "") ;; + *) echo "ERROR: unknown argument '$1'" >&2; exit 1 ;; +esac + +YBIN="$(cd "$(dirname "$0")" && pwd)" +YSTACK_HOME="$(cd "$YBIN/.." && pwd)" +SERVE_DIR="$YSTACK_HOME/y-kustomize" +PORT=8944 + +[ -f "$SERVE_DIR/y-cluster-serve.yaml" ] || { echo "ERROR: $SERVE_DIR/y-cluster-serve.yaml not found" >&2; exit 1; } + +STATE_DIR=$(mktemp -d -t acceptance-y-kustomize-local.XXXXXX) +LOG_FILE="$STATE_DIR/serve.log" + +cleanup() { + local rc=$? + echo "# Stopping y-cluster serve ..." + y-cluster serve stop --state-dir "$STATE_DIR" >/dev/null 2>&1 || true # y-script-lint:disable=or-true # best-effort + rm -rf "$STATE_DIR" + exit $rc +} +trap cleanup EXIT INT TERM + +echo "# Starting y-cluster serve (state-dir=$STATE_DIR)" +y-cluster serve ensure -c "$SERVE_DIR" --state-dir "$STATE_DIR" >"$LOG_FILE" 2>&1 + +echo "# Probing /health" +HEALTH=$(curl -fsS --max-time 5 "http://127.0.0.1:$PORT/health") +echo " $HEALTH" +echo "$HEALTH" | grep -q '"routes":4' \ + || { echo "ERROR: expected routes=4 in /health, got: $HEALTH" >&2; exit 1; } + +PASS=0 +FAIL=0 +for route in \ + v1/blobs/setup-bucket-job/base-for-annotations.yaml \ + v1/blobs/setup-bucket-prep/base-for-annotations.yaml \ + v1/kafka/setup-topic-job/base-for-annotations.yaml \ + v1/kafka/setup-topic-prep/base-for-annotations.yaml +do + if curl -fsS --max-time 5 "http://127.0.0.1:$PORT/$route" 2>/dev/null | grep -q '^apiVersion:'; then + echo " PASS /$route" + PASS=$((PASS + 1)) + else + echo " FAIL /$route" >&2 + FAIL=$((FAIL + 1)) + fi +done + +echo "# Results: $PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ] diff --git a/bin/kubectl-yconverge b/bin/kubectl-yconverge new file mode 120000 index 00000000..e89bbc81 --- /dev/null +++ b/bin/kubectl-yconverge @@ -0,0 +1 @@ +cluster \ No newline at end of file diff --git a/bin/y-bin.runner.yaml b/bin/y-bin.runner.yaml index ae23f11f..46526e11 100755 --- a/bin/y-bin.runner.yaml +++ b/bin/y-bin.runner.yaml @@ -155,6 +155,39 @@ cue: tool: tar path: cue +cluster: + version: 0.3.7 + templates: + download: https://github.com/Yolean/y-cluster/releases/download/v${version}/y-cluster_v${version}_${os}_${arch} + sha256: + darwin_amd64: 8b46a3e771a4afc1da855a6cb22f7729bce5b8f09f1b53ab7c02d0d20068b15d + darwin_arm64: b220ffd5062e6de3b55d84d2dbb489977ec84517553d48880735a44dd7a0a961 + linux_amd64: 7c0c97efc6fa3689d6eeb00a7c3a0f1ec9ad4e02d8cc0373434e880d4b807727 + linux_arm64: 224fb614edfd840e4f06488cc2b51e911286352e2ddb1e724fe9861c71707a2b + +contain: + version: 0.9.1 + templates: + download: https://github.com/turbokube/contain/releases/download/v${version}/contain-v${version}-${os}-${arch} + sha256: + darwin_amd64: 514d8492f5daf2a406c0f5a835e5c8887aa20bea0be83b6096659215bd559d55 + darwin_arm64: 04e907d9ad93f3b00bd028f60ababefd5edcdd25e39a4f1f9eb730454097caaf + linux_amd64: 38c070ca6e6057d8f8ff91f1d1ecc79f57ffd2338f19a0e6a48456c15e342429 + linux_arm64: 2ad19485957456a08373b820ec7ba491befe27454f36a57ab420bb1de5b45781 + +kustomize-traverse: + version: 0.1.0 + templates: + download: https://github.com/Yolean/kustomize-traverse/releases/download/v${version}/kustomize-traverse-${os}-${arch}.tar.gz + sha256: + darwin_amd64: bdca1fe29afcbc9817557046a3de2661f9ce5044aec3086a263e2724200bb580 + darwin_arm64: 67acdd588a37cb213afad319ef18b67090214ee1d3bad06a469137cb5ef2b2b8 + linux_amd64: e643fe6a162ef22ef8ecffc960e0fc6c76741613098b3f583c16d9206a4f3628 + linux_arm64: d5e564c54d043350e928fb366a4ab004b09381e1aa3f07c750b598bc2bf2b85c + archive: + tool: tar + path: kustomize-traverse + npx: version: 0.2.1 templates: diff --git a/bin/y-cluster b/bin/y-cluster new file mode 100755 index 00000000..467ca4a8 --- /dev/null +++ b/bin/y-cluster @@ -0,0 +1,11 @@ +#!/bin/bash +[ -z "$DEBUG" ] || set -x +set -e +YBIN="$(dirname "$0")" + +# bin/kubectl-yconverge is a tracked symlink to y-cluster so kubectl +# plugin discovery picks it up. exec -a preserves the invocation +# name so the binary detects which mode it's in. + +version=$(y-bin-download "$YBIN/y-bin.runner.yaml" cluster) +exec -a "$(basename "$0")" "$YBIN/y-cluster-v${version}-bin" "$@" diff --git a/bin/y-cluster-converge-ystack b/bin/y-cluster-converge-ystack deleted file mode 100755 index 03384ede..00000000 --- a/bin/y-cluster-converge-ystack +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env bash -[ -z "$DEBUG" ] || set -x -set -eo pipefail - -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 - -# 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 - -# 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 -fi - -echo "[y-cluster-converge-ystack] Completed. To verify use: y-cluster-validate-ystack --context=$CONTEXT" diff --git a/bin/y-cluster-local-crictl b/bin/y-cluster-local-crictl deleted file mode 100755 index ef0c64ad..00000000 --- a/bin/y-cluster-local-crictl +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -[ -z "$DEBUG" ] || set -x -set -eo pipefail - -PROVISIONER=$(y-cluster-local-detect) - -case "$PROVISIONER" in - k3d) - docker exec -i k3d-ystack-server-0 crictl "$@" - ;; - multipass) - multipass exec ystack-master -- sudo k3s crictl "$@" - ;; - lima) - limactl shell ystack sudo k3s crictl "$@" - ;; -esac diff --git a/bin/y-cluster-local-ctr b/bin/y-cluster-local-ctr deleted file mode 100755 index 3933eac7..00000000 --- a/bin/y-cluster-local-ctr +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -[ -z "$DEBUG" ] || set -x -set -eo pipefail - -PROVISIONER=$(y-cluster-local-detect) - -case "$PROVISIONER" in - k3d) - docker exec -i k3d-ystack-server-0 ctr "$@" - ;; - multipass) - multipass exec ystack-master -- sudo k3s ctr "$@" - ;; - lima) - limactl shell ystack sudo k3s ctr "$@" - ;; -esac diff --git a/bin/y-cluster-local-detect b/bin/y-cluster-local-detect deleted file mode 100755 index 4bdd7fe5..00000000 --- a/bin/y-cluster-local-detect +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -[ -z "$DEBUG" ] || set -x -set -eo pipefail - -CLUSTER=$(kubectl --context=local config view -o jsonpath='{.contexts[?(@.name=="local")].context.cluster}' 2>/dev/null) || true - -case "$CLUSTER" in - ystack-k3d) PROVISIONER=k3d ;; - ystack-multipass) PROVISIONER=multipass ;; - ystack-lima) PROVISIONER=lima ;; - ystack-qemu) PROVISIONER=qemu ;; - *) - echo "No recognized ystack cluster at --context=local (cluster name: '$CLUSTER')" >&2 - exit 1 - ;; -esac - -if [ -z "$1" ]; then - echo "$PROVISIONER" -else - if [ "$1" = "$PROVISIONER" ]; then - echo "up" - else - exit 1 - fi -fi diff --git a/bin/y-cluster-provision b/bin/y-cluster-provision deleted file mode 100755 index 5b5936dc..00000000 --- a/bin/y-cluster-provision +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -[ -z "$DEBUG" ] || set -x -set -eo pipefail - -TEARDOWN=false -[ "$1" = "--teardown" ] && TEARDOWN=true - -if [ "$TEARDOWN" = "true" ]; then - YSTACK_PROVISIONER=$(y-cluster-local-detect) - echo "[y-cluster-provision] Tearing down $YSTACK_PROVISIONER cluster ..." - y-cluster-provision-$YSTACK_PROVISIONER --teardown - exit $? -fi - -if [ -n "$YSTACK_PROVISIONER" ]; then - true -elif command -v qemu-system-x86_64 >/dev/null 2>&1 && command -v qemu-img >/dev/null 2>&1 && command -v cloud-localds >/dev/null 2>&1 && [ -e /dev/kvm ]; then - YSTACK_PROVISIONER=qemu -elif command -v multipass >/dev/null 2>&1; then - YSTACK_PROVISIONER=multipass -elif command -v docker >/dev/null 2>&1; then - YSTACK_PROVISIONER=k3d -else - echo "No provisioner found. Set the YSTACK_PROVISIONER env." && exit 1 -fi - -echo "[y-cluster-provision] Provisioning using y-cluster-provision-$YSTACK_PROVISIONER ..." - -exec y-cluster-provision-$YSTACK_PROVISIONER "$@" diff --git a/bin/y-cluster-provision-k3d b/bin/y-cluster-provision-k3d deleted file mode 100755 index 9baa6595..00000000 --- a/bin/y-cluster-provision-k3d +++ /dev/null @@ -1,143 +0,0 @@ -#!/usr/bin/env bash -[ -z "$DEBUG" ] || set -x -set -eo pipefail - -YSTACK_HOME="$(cd "$(dirname "$0")/.." && pwd)" - -[ -z "$KUBECONFIG" ] && echo "Provision requires an explicit KUBECONFIG env" && exit 1 - -CTX=local -K3D_NAME=ystack -YSTACK_HOST=ystack.local -K3D_MEMORY="8G" -K3D_AGENTS="0" -K3D_DOCKER_UPDATE="--cpuset-cpus=3 --cpus=3" -SKIP_CONVERGE=false -SKIP_IMAGE_LOAD=false -EXCLUDE=monitoring - -while [ $# -gt 0 ]; do - case "$1" in - -h|--help) - cat >&2 <&2; exit 1 ;; - esac -done - -# Verify prerequisites -docker info >/dev/null 2>&1 || { echo "ERROR: Docker is not running" >&2; exit 1; } -command -v y-k3d >/dev/null 2>&1 || { echo "ERROR: y-k3d not found in PATH" >&2; exit 1; } - -# Teardown mode -if [ "$TEARDOWN" = "true" ]; then - if y-k3d cluster list 2>/dev/null | grep -q "^$K3D_NAME "; then - y-k3d cluster delete $K3D_NAME - kubectl config delete-context $CTX 2>/dev/null || true - else - echo "# No k3d cluster '$K3D_NAME' found" - fi - exit 0 -fi - -# Check for existing cluster -if y-k3d cluster list 2>/dev/null | grep -q "^$K3D_NAME "; then - echo "ERROR: k3d cluster '$K3D_NAME' already exists. Delete it first with: y-cluster-provision-k3d --teardown" >&2 - exit 1 -fi - -[ -z "$YSTACK_PORTS_IP" ] && export YSTACK_PORTS_IP=$(y-localhost $YSTACK_HOST show 2>/dev/null) -[ -z "$YSTACK_PORTS_IP" ] || echo "Will bind ports to $YSTACK_PORTS_IP $YSTACK_HOST" - -# k3s airgap image volume mount -AIRGAP_TAR=$(y-k3s-airgap-download) -K3D_AIRGAP_VOL="" -if [ -f "$AIRGAP_TAR" ]; then - echo "# Mounting airgap tarball: $AIRGAP_TAR" - K3D_AIRGAP_VOL="-v $AIRGAP_TAR:/var/lib/rancher/k3s/agent/images/k3s-airgap-images.tar.zst@server:0" -fi - -# Clean up renamed entries from failed provisions (must happen before k3d reads kubeconfig) -kubectl config delete-context $CTX 2>/dev/null || true -kubectl config delete-cluster ystack-k3d 2>/dev/null || true -kubectl config delete-user ystack-k3d 2>/dev/null || true - -# Port-map traefik ports so the host can reach the gateway (Docker bridge IPs aren't routable on macOS) -K3D_PORT_BIND="${YSTACK_PORTS_IP:+$YSTACK_PORTS_IP:}" - -# K3S version from the single source of truth (y-k3s-install) -K3S_VERSION=$(grep '^export INSTALL_K3S_VERSION=' "$YSTACK_HOME/bin/y-k3s-install" | cut -d= -f2) -K3D_IMAGE="rancher/k3s:${K3S_VERSION//+/-}" - -y-k3d cluster create $K3D_NAME \ - --registry-config "$YSTACK_HOME/k3s/docker-image/registries.yaml" \ - --agents="$K3D_AGENTS" \ - --servers-memory="$K3D_MEMORY" \ - --image "$K3D_IMAGE" \ - -p "${K3D_PORT_BIND}80:80@loadbalancer" \ - -p "${K3D_PORT_BIND}443:443@loadbalancer" \ - $K3D_AIRGAP_VOL - -# TODO support agents >0 -K3D_DOCKER_NAME=k3d-$K3D_NAME-server-0 -docker update $K3D_DOCKER_NAME $K3D_DOCKER_UPDATE -docker inspect $K3D_DOCKER_NAME | grep Cpu - -# Could interfere with some k3d functionality. For example skaffold's k3d detection will probably not work. -y-kubectl config rename-context k3d-ystack $CTX - -# Set cluster name for y-cluster-local-detect -sed -e 's/name: k3d-ystack/name: ystack-k3d/g' \ - -e 's/cluster: k3d-ystack/cluster: ystack-k3d/g' \ - -e 's/name: admin@k3d-ystack/name: ystack-k3d/g' \ - -e 's/user: admin@k3d-ystack/user: ystack-k3d/g' "$KUBECONFIG" > "$KUBECONFIG.tmp" \ - && mv "$KUBECONFIG.tmp" "$KUBECONFIG" - -echo "# Waiting for API server to be ready ..." -until kubectl --context=$CTX get nodes >/dev/null 2>&1; do sleep 2; done - -if [ "$SKIP_CONVERGE" = "true" ]; then - echo "# --skip-converge: skipping converge, validate, and post-provision steps" - exit 0 -fi - -if [ "$SKIP_IMAGE_LOAD" = "true" ]; then - echo "# --skip-image-load: skipping image cache and load" -else - echo "# Saving ystack images to local cache ..." - y-image-cache-ystack > /etc/hosts" -docker exec k3d-ystack-server-0 sh -cex "echo '$PROD_REGISTRY_IP prod-registry.ystack.svc.cluster.local' >> /etc/hosts" diff --git a/bin/y-cluster-provision-lima b/bin/y-cluster-provision-lima deleted file mode 100755 index 9e02a154..00000000 --- a/bin/y-cluster-provision-lima +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env bash -[ -z "$DEBUG" ] || set -x -set -eo pipefail - -YSTACK_HOME="$(cd "$(dirname "$0")/.." && pwd)" - -[ -z "$KUBECONFIG" ] && echo "Provision requires an explicit KUBECONFIG env" && exit 1 - -CTX=local -SKIP_CONVERGE=false -SKIP_IMAGE_LOAD=false -EXCLUDE=monitoring -while [ $# -gt 0 ]; do - case "$1" in - -h|--help) - cat >&2 <&2; exit 1 ;; - esac -done - -# Verify prerequisites -command -v limactl >/dev/null 2>&1 || { echo "ERROR: limactl not found in PATH" >&2; exit 1; } - -# Teardown mode -if [ "$TEARDOWN" = "true" ]; then - if limactl list 2>/dev/null | grep -q "^ystack "; then - limactl delete -f ystack - [ "$TEARDOWN_PRUNE" = "true" ] && limactl prune - kubectl config delete-context $CTX 2>/dev/null || true - else - echo "[y-cluster-provision-lima] No Lima VM 'ystack' found" - fi - exit 0 -fi - -# Check for existing VM -if limactl list 2>/dev/null | grep -q "^ystack "; then - echo "ERROR: Lima VM 'ystack' already exists. Delete it first with: limactl delete ystack && limactl prune" >&2 - exit 1 -fi - -# Not reusing y-k3s-install, avoid breaking multipass provision -K3S_INSTALLER_REVISION=50fa2d70c239b3984dab99a2fb1ddaa35c3f2051 - -mkdir -p /tmp/lima/ystack/rancher/k3s -curl -sfL https://github.com/k3s-io/k3s/raw/$K3S_INSTALLER_REVISION/install.sh > /tmp/lima/ystack/install.sh - -limactl start --tty=false $YSTACK_HOME/k3s/ystack.yaml -cp $YSTACK_HOME/k3s/docker-image/registries.yaml /tmp/lima/ystack/rancher/k3s - -TOPOLOGY_ZONE="local" - -# Place airgap tarball before k3s starts -AIRGAP_TAR=$(y-k3s-airgap-download) -if [ -f "$AIRGAP_TAR" ]; then - echo "[y-cluster-provision-lima] Placing airgap tarball into VM" - limactl shell ystack sudo mkdir -p /var/lib/rancher/k3s/agent/images - limactl shell ystack sudo cp "$AIRGAP_TAR" /var/lib/rancher/k3s/agent/images/ -fi - -limactl shell ystack sudo swapoff -a -limactl shell ystack sudo cp -rv /tmp/lima/ystack/rancher /etc -limactl shell ystack sh /tmp/lima/ystack/install.sh --node-label "topology.kubernetes.io/zone=$TOPOLOGY_ZONE" -limactl shell ystack sudo sh -c 'until test -f /etc/rancher/k3s/k3s.yaml; do sleep 1; done; cat /etc/rancher/k3s/k3s.yaml' > "$KUBECONFIG.tmp" - -KUBECONFIG="$KUBECONFIG.tmp" kubectl config rename-context default $CTX - -# Set cluster name for y-cluster-local-detect (after context rename, remaining "default" refs are cluster/user) -sed -i '' -e 's/name: default/name: ystack-lima/g' \ - -e 's/cluster: default/cluster: ystack-lima/g' \ - -e 's/user: default/user: ystack-lima/g' "$KUBECONFIG.tmp" -k() { - KUBECONFIG="$KUBECONFIG.tmp" kubectl --context=$CTX "$@" -} - -until k -n kube-system get pods 2>/dev/null; do - echo "[y-cluster-provision-lima] Waiting for the cluster to respond" - sleep 1 -done - -until k -n kube-system get serviceaccount default 2>/dev/null; do - echo "[y-cluster-provision-lima] Waiting for the default service account to exist" - sleep 1 -done - -if [ "$SKIP_CONVERGE" = "true" ]; then - echo "[y-cluster-provision-lima] --skip-converge: skipping converge, validate, and post-provision steps" - y-kubeconfig-import "$KUBECONFIG.tmp" - exit 0 -fi - -# echo "==> Testing amd64 compatibility ..." -# k run amd64test --image=gcr.io/google_containers/pause-amd64:3.2@sha256:4a1c4b21597c1b4415bdbecb28a3296c6b5e23ca4f9feeb599860a1dac6a0108 -# while k get pod amd64test -o=jsonpath='{.status.containerStatuses[0]}' | grep -v '"started":true'; do sleep 3; done -# k delete --wait=false pod amd64test - -# Import kubeconfig before cache-load and converge (y-kubeconfig-import moves the .tmp file) -y-kubeconfig-import "$KUBECONFIG.tmp" - -if [ "$SKIP_IMAGE_LOAD" = "true" ]; then - echo "[y-cluster-provision-lima] --skip-image-load: skipping image cache and load" -else - echo "[y-cluster-provision-lima] Saving ystack images to local cache" - y-image-cache-ystack > /etc/hosts" -limactl shell ystack sudo sh -c "echo '$PROD_REGISTRY_IP prod-registry.ystack.svc.cluster.local' >> /etc/hosts" diff --git a/bin/y-cluster-provision-multipass b/bin/y-cluster-provision-multipass deleted file mode 100755 index 9e93dcac..00000000 --- a/bin/y-cluster-provision-multipass +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/env bash -[ -z "$DEBUG" ] || set -x -set -eo pipefail - -YSTACK_HOME="$(cd "$(dirname "$0")/.." && pwd)" - -[ -z "$KUBECONFIG" ] && echo "Provision requires an explicit KUBECONFIG env" && exit 1 - -CTX=local -VM_NAME="ystack-master" -VM_RESOURCES="-m 8G -d 40G -c 4" -SKIP_CONVERGE=false -SKIP_IMAGE_LOAD=false -EXCLUDE=monitoring -while [ $# -gt 0 ]; do - case "$1" in - -h|--help) - cat >&2 <&2; exit 1 ;; - esac -done - -# Verify prerequisites -command -v multipass >/dev/null 2>&1 || { echo "ERROR: multipass not found in PATH" >&2; exit 1; } - -# Teardown mode -if [ "$TEARDOWN" = "true" ]; then - if multipass list | grep -q "$VM_NAME"; then - multipass delete "$VM_NAME" - multipass purge - kubectl config delete-context $CTX 2>/dev/null || true - else - echo "# No multipass VM '$VM_NAME' found" - fi - exit 0 -fi - -if multipass list | grep -q "$VM_NAME.*Deleted"; then - echo "# Purging deleted VM $VM_NAME ..." - multipass purge -elif multipass list | grep -q "$VM_NAME"; then - echo "Y-stack appears to be running already" >&2 && exit 1 -fi - -# "noble 24.04.." is currently the version our nodes run -multipass launch noble -n "$VM_NAME" $VM_RESOURCES - -# https://medium.com/@mattiaperi/kubernetes-cluster-with-k3s-and-multipass-7532361affa3 -K3S_NODEIP_MASTER="$(multipass info $VM_NAME | grep "IPv4" | awk -F' ' '{print $2}')" - -YSTACK_PROD_REGISTRY=$YSTACK_PROD_REGISTRY YSTACK_PROD_REGISTRY_REWRITE=$YSTACK_PROD_REGISTRY_REWRITE y-registry-config k3s-yaml \ - | multipass transfer - "$VM_NAME:/tmp/registries.yaml" - -multipass exec "$VM_NAME" -- sudo bash -cex " - $(cat $YSTACK_HOME/bin/y-ubuntu-swapoff) - mkdir -p /etc/rancher/k3s - mv /tmp/registries.yaml /etc/rancher/k3s/ -"; - -AIRGAP_TAR=$(y-k3s-airgap-download) -if [ -f "$AIRGAP_TAR" ]; then - echo "# Transferring airgap tarball to VM ..." - multipass transfer "$AIRGAP_TAR" "$VM_NAME:/tmp/k3s-airgap.tar.zst" - multipass exec "$VM_NAME" -- sudo bash -cex " - mkdir -p /var/lib/rancher/k3s/agent/images - mv /tmp/k3s-airgap.tar.zst /var/lib/rancher/k3s/agent/images/ - " -fi - -multipass exec "$VM_NAME" -- sudo INSTALL_K3S_EXEC="$INSTALL_K3S_EXEC" bash -cex "$(cat $YSTACK_HOME/bin/y-k3s-install)"; - -multipass exec "$VM_NAME" -- sudo cat /etc/rancher/k3s/k3s.yaml \ - | sed "s|127.0.0.1|$K3S_NODEIP_MASTER|" \ - > "$KUBECONFIG.tmp" - -KUBECONFIG="$KUBECONFIG.tmp" kubectl config rename-context default $CTX - -# Set cluster name for y-cluster-local-detect (after context rename, remaining "default" refs are cluster/user) -sed -i '' -e 's/name: default/name: ystack-multipass/g' \ - -e 's/cluster: default/cluster: ystack-multipass/g' \ - -e 's/user: default/user: ystack-multipass/g' "$KUBECONFIG.tmp" - -y-kubeconfig-import "$KUBECONFIG.tmp" - -if [ "$SKIP_CONVERGE" = "true" ]; then - echo "# --skip-converge: skipping converge, validate, and post-provision steps" - echo "# Done. Master IP: $K3S_NODEIP_MASTER" - exit 0 -fi - -if [ "$SKIP_IMAGE_LOAD" = "true" ]; then - echo "# --skip-image-load: skipping image cache and load" -else - echo "# Saving ystack images to local cache ..." - y-image-cache-ystack > /etc/hosts" -multipass exec "$VM_NAME" -- sudo sh -c "echo '$PROD_REGISTRY_IP prod-registry.ystack.svc.cluster.local' >> /etc/hosts" - -echo "# Done. Master IP: $K3S_NODEIP_MASTER" diff --git a/bin/y-cluster-provision-qemu b/bin/y-cluster-provision-qemu deleted file mode 100755 index 0daf25f5..00000000 --- a/bin/y-cluster-provision-qemu +++ /dev/null @@ -1,269 +0,0 @@ -#!/usr/bin/env bash -[ -z "$DEBUG" ] || set -x -set -eo pipefail - -YSTACK_HOME="$(cd "$(dirname "$0")/.." && pwd)" - -[ -z "$KUBECONFIG" ] && echo "Provision requires an explicit KUBECONFIG env" && exit 1 - -CTX=local -VM_NAME="ystack-qemu" -VM_DISK="$HOME/.cache/ystack-qemu/$VM_NAME.qcow2" -VM_DISK_SIZE="40G" -VM_MEMORY="8192" -VM_CPUS="4" -VM_SSH_PORT="2222" -SKIP_CONVERGE=false -SKIP_IMAGE_LOAD=false -EXCLUDE=monitoring - -while [ $# -gt 0 ]; do - case "$1" in - -h|--help) - cat >&2 <&2; exit 1 ;; - esac -done - -VM_DIR="$(dirname "$VM_DISK")" -VM_PIDFILE="$VM_DIR/$VM_NAME.pid" -VM_SEED="$VM_DIR/$VM_NAME-seed.img" - -ssh_vm() { - ssh -i "$VM_SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - -o LogLevel=ERROR -p "$VM_SSH_PORT" ystack@localhost "$@" -} - -scp_to_vm() { - scp -i "$VM_SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - -o LogLevel=ERROR -P "$VM_SSH_PORT" "$1" "ystack@localhost:$2" -} - -# Verify prerequisites -MISSING="" -command -v qemu-system-x86_64 >/dev/null 2>&1 || MISSING="$MISSING qemu-system-x86" -command -v qemu-img >/dev/null 2>&1 || MISSING="$MISSING qemu-utils" -command -v cloud-localds >/dev/null 2>&1 || MISSING="$MISSING cloud-image-utils" -if [ -n "$MISSING" ]; then - echo "Missing packages:$MISSING" >&2 - echo "" >&2 - echo " sudo apt install qemu-system-x86 qemu-utils cloud-image-utils" >&2 - exit 1 -fi -if [ ! -e /dev/kvm ]; then - echo "ERROR: /dev/kvm not found — KVM not available on this machine" >&2 - exit 1 -fi -if ! id -nG | grep -qw kvm; then - echo "ERROR: $USER is not in the kvm group" >&2 - echo "" >&2 - echo " sudo usermod -aG kvm $USER" >&2 - echo " # then log out and back in, or: newgrp kvm" >&2 - exit 1 -fi - -# Export mode -if [ -n "$EXPORT_VMDK" ]; then - [ -f "$VM_DISK" ] || { echo "ERROR: VM disk $VM_DISK not found" >&2; exit 1; } - echo "[y-cluster-provision-qemu] Exporting $VM_DISK to $EXPORT_VMDK ..." - qemu-img convert -f qcow2 -O vmdk -o subformat=streamOptimized "$VM_DISK" "$EXPORT_VMDK" - echo "[y-cluster-provision-qemu] Exported: $EXPORT_VMDK" - exit 0 -fi - -# Teardown mode -if [ "$TEARDOWN" = "true" ]; then - if [ -f "$VM_PIDFILE" ]; then - PID=$(cat "$VM_PIDFILE") - if kill -0 "$PID" 2>/dev/null; then - echo "[y-cluster-provision-qemu] Stopping VM (pid $PID) ..." - kill "$PID" - sleep 2 - fi - rm -f "$VM_PIDFILE" - fi - kubectl config delete-context $CTX 2>/dev/null || true - 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 - -# Check for running VM -if [ -f "$VM_PIDFILE" ] && kill -0 "$(cat "$VM_PIDFILE")" 2>/dev/null; then - echo "ERROR: VM already running (pid $(cat "$VM_PIDFILE")). Use --teardown first." >&2 - exit 1 -fi - -mkdir -p "$VM_DIR" - -# Download Ubuntu cloud image if not cached -UBUNTU_VERSION="noble" -CLOUD_IMG="$VM_DIR/ubuntu-${UBUNTU_VERSION}-server-cloudimg-amd64.img" -if [ ! -f "$CLOUD_IMG" ]; then - echo "[y-cluster-provision-qemu] Downloading Ubuntu $UBUNTU_VERSION cloud image ..." - curl -fSL -o "$CLOUD_IMG" \ - "https://cloud-images.ubuntu.com/${UBUNTU_VERSION}/current/${UBUNTU_VERSION}-server-cloudimg-amd64.img" -fi - -# Create VM disk from cloud image -if [ ! -f "$VM_DISK" ]; then - echo "[y-cluster-provision-qemu] Creating VM disk ($VM_DISK_SIZE) ..." - qemu-img create -f qcow2 -b "$CLOUD_IMG" -F qcow2 "$VM_DISK" "$VM_DISK_SIZE" -fi - -# Generate SSH key for VM access -VM_SSH_KEY="$VM_DIR/$VM_NAME-ssh" -if [ ! -f "$VM_SSH_KEY" ]; then - ssh-keygen -t ed25519 -f "$VM_SSH_KEY" -N "" -q -fi - -# Create cloud-init seed -SSH_PUB=$(cat "$VM_SSH_KEY.pub") -CLOUD_INIT="$VM_DIR/cloud-init.yaml" -cat > "$CLOUD_INIT" </dev/null || echo 1024) -if [ "$UNPRIV_PORT_START" -gt 80 ]; then - echo "ERROR: Cannot bind to port 80 (ip_unprivileged_port_start=$UNPRIV_PORT_START)" >&2 - echo "" >&2 - echo " sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80" >&2 - echo " # To persist: echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/50-unprivileged-ports.conf" >&2 - exit 1 -fi - -# Start VM -echo "[y-cluster-provision-qemu] Starting VM ..." -qemu-system-x86_64 \ - -name "$VM_NAME" \ - -machine accel=kvm \ - -cpu host \ - -smp "$VM_CPUS" \ - -m "$VM_MEMORY" \ - -drive file="$VM_DISK",format=qcow2,if=virtio \ - -drive file="$VM_SEED",format=raw,if=virtio \ - -netdev user,id=net0,hostfwd=tcp::"$VM_SSH_PORT"-:22,hostfwd=tcp::6443-:6443,hostfwd=tcp::80-:80,hostfwd=tcp::443-:443 \ - -device virtio-net-pci,netdev=net0 \ - -serial file:"$VM_DIR/$VM_NAME-console.log" \ - -display none \ - -daemonize \ - -pidfile "$VM_PIDFILE" - -echo "[y-cluster-provision-qemu] Waiting for SSH ..." -for i in $(seq 1 60); do - ssh_vm true 2>/dev/null && break - sleep 2 -done -ssh_vm true || { echo "ERROR: SSH not available after 120s" >&2; exit 1; } - -echo "[y-cluster-provision-qemu] VM ready, installing k3s ..." - -# Disable swap -ssh_vm "sudo swapoff -a" - -# Transfer and configure registry mirrors -REGISTRY_TMP=$(mktemp) -YSTACK_PROD_REGISTRY=$YSTACK_PROD_REGISTRY YSTACK_PROD_REGISTRY_REWRITE=$YSTACK_PROD_REGISTRY_REWRITE y-registry-config k3s-yaml > "$REGISTRY_TMP" -scp_to_vm "$REGISTRY_TMP" /tmp/registries.yaml -rm -f "$REGISTRY_TMP" -ssh_vm "sudo mkdir -p /etc/rancher/k3s && sudo mv /tmp/registries.yaml /etc/rancher/k3s/" - -# Transfer airgap images if available -AIRGAP_TAR=$(y-k3s-airgap-download) -if [ -f "$AIRGAP_TAR" ]; then - echo "[y-cluster-provision-qemu] Transferring airgap tarball ..." - scp_to_vm "$AIRGAP_TAR" /tmp/k3s-airgap.tar.zst - ssh_vm "sudo mkdir -p /var/lib/rancher/k3s/agent/images && sudo mv /tmp/k3s-airgap.tar.zst /var/lib/rancher/k3s/agent/images/" -fi - -# Install k3s -ssh_vm "sudo bash -cex '$(cat $YSTACK_HOME/bin/y-k3s-install)'" - -# Extract kubeconfig -ssh_vm "sudo cat /etc/rancher/k3s/k3s.yaml" \ - | sed "s|127.0.0.1|127.0.0.1|" \ - > "$KUBECONFIG.tmp" - -KUBECONFIG="$KUBECONFIG.tmp" kubectl config rename-context default $CTX - -# Set cluster name for y-cluster-local-detect -sed -i 's/name: default/name: ystack-qemu/g; s/cluster: default/cluster: ystack-qemu/g; s/user: default/user: ystack-qemu/g' "$KUBECONFIG.tmp" - -y-kubeconfig-import "$KUBECONFIG.tmp" - -if [ "$SKIP_CONVERGE" = "true" ]; then - echo "[y-cluster-provision-qemu] --skip-converge: done" - exit 0 -fi - -if [ "$SKIP_IMAGE_LOAD" = "true" ]; then - echo "[y-cluster-provision-qemu] --skip-image-load: skipping" -else - echo "[y-cluster-provision-qemu] Loading images ..." - y-image-cache-ystack > /etc/hosts'" -ssh_vm "sudo sh -c 'echo \"$PROD_REGISTRY_IP prod-registry.ystack.svc.cluster.local\" >> /etc/hosts'" - -echo "[y-cluster-provision-qemu] Done. SSH: ssh -p $VM_SSH_PORT -i $VM_SSH_KEY ystack@localhost" -echo "[y-cluster-provision-qemu] Export: y-cluster-provision-qemu --export-vmdk=appliance.vmdk" diff --git a/bin/y-cluster-validate-ystack b/bin/y-cluster-validate-ystack index 11e7f373..b246ceb5 100755 --- a/bin/y-cluster-validate-ystack +++ b/bin/y-cluster-validate-ystack @@ -20,10 +20,14 @@ k() { } # HTTP requests to cluster services via the K8s API proxy (works regardless of provisioner) -# Usage: kurl +# Usage: kurl +# Pass svc:port (e.g. y-kustomize:8944) when the service doesn't expose port 80. kurl() { local ns="$1" svc="$2" path="$3" - k get --raw "/api/v1/namespaces/$ns/services/$svc:80/proxy/$path" + case "$svc" in + *:*) k get --raw "/api/v1/namespaces/$ns/services/$svc/proxy/$path" ;; + *) k get --raw "/api/v1/namespaces/$ns/services/$svc:80/proxy/$path" ;; + esac } PASS=0 @@ -111,12 +115,17 @@ run_pre_build_checks() { || report "registry v2 API ($phase)" "no response" echo "[y-cluster-validate-ystack] y-kustomize bases" - kurl ystack y-kustomize v1/blobs/setup-bucket-job/base-for-annotations.yaml | k apply --dry-run=client -f - >/dev/null 2>&1 \ - && report "y-kustomize blobs base ($phase)" "ok" \ - || report "y-kustomize blobs base ($phase)" "not serving valid YAML" - kurl ystack y-kustomize v1/kafka/setup-topic-job/base-for-annotations.yaml | k apply --dry-run=client -f - >/dev/null 2>&1 \ - && report "y-kustomize kafka base ($phase)" "ok" \ - || report "y-kustomize kafka base ($phase)" "not serving valid YAML" + if k -n ystack get deployment y-kustomize -o=jsonpath='{.status.readyReplicas}' 2>/dev/null | grep -q '^[1-9]'; then + kurl ystack y-kustomize:8944 v1/blobs/setup-bucket-job/base-for-annotations.yaml | k apply --dry-run=client -f - >/dev/null 2>&1 \ + && report "y-kustomize blobs base ($phase)" "ok" \ + || report "y-kustomize blobs base ($phase)" "not serving valid YAML" + kurl ystack y-kustomize:8944 v1/kafka/setup-topic-job/base-for-annotations.yaml | k apply --dry-run=client -f - >/dev/null 2>&1 \ + && report "y-kustomize kafka base ($phase)" "ok" \ + || report "y-kustomize kafka base ($phase)" "not serving valid YAML" + else + echo "[y-cluster-validate-ystack] SKIP y-kustomize blobs base ($phase) - deploy/y-kustomize not ready (likely host-side serve in use; v0.3.0 image not released)" + echo "[y-cluster-validate-ystack] SKIP y-kustomize kafka base ($phase) - deploy/y-kustomize not ready (likely host-side serve in use; v0.3.0 image not released)" + fi } echo "[y-cluster-validate-ystack] Dev cluster validation: context=$CONTEXT" @@ -147,7 +156,7 @@ if k get ns kafka >/dev/null 2>&1; then TOPIC_NAME="y-cluster-validate-ystack" # Create topic via y-kustomize setup job - k -n kafka delete job setup-topic 2>/dev/null || true + k -n kafka delete job setup-topic 2>/dev/null || true # y-script-lint:disable=or-true # best-effort: previous-run leftover may not exist k apply -k "$YSTACK_HOME/kafka/validate-topic/" 2>&1 | head -5 k -n kafka wait --for=condition=complete job/setup-topic --timeout=60s 2>&1 \ && report "kafka topic create" "ok" \ @@ -176,7 +185,7 @@ echo "[y-cluster-validate-ystack] Build + deploy (y-build)" EXAMPLE_DIR="$YSTACK_HOME/examples/y-build" REGISTRY_HOST="builds-registry.ystack.svc.cluster.local" VALIDATE_IMAGE="$REGISTRY_HOST/ystack-validate/y-build-test:latest" -y-buildkitd-available --context="$CONTEXT" 2>&1 || true +y-buildkitd-available --context="$CONTEXT" 2>&1 || true # y-script-lint:disable=or-true # advisory pre-check; y-build below is the real gate echo "[y-cluster-validate-ystack] Building example image" if BUILD_CONTEXT="$EXAMPLE_DIR" IMAGE="$VALIDATE_IMAGE" IMPORT_CACHE=false EXPORT_CACHE=false y-build; then report "y-build" "ok" @@ -184,6 +193,29 @@ if BUILD_CONTEXT="$EXAMPLE_DIR" IMAGE="$VALIDATE_IMAGE" IMPORT_CACHE=false EXPOR kurl ystack builds-registry v2/ystack-validate/y-build-test/tags/list 2>&1 | grep -q '"latest"' \ && report "y-build-test pushed" "ok" \ || report "y-build-test pushed" "image not found in registry" + + # Node-side pull through the registries.yaml mirror. The build/push + # path above goes pod -> Service ClusterIP, which works without any + # mirror; this step exercises the path that the cluster-config + # `registries:` block enables -- containerd on the node resolving + # builds-registry.ystack.svc.cluster.local to its magic ClusterIP. + # imagePullPolicy=Always forces a fetch even if a local cache exists. + PULL_POD=y-build-test-pull + k -n ystack delete pod "$PULL_POD" --ignore-not-found --wait=true >/dev/null 2>&1 || true # y-script-lint:disable=or-true # best-effort cleanup before run + # The just-pushed image is built FROM ghcr.io/yolean/static-web-server, + # a distroless image whose entrypoint is `sws` (no shell, no /bin/true). + # Don't override --command; let sws run, and treat Pod-Ready as the + # signal that containerd resolved + pulled via the registries.yaml + # mirror. The post-run delete cleans up the long-running server. + k -n ystack run "$PULL_POD" --image="$VALIDATE_IMAGE" \ + --image-pull-policy=Always --restart=Never >/dev/null + if k -n ystack wait --for=condition=Ready "pod/$PULL_POD" --timeout=60s >/dev/null 2>&1; then + report "y-build-test node pull (registries.yaml mirror)" "ok" + else + PULL_REASON=$(k -n ystack get pod "$PULL_POD" -o jsonpath='{.status.containerStatuses[0].state.waiting.reason}{"/"}{.status.phase}' 2>/dev/null) + report "y-build-test node pull (registries.yaml mirror)" "pod state: ${PULL_REASON:-unknown}" + fi + k -n ystack delete pod "$PULL_POD" --ignore-not-found >/dev/null 2>&1 || true # y-script-lint:disable=or-true # best-effort cleanup else report "y-build" "build failed" fi diff --git a/bin/y-contain b/bin/y-contain index 0909efa6..56d3784b 100755 --- a/bin/y-contain +++ b/bin/y-contain @@ -3,6 +3,6 @@ set -e YBIN="$(dirname $0)" -version=$(y-bin-download $YBIN/y-bin.optional.yaml contain) +version=$(y-bin-download $YBIN/y-bin.runner.yaml contain) y-contain-v${version}-bin "$@" || exit $? diff --git a/bin/y-image-cache-load b/bin/y-image-cache-load deleted file mode 100755 index 7cac3bd1..00000000 --- a/bin/y-image-cache-load +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env bash -[ -z "$DEBUG" ] || set -x -set -eo pipefail - -[ -z "$1" ] && echo "Usage: y-image-cache-load " >&2 && exit 1 - -IMAGE_REF="$1" -CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/ystack-image-cache" - -TAG_REF="${IMAGE_REF%%@*}" -OCI_NAME="${TAG_REF//[:\/]/-}" -OCI_DIR="$CACHE_DIR/oci/$OCI_NAME" - -if [ ! -f "$OCI_DIR/index.json" ]; then - echo "# Not cached: $TAG_REF (run y-image-cache-save first)" >&2 - exit 1 -fi - -echo "# Loading $TAG_REF into local cluster containerd" - -PROVISIONER=$(y-cluster-local-detect) - -if [ "$PROVISIONER" = "multipass" ]; then - # multipass exec truncates large stdin pipes; transfer as file instead - TMPTAR=$(mktemp /tmp/ystack-cache-load-XXXXXX.tar) - trap "rm -f '$TMPTAR'" EXIT - tar -cf "$TMPTAR" -C "$OCI_DIR" . - multipass transfer "$TMPTAR" "ystack-master:/tmp/oci-import.tar" - multipass exec ystack-master -- sudo k3s ctr images import --all-platforms --digests /tmp/oci-import.tar - multipass exec ystack-master -- rm -f /tmp/oci-import.tar -else - tar -cf - -C "$OCI_DIR" . | y-cluster-local-ctr images import --all-platforms --digests - -fi - -# Ensure digest-pinned references work (ctr --digests only creates them for Docker Hub annotations) -CACHED_DIGEST=$(jq -r '.manifests[0].digest' "$OCI_DIR/index.json") -ANNOTATED_REF=$(jq -r '.manifests[0].annotations["org.opencontainers.image.ref.name"]' "$OCI_DIR/index.json") - -# Fix Docker Hub naming: crane annotates as index.docker.io but kubelet expects docker.io -if [[ "$ANNOTATED_REF" == index.docker.io/* ]]; then - FIXED_REF="docker.io/${ANNOTATED_REF#index.docker.io/}" - echo "# Tagging $ANNOTATED_REF -> $FIXED_REF" - y-cluster-local-ctr images tag "$ANNOTATED_REF" "$FIXED_REF" - ANNOTATED_REF="$FIXED_REF" -fi - -# Tag with digest so pods using image@sha256:... with imagePullPolicy Never/IfNotPresent work -if [[ "$ANNOTATED_REF" == *@sha256:* ]]; then - # Annotation is digest-only (e.g. repo@sha256:...), also create a tag ref if we know the tag - if [[ "$TAG_REF" == *:* ]]; then - # Ensure Docker Hub images get the docker.io/ prefix kubelet expects - FULL_TAG_REF="$TAG_REF" - FIRST_SEGMENT="${FULL_TAG_REF%%/*}" - if [[ "$FIRST_SEGMENT" == index.docker.io ]]; then - FULL_TAG_REF="docker.io/${FULL_TAG_REF#index.docker.io/}" - elif [[ "$FIRST_SEGMENT" != *.* ]]; then - # No dot in the first path segment means Docker Hub (e.g. versity/versitygw:v1.3.0) - FULL_TAG_REF="docker.io/$FULL_TAG_REF" - fi - echo "# Tagging tag ref: $FULL_TAG_REF" - y-cluster-local-ctr images tag "$ANNOTATED_REF" "$FULL_TAG_REF" 2>/dev/null || true - fi -else - REPO="${ANNOTATED_REF%:*}" - DIGEST_REF="${REPO}@${CACHED_DIGEST}" - echo "# Tagging digest ref: $DIGEST_REF" - y-cluster-local-ctr images tag "$ANNOTATED_REF" "$DIGEST_REF" 2>/dev/null || true -fi diff --git a/bin/y-image-cache-load-all b/bin/y-image-cache-load-all index abce72aa..dfcc44fc 100755 --- a/bin/y-image-cache-load-all +++ b/bin/y-image-cache-load-all @@ -2,8 +2,82 @@ [ -z "$DEBUG" ] || set -x set -eo pipefail -IMAGES=$(y-image-list-ystack) -while IFS= read -r image; do - [ -z "$image" ] && continue - y-image-cache-load "$image" to fetch each image into the shared +cache (XDG_CACHE_HOME/y-cluster/images//), then y-cluster images +load to import that layout into the cluster node containerd. + +Usage: y-image-cache-load-all [--context=NAME] [--converge=LIST] + --context=NAME kubeconfig context (default: local) + --converge=LIST comma-separated k3s/ base names to cache + load + (default: every k3s/* base with non-empty image list) + +Requires y-cluster on PATH (provides images list/cache/load + cache info). +' && exit 0 + +YSTACK_HOME="$(cd "$(dirname "$0")/.." && pwd)" +CONTEXT=local +CONVERGE_TARGETS="" + +while [ $# -gt 0 ]; do + case "$1" in + --context=*) CONTEXT="${1#*=}"; shift ;; + --converge=*) CONVERGE_TARGETS="${1#*=}"; shift ;; + *) echo "Unknown flag: $1" >&2; exit 1 ;; + esac +done + +# Build the list of k3s/ bases to scan. +BASES=() +if [ -n "$CONVERGE_TARGETS" ]; then + for t in ${CONVERGE_TARGETS//,/ }; do + for d in "$YSTACK_HOME"/k3s/*/; do + base="${d%/}" + base="${base##*/}" + base="${base#[0-9][0-9]-}" + if [ "$base" = "$t" ]; then + BASES+=("$d") + break + fi + done + done +else + for d in "$YSTACK_HOME"/k3s/*/; do + [ -f "$d/kustomization.yaml" ] || continue + BASES+=("$d") + done +fi + +# Collect unique image refs across all bases. +REFS=$( + for d in "${BASES[@]}"; do + if ! kubectl kustomize "$d" 2>/dev/null | y-cluster images list - 2>/dev/null; then + >&2 echo "# skip: $d (kustomize build failed; likely depends on a running cluster)" + fi + done | sort -u +) + +[ -z "$REFS" ] && { echo "# no images found"; exit 0; } + +CACHE_ROOT=$(y-cluster cache info -p) + +while IFS= read -r ref; do + [ -z "$ref" ] && continue + + echo "# cache: $ref" + digest_ref=$(y-cluster images cache "$ref") + digest="${digest_ref#*@}" + digest_dir="$CACHE_ROOT/images/$digest" + + if [ ! -d "$digest_dir" ]; then + echo "# WARN: no cache layout at $digest_dir, skipping load" >&2 + continue + fi + + echo "# load: $ref" + tar -cf - -C "$digest_dir" . | y-cluster images load --context="$CONTEXT" - +done <<< "$REFS" diff --git a/bin/y-image-cache-save b/bin/y-image-cache-save deleted file mode 100755 index b097a598..00000000 --- a/bin/y-image-cache-save +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -[ -z "$DEBUG" ] || set -x -set -eo pipefail - -[ -z "$1" ] && echo "Usage: y-image-cache-save " >&2 && exit 1 - -IMAGE_REF="$1" -CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/ystack-image-cache" - -TAG_REF="${IMAGE_REF%%@*}" -OCI_NAME="${TAG_REF//[:\/]/-}" -OCI_DIR="$CACHE_DIR/oci/$OCI_NAME" - -if [ -f "$OCI_DIR/index.json" ]; then - EXPECTED_DIGEST="${IMAGE_REF##*@}" - if [ "$EXPECTED_DIGEST" != "$IMAGE_REF" ]; then - CACHED_DIGEST=$(jq -r '.manifests[0].digest' "$OCI_DIR/index.json") - if [ "$CACHED_DIGEST" = "$EXPECTED_DIGEST" ]; then - echo "# Already cached: $TAG_REF ($EXPECTED_DIGEST)" - exit 0 - fi - echo "# Digest mismatch, re-caching $TAG_REF" - else - echo "# Already cached: $TAG_REF" - exit 0 - fi -fi - -mkdir -p "$OCI_DIR" -echo "# Saving $IMAGE_REF to $OCI_DIR" -y-crane pull --format=oci --annotate-ref "$IMAGE_REF" "$OCI_DIR" -echo "# Saved: $(jq -r '.manifests[0].digest' "$OCI_DIR/index.json")" diff --git a/bin/y-image-cache-ystack b/bin/y-image-cache-ystack deleted file mode 100755 index 88d7e6f8..00000000 --- a/bin/y-image-cache-ystack +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash -[ -z "$DEBUG" ] || set -x -set -eo pipefail - -y-image-list-ystack | while read -r image; do - [ -z "$image" ] && continue - y-image-cache-save "$image" -done diff --git a/bin/y-image-list-ystack b/bin/y-image-list-ystack deleted file mode 100755 index c15a13fa..00000000 --- a/bin/y-image-list-ystack +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash -[ -z "$DEBUG" ] || set -x -set -eo pipefail - -YSTACK_HOME="$(cd "$(dirname "$0")/.." && pwd)" - -# Converge bases from y-cluster-converge-ystack BASES array -BASES=$(sed -n '/^BASES=(/,/^)/{ /^BASES=(/d; /^)/d; s/^[[:space:]]*//; p; }' "$YSTACK_HOME/bin/y-cluster-converge-ystack") -for base in $BASES; do - kubectl kustomize "$YSTACK_HOME/k3s/$base/" 2>/dev/null \ - | grep -oE 'image:\s*\S+' \ - | sed 's/image:[[:space:]]*//' \ - || true -done | sort -u diff --git a/bin/y-k3s-airgap-download b/bin/y-k3s-airgap-download deleted file mode 100755 index 49c9d4ff..00000000 --- a/bin/y-k3s-airgap-download +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash -[ -z "$DEBUG" ] || set -x -set -eo pipefail - -YSTACK_HOME="$(cd "$(dirname "$0")/.." && pwd)" - -# Single source of truth for K3S version (parsed from y-k3s-install) -K3S_VERSION=$(grep '^export INSTALL_K3S_VERSION=' "$YSTACK_HOME/bin/y-k3s-install" | cut -d= -f2) -export K3S_VERSION - -ARCH=$(uname -m) -case "$ARCH" in - x86_64) ARCH=amd64 ;; - aarch64) ARCH=arm64 ;; -esac - -CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/ystack-image-cache" -AIRGAP_DIR="$CACHE_DIR/airgap/$K3S_VERSION" -AIRGAP_TAR="$AIRGAP_DIR/k3s-airgap-images-$ARCH.tar.zst" - -if [ -f "$AIRGAP_TAR" ]; then - echo "# Already cached: $AIRGAP_TAR" >&2 - echo "$AIRGAP_TAR" - exit 0 -fi - -mkdir -p "$AIRGAP_DIR" - -DOWNLOAD_URL="https://github.com/k3s-io/k3s/releases/download/${K3S_VERSION/+/%2B}/k3s-airgap-images-$ARCH.tar.zst" -echo "# Downloading k3s airgap images for $K3S_VERSION ($ARCH) ..." >&2 -curl -fSL -o "$AIRGAP_TAR.tmp" "$DOWNLOAD_URL" -mv "$AIRGAP_TAR.tmp" "$AIRGAP_TAR" -echo "# Saved: $AIRGAP_TAR" >&2 -echo "$AIRGAP_TAR" diff --git a/bin/y-k3s-install b/bin/y-k3s-install deleted file mode 100755 index 7a17c3b3..00000000 --- a/bin/y-k3s-install +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -[ -z "$DEBUG" ] || set -x -set -eo pipefail - -[ $(id -u) -ne 0 ] && echo "su privileges required for the k3s installer" && exec sudo -E $0 "$@" - -export INSTALL_K3S_SKIP_START=true -export K3S_NODE_NAME=ystack-master - -# For kubectl top to work with metrics-server, https://github.com/rancher/k3s/issues/252#issuecomment-482662774 -export INSTALL_K3S_EXEC="--kubelet-arg=address=0.0.0.0 ${INSTALL_K3S_EXEC}" - -INSTALLER_REVISION=50fa2d70c239b3984dab99a2fb1ddaa35c3f2051 -export INSTALL_K3S_VERSION=v1.35.1+k3s1 -curl -sfL https://github.com/k3s-io/k3s/raw/$INSTALLER_REVISION/install.sh | sh - - -service k3s start - -# Validate containerd runtime is ready (crictl info returns verbose JSON) -if k3s crictl info 2>&1 | grep -q '"RuntimeReady"'; then - echo "# containerd: RuntimeReady" -else - echo "ERROR: containerd runtime not ready" >&2 - k3s crictl info >&2 - exit 1 -fi - -ctx="--kubeconfig=/etc/rancher/k3s/k3s.yaml" -k3s kubectl $ctx get node -sleep 5 -until k3s kubectl $ctx wait --for=condition=Ready node/ystack-master; do sleep 5; done diff --git a/bin/y-k8s-ingress-hosts b/bin/y-k8s-ingress-hosts index b10529cc..02601d99 100755 --- a/bin/y-k8s-ingress-hosts +++ b/bin/y-k8s-ingress-hosts @@ -8,7 +8,7 @@ YBIN="$(dirname $0)" CTX="" CHECK=false ENSURE=false -EXPLICIT_OVERRIDE_IP="" +EXPLICIT_HOST_IP="" PASSTHROUGH=() while [ $# -gt 0 ]; do @@ -22,19 +22,32 @@ Flags: -write rewrite host file -check|--check check if /etc/hosts includes required entries (no sudo) --ensure check, then write if needed (combines -check and -write) - -override-ip=IP use this IP for all entries (overrides gateway annotation) - -override-ip IP use this IP for all entries (overrides gateway annotation) + --host-ip=IP override IP for all entries (otherwise resolved from + the GatewayClass yolean.se/dns-hint-ip annotation) -h, --help show this help -If no -override-ip is given, reads yolean.se/override-ip annotation from -the ystack gateway in ystack namespace. +If --host-ip is not given, resolution walks + Gateway/ystack.ystack -> spec.gatewayClassName -> GatewayClass + -> metadata.annotations[yolean.se/dns-hint-ip] +which y-cluster provision stamps when the host forwards guest:80. +The legacy yolean.se/override-ip annotation on Gateway/ystack.ystack +is consulted as a fallback for clusters provisioned before that +contract landed. EOF exit 0 ;; --context=*) CTX="${1#*=}"; shift ;; -check|--check) CHECK=true; shift ;; --ensure) ENSURE=true; shift ;; - -override-ip=*) EXPLICIT_OVERRIDE_IP="${1#*=}"; shift ;; - -override-ip) EXPLICIT_OVERRIDE_IP="$2"; shift; shift ;; + --host-ip=*) EXPLICIT_HOST_IP="${1#*=}"; shift ;; + --host-ip) EXPLICIT_HOST_IP="$2"; shift; shift ;; + -override-ip=*|--override-ip=*) + EXPLICIT_HOST_IP="${1#*=}" + echo "# warn: -override-ip is deprecated; use --host-ip" >&2 + shift ;; + -override-ip|--override-ip) + EXPLICIT_HOST_IP="$2" + echo "# warn: -override-ip is deprecated; use --host-ip" >&2 + shift; shift ;; *) PASSTHROUGH+=("$1"); shift ;; esac done @@ -45,17 +58,35 @@ CONTEXT_KUBECONFIG=$(mktemp) trap "rm -f $CONTEXT_KUBECONFIG" EXIT kubectl config view --raw --minify --context="$CTX" --request-timeout=5s > "$CONTEXT_KUBECONFIG" -# Resolve override IP: explicit flag > gateway annotation -OVERRIDE_IP="$EXPLICIT_OVERRIDE_IP" -if [ -z "$OVERRIDE_IP" ]; then - OVERRIDE_IP=$(kubectl --context="$CTX" --request-timeout=5s -n ystack get gateway ystack \ - -o jsonpath='{.metadata.annotations.yolean\.se/override-ip}' 2>/dev/null || true) - if [ -n "$OVERRIDE_IP" ]; then - echo "# Using override-ip=$OVERRIDE_IP from gateway annotation" +# Resolve the host-side dial IP, in priority order: +# 1. --host-ip flag (or Y_HOST_IP env) +# 2. Provisioner-published annotation on the GatewayClass (per +# specs/ystack/CHANGE_REQUEST_HINT_IP.md) +# 3. Legacy yolean.se/override-ip annotation on the consumer Gateway +# The resolved value is fed to the underlying Go binary as +# `-override-ip `, which still names the override flag in the +# k8s-ingress-hosts v0.5.x release. +HOST_IP="${EXPLICIT_HOST_IP:-${Y_HOST_IP:-}}" +if [ -z "$HOST_IP" ]; then + GATEWAY_CLASS=$(kubectl --context="$CTX" --request-timeout=5s -n ystack get gateway ystack \ + -o jsonpath='{.spec.gatewayClassName}' 2>/dev/null || true) # y-script-lint:disable=or-true # missing Gateway is a normal pre-converge state + if [ -n "$GATEWAY_CLASS" ]; then + HOST_IP=$(kubectl --context="$CTX" --request-timeout=5s get gatewayclass "$GATEWAY_CLASS" \ + -o jsonpath='{.metadata.annotations.yolean\.se/dns-hint-ip}' 2>/dev/null || true) # y-script-lint:disable=or-true # GatewayClass may exist without the annotation + if [ -n "$HOST_IP" ]; then + echo "# Using host-ip=$HOST_IP from GatewayClass/$GATEWAY_CLASS yolean.se/dns-hint-ip" + fi + fi +fi +if [ -z "$HOST_IP" ]; then + HOST_IP=$(kubectl --context="$CTX" --request-timeout=5s -n ystack get gateway ystack \ + -o jsonpath='{.metadata.annotations.yolean\.se/override-ip}' 2>/dev/null || true) # y-script-lint:disable=or-true # legacy annotation is best-effort + if [ -n "$HOST_IP" ]; then + echo "# Using host-ip=$HOST_IP from Gateway/ystack yolean.se/override-ip (legacy)" fi fi -if [ -n "$OVERRIDE_IP" ]; then - PASSTHROUGH+=("-override-ip" "$OVERRIDE_IP") +if [ -n "$HOST_IP" ]; then + PASSTHROUGH+=("-override-ip" "$HOST_IP") fi version=$(y-bin-download $YBIN/y-bin.optional.yaml k8s-ingress-hosts) @@ -67,11 +98,11 @@ if $CHECK || $ENSURE; then [ -z "$line" ] && continue EXPECTED_IP=$(echo "$line" | awk '{print $1}') HOST=$(echo "$line" | awk '{print $2}') - ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" /etc/hosts 2>/dev/null || true) + ACTUAL=$(grep -E "^[^#]*[[:space:]]$HOST([[:space:]]|$)" /etc/hosts 2>/dev/null || true) # y-script-lint:disable=or-true # grep exits 1 on no match -- expected for a missing-host check if [ -z "$ACTUAL" ]; then echo "Missing: $line" STALE=1 - elif ! echo "$ACTUAL" | grep -qE "^[[:space:]]*$EXPECTED_IP[[:space:]]"; then + elif ! echo "$ACTUAL" | grep -qE "^[[:space:]]*${EXPECTED_IP}[[:space:]]"; then ACTUAL_IP=$(echo "$ACTUAL" | awk '{print $1}') echo "Stale: $HOST has $ACTUAL_IP, expected $EXPECTED_IP" STALE=1 @@ -89,6 +120,33 @@ if $CHECK || $ENSURE; then PASSTHROUGH+=("-write") fi +# Guard: don't write an empty block that clears existing entries. +# Preview without -write to check if there are entries. +_PREVIEW_ARGS=() +for _a in "${PASSTHROUGH[@]}"; do + [ "$_a" = "-write" ] || _PREVIEW_ARGS+=("$_a") +done +echo "# reading k8s ingress resources..." +_PREVIEW=$($YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${_PREVIEW_ARGS[@]}" 2>/dev/null | grep -v '^#') +if [ -z "$_PREVIEW" ]; then + echo "# no ingress/gateway entries found, skipping write to preserve existing /etc/hosts" + exit 0 +fi + +# One-line stdout log when this invocation will actually mutate +# /etc/hosts (i.e. -write is in PASSTHROUGH, set either explicitly +# by the caller or appended above by --ensure on detected drift). +# Useful as a converge-trace breadcrumb so a yconverge exec check +# that ran y-k8s-ingress-hosts is visibly attributable. +WRITE_MODE=false +for _a in "${PASSTHROUGH[@]}"; do + [ "$_a" = "-write" ] && WRITE_MODE=true +done +if $WRITE_MODE; then + HOST_COUNT=$(echo "$_PREVIEW" | wc -l | tr -d ' ') + echo "y-k8s-ingress-hosts: writing $HOST_COUNT host entries to /etc/hosts" +fi + [ $(id -u) -ne 0 ] && exec sudo $YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" $YBIN/y-k8s-ingress-hosts-v${version}-bin -kubeconfig "$CONTEXT_KUBECONFIG" "${PASSTHROUGH[@]}" || exit $? diff --git a/bin/y-kubeconfig-import b/bin/y-kubeconfig-import deleted file mode 100755 index 6dc2a94c..00000000 --- a/bin/y-kubeconfig-import +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env bash -[ -z "$DEBUG" ] || set -x -set -eo pipefail - -[ -z "$1" ] && echo "First arg should be the path to a _temporary_ kubeconfig file" && exit 1 - -CONFTEMP="$1" - -[ ! -f "$CONFTEMP" ] && echo "Temporary file $CONFTEMP not found. No import performed." && exit 1 - -[ -z "$KUBECONFIG" ] && echo "This script requires a KUBECONFIG env. Aborting merge." && exit 1 - -if [ -f "$KUBECONFIG" ]; then - echo "Target kubeconfig $KUBECONFIG already exists. Merging." - KUBECONFIG="$CONFTEMP:$KUBECONFIG" kubectl config view --flatten > "$CONFTEMP-merged" - mv "$CONFTEMP-merged" "$CONFTEMP" -else - echo "Target kubeconfig $KUBECONFIG doesn't exist. Importing temp as is." -fi -mv "$CONFTEMP" "$KUBECONFIG" diff --git a/bin/y-kustomize-traverse b/bin/y-kustomize-traverse new file mode 100755 index 00000000..6e23a6fa --- /dev/null +++ b/bin/y-kustomize-traverse @@ -0,0 +1,8 @@ +#!/bin/sh +[ -z "$DEBUG" ] || set -x +set -e +YBIN="$(dirname $0)" + +version=$(y-bin-download $YBIN/y-bin.runner.yaml kustomize-traverse) + +y-kustomize-traverse-v${version}-bin "$@" || exit $? diff --git a/bin/y-registry-config b/bin/y-registry-config index 284198a3..bc96b448 100755 --- a/bin/y-registry-config +++ b/bin/y-registry-config @@ -25,14 +25,20 @@ YSTACK_PROD_REGISTRY=europe-west3-docker.pkg.dev YSTACK_PROD_REGISTRY_TEST_IMAGE YSTACK_PROD_REGISTRY_PROTOCOL="https" [ "$YSTACK_PROD_REGISTRY" != prod-registry.ystack.svc.cluster.local ] || [ "$YSTACK_PROD_REGISTRY_INSECURE" = "false" ] || YSTACK_PROD_REGISTRY_PROTOCOL="http" +# ClusterIPs are fixed via builds-registry-magic-numbers.yaml and prod-registry-magic-numbers.yaml. +# Using IPs instead of hostnames avoids needing /etc/hosts hacks on the node. +YSTACK_HOME="$(cd "$(dirname "$0")/.." && pwd)" +BUILDS_REGISTRY_IP=$(y-yq '.spec.clusterIP' "$YSTACK_HOME/k3s/60-builds-registry/builds-registry-magic-numbers.yaml") +PROD_REGISTRY_IP=$(y-yq '.spec.clusterIP' "$YSTACK_HOME/k3s/61-prod-registry/prod-registry-magic-numbers.yaml") + cat <&2; exit 1; } + echo "[mc] versitygw bucket $BUCKET_NAME" + until mc alias set s3 "$S3_ENDPOINT" "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY" 2>/dev/null; do + sleep 2 + done + mc mb --ignore-existing "s3/$BUCKET_NAME" + # Impl-agnostic: SSA-upserts a consumer-facing Secret (named via + # yolean.se/secret-name) carrying endpoint + bucket + credentials so + # downstream Deployments can mount one Secret instead of resolving + # endpoint and bucket name independently. + containers: + - name: secret + image: ghcr.io/yolean/curl:8.18.0@sha256:d94d07ba9e7d6de898b6d96c1a072f6f8266c687af78a74f380087a0addf5d17 + env: + - name: NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: BUCKET_NAME_TEMPLATE + valueFrom: + fieldRef: + fieldPath: metadata.annotations['yolean.se/bucket-name'] + - name: SECRET_NAME + valueFrom: + fieldRef: + fieldPath: metadata.annotations['yolean.se/secret-name'] + - name: S3_ENDPOINT + valueFrom: + secretKeyRef: + name: bucket + key: endpoint + - name: AWS_ACCESS_KEY_ID + valueFrom: + secretKeyRef: + name: bucket + key: accesskey + - name: AWS_SECRET_ACCESS_KEY + valueFrom: + secretKeyRef: + name: bucket + key: secretkey + command: + - sh + - -ce + - | + BUCKET_NAME=$(eval "echo \"$BUCKET_NAME_TEMPLATE\"") + [ -n "$SECRET_NAME" ] || { echo "yolean.se/secret-name annotation is required" >&2; exit 1; } + TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + curl -fsS \ + --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/apply-patch+yaml" \ + -X PATCH \ + "https://kubernetes.default.svc/api/v1/namespaces/${NAMESPACE}/secrets/${SECRET_NAME}?fieldManager=setup-bucket&force=true" \ + --data-binary @- </dev/null; do - sleep 2 - done - mc mb --ignore-existing s3/$BUCKET_NAME - restartPolicy: Never - backoffLimit: 10 diff --git a/blobs/minio/y-s3-api-service.yaml b/blobs/minio/y-s3-api-service.yaml index 01f01d25..4696312e 100644 --- a/blobs/minio/y-s3-api-service.yaml +++ b/blobs/minio/y-s3-api-service.yaml @@ -7,5 +7,5 @@ spec: app: minio ports: - name: http - port: 80 + port: 9000 targetPort: 9000 diff --git a/blobs/versitygw/y-s3-api-service.yaml b/blobs/versitygw/y-s3-api-service.yaml index ed5e7148..8d84dc24 100644 --- a/blobs/versitygw/y-s3-api-service.yaml +++ b/blobs/versitygw/y-s3-api-service.yaml @@ -7,5 +7,5 @@ spec: app: versitygw ports: - name: http - port: 80 + port: 9000 targetPort: 7070 diff --git a/cluster-configs/local-docker/y-cluster-provision.yaml b/cluster-configs/local-docker/y-cluster-provision.yaml new file mode 100644 index 00000000..e78eaaa7 --- /dev/null +++ b/cluster-configs/local-docker/y-cluster-provision.yaml @@ -0,0 +1,36 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Yolean/y-cluster/main/pkg/provision/schema/docker.schema.json +# +# Local development cluster on the docker provider. +# y-cluster v0.3.3 docker provider takes the same PortForwards shape +# as qemu (each entry binds the host port via Docker port mapping). +# We list the four ystack acceptance ports explicitly because +# PortForwards replaces the y-cluster default wholesale when set: +# 6443 (kubectl API), 80/443 (Gateway/HTTPS), 8944 (in-cluster +# y-kustomize via ServiceLB; see cluster-configs/local-qemu for the +# canonical hostname rationale). +provider: docker +context: local +name: local + +portForwards: +- {host: "6443", guest: "6443"} +- {host: "80", guest: "80"} +- {host: "443", guest: "443"} +- {host: "8944", guest: "8944"} + +# Mirror in-cluster registry hostnames at the magic ClusterIPs that +# k3s/{60-builds-registry,61-prod-registry}/*-magic-numbers.yaml pin +# (and that y-cluster-validate-ystack asserts as `*-registry clusterIP` +# checks). Without these mirrors, containerd on the node can't resolve +# *.svc.cluster.local at image-pull time -- pods that reference +# prod-registry.ystack.svc.cluster.local/yolean/... would hit +# ImagePullBackOff. Same magic IPs as local-qemu (the mirror is a +# node-side concern, not a provider-side one). +registries: + mirrors: + builds-registry.ystack.svc.cluster.local: + endpoint: + - http://10.43.0.50 + prod-registry.ystack.svc.cluster.local: + endpoint: + - http://10.43.0.51 diff --git a/cluster-configs/local-qemu/y-cluster-provision.yaml b/cluster-configs/local-qemu/y-cluster-provision.yaml new file mode 100644 index 00000000..147d1bcf --- /dev/null +++ b/cluster-configs/local-qemu/y-cluster-provision.yaml @@ -0,0 +1,38 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Yolean/y-cluster/main/pkg/provision/schema/qemu.schema.json +# +# Local development cluster on the qemu provider. +# Adds 8944 to the y-cluster default 6443/80/443 forwards so the +# in-cluster y-kustomize LoadBalancer Service is reachable from the +# host: ServiceLB binds 0.0.0.0:8944 on the node, qemu hostfwd routes +# the host's 127.0.0.1:8944 to it. /etc/hosts maps +# `y-kustomize -> 127.0.0.1` (via the y-kustomize HTTPRoute hostname, +# discovered by y-k8s-ingress-hosts), so kustomize-build's fetches of +# http://y-kustomize:8944/v1/... resolve to the in-cluster Deployment. +provider: qemu +context: local +name: local + +# y-cluster default is 6443/80/443; this list replaces it wholesale. +portForwards: +- {host: "6443", guest: "6443"} +- {host: "80", guest: "80"} +- {host: "443", guest: "443"} +- {host: "8944", guest: "8944"} + +# Mirror in-cluster registry hostnames at the magic ClusterIPs that +# k3s/{60-builds-registry,61-prod-registry}/*-magic-numbers.yaml pin +# (and that y-cluster-validate-ystack asserts as `*-registry clusterIP` +# checks). Without these mirrors, containerd on the node can't resolve +# *.svc.cluster.local at image-pull time -- pods that reference +# prod-registry.ystack.svc.cluster.local/yolean/... would hit +# ImagePullBackOff. ystack's own acceptance pulls only via the kubectl +# API proxy and via in-cluster buildkit, so the gap is invisible here; +# checkit and any real-workload consumer needs this. +registries: + mirrors: + builds-registry.ystack.svc.cluster.local: + endpoint: + - http://10.43.0.50 + prod-registry.ystack.svc.cluster.local: + endpoint: + - http://10.43.0.51 diff --git a/cue.mod/module.cue b/cue.mod/module.cue new file mode 100644 index 00000000..10e646fd --- /dev/null +++ b/cue.mod/module.cue @@ -0,0 +1,4 @@ +module: "yolean.se/ystack" +language: { + version: "v0.16.0" +} diff --git a/e2e/agents-clusterautomation-acceptance-linux-amd64.sh b/e2e/agents-clusterautomation-acceptance-linux-amd64.sh index 713d47fe..781542b0 100755 --- a/e2e/agents-clusterautomation-acceptance-linux-amd64.sh +++ b/e2e/agents-clusterautomation-acceptance-linux-amd64.sh @@ -24,11 +24,49 @@ echo "$PATH" set -eo pipefail +CONFIG=cluster-configs/local-docker + +# Host reachability flows from y-cluster's yolean.se/dns-hint-ip +# annotation on the installed GatewayClass: when guest:80 is in +# PortForwards (qemu and docker default), provision stamps +# 127.0.0.1 there, and y-k8s-ingress-hosts walks +# Gateway -> gatewayClassName -> GatewayClass annotation to find +# it. No env var, no per-cluster operator setup. + +KEEP_ON_FAILURE=false +while [ $# -gt 0 ]; do + case "$1" in + --keep-on-failure) KEEP_ON_FAILURE=true; shift ;; + *) echo "Unknown flag: $1" >&2; exit 1 ;; + esac +done + cleanup() { - local provisioner - provisioner=$(y-cluster-local-detect 2>/dev/null) || return 0 - echo "# Cleaning up $provisioner cluster ..." - y-cluster-provision-$provisioner --teardown || true + local rc=$? + if [ "$KEEP_ON_FAILURE" = "true" ] && [ "$rc" -ne 0 ]; then + echo "# Acceptance failed (rc=$rc); cluster left up for inspection." + echo "# Manual cleanup: y-cluster teardown -c $CONFIG" + return + fi + # Default: teardown on every EXIT (success or failure). + # FUTURE: the default is intended to become "keep cluster on + # failure for a configurable number of minutes, then teardown" -- + # a window for post-mortem inspection without leaving stale VMs + # around forever. --keep-on-failure is the manual opt-in until + # that timed-keep mode lands. + echo "# Cleaning up cluster ..." + y-cluster teardown -c "$CONFIG" || true # y-script-lint:disable=or-true # best-effort cleanup in EXIT trap + # The acceptance flow uses the in-cluster y-kustomize Deployment via + # the qemu hostfwd 8944. If 8944 is still bound on the host after + # teardown, a leftover host-local `y-cluster serve` from a downstream + # user's run (or a developer poking at bin/acceptance-y-kustomize-local) + # would block the next provision's hostfwd from binding. Probe and + # best-effort stop -- not fatal if the binding is something else + # entirely. + if ss -lnt 'sport = :8944' 2>/dev/null | grep -q ':8944 '; then + echo "# Port 8944 still in use; attempting host-local y-cluster serve stop" + y-cluster serve stop || true # y-script-lint:disable=or-true # best-effort + fi } trap cleanup EXIT @@ -36,8 +74,77 @@ trap cleanup EXIT cleanup -ss -tlnp 2>/dev/null | grep -qE ':80 |:443 ' && echo "port 80 and 443 must be available for local cluster to bind to" && exit 1 -y-cluster-provision-k3d +# --- provision (no converge) --- +# +# y-cluster v0.3.5 added a host-side /readyz probe between the +# in-container kubeconfig appearing and "k3s ready" being declared, +# closing the docker port-forward race that made the next step +# (envoy-gateway install via kubectl apply) fail with "dial tcp +# 127.0.0.1:6443: connect: connection refused" (Yolean/y-cluster#12). +# v0.3.6 fixed a separate silent-drop in the docker provider where +# moby v1.54+ sent every PortBinding's HostIp as the empty string +# (zero netip.Addr) and Docker Engine 28 dropped them all, so +# NetworkSettings.Ports came back empty (Yolean/y-cluster#15). +# v0.3.7 mirrors PortBindings into Config.ExposedPorts to match +# `docker run -p` semantics (Yolean/y-cluster#17), addressing the +# remaining ubuntu-latest case where Engine 28 still dropped +# bindings even after the HostIP fix (Yolean/y-cluster#16). +y-cluster provision -c "$CONFIG" + +# Label nodes that don't yet have a cluster identity. Selector form +# avoids overwriting an existing label on a misclaimed cluster. +kubectl --context=local label nodes -l '!yolean.se/cluster' yolean.se/cluster=local + +# --- gateway: just the consumer Gateway resource (CRDs + GatewayClass come from y-cluster provision) --- + +echo "" +echo "# ystack Gateway resource" +y-cluster yconverge --context=local -k k3s/20-gateway/ + +# --- y-kustomize served by the in-cluster Deployment (no host-local serve) --- +# +# k3s/29-y-kustomize applies a LoadBalancer Service on port 8944 that +# ServiceLB binds on the node. cluster-configs/local-qemu/y-cluster-provision.yaml +# adds host:8944 -> guest:8944 to PortForwards, so the host reaches the +# in-cluster Deployment via 127.0.0.1:8944. /etc/hosts maps +# `y-kustomize -> 127.0.0.1` (y-k8s-ingress-hosts walks the dummy +# y-kustomize HTTPRoute hostname). +# +# Downstream users that want to run y-cluster serve locally can do so +# via `y-cluster serve -c y-kustomize/` -- see +# bin/acceptance-y-kustomize-local for the standalone test of that path. + +# --- progressive convergence: proves DAG resolves deps without include/exclude --- + +echo "" +echo "# Phase 1: base platform (registry + y-kustomize serving)" +y-cluster yconverge --context=local -k k3s/60-builds-registry/ + +echo "" +echo "# Phase 2: kafka stack (transitive deps through y-kustomize)" +y-cluster yconverge --context=local -k k3s/40-kafka/ + +echo "" +echo "# Phase 3: build infra" +y-cluster yconverge --context=local -k k3s/62-buildkit/ + +echo "" +echo "# Phase 4: prod registry" +y-cluster yconverge --context=local -k k3s/61-prod-registry/ + +echo "" +echo "# Phase 5: monitoring (independent branch)" +y-cluster yconverge --context=local -k k3s/50-monitoring/ + +echo "" +echo "# Phase 6: idempotency proof -- re-converge everything" +y-cluster yconverge --context=local -k k3s/62-buildkit/ +y-cluster yconverge --context=local -k k3s/50-monitoring/ +y-cluster yconverge --context=local -k k3s/61-prod-registry/ +y-cluster yconverge --context=local -k k3s/40-kafka/ + +echo "" +echo "# Phase 7: validate the complete stack" y-cluster-validate-ystack --context=local echo "Acceptance tests completed" diff --git a/e2e/agents-clusterautomation-acceptance-osx-amd64.sh b/e2e/agents-clusterautomation-acceptance-osx-amd64.sh index 527a08a7..c8aa1cf6 100755 --- a/e2e/agents-clusterautomation-acceptance-osx-amd64.sh +++ b/e2e/agents-clusterautomation-acceptance-osx-amd64.sh @@ -20,7 +20,7 @@ if [[ "$ENV_IS_CLEAN" != "true" ]]; then PATH="/usr/bin:/bin:/usr/sbin:/sbin" \ ENV_IS_CLEAN=true \ /bin/zsh -ilc "$SCRIPT_PATH $*" - + exit 0 fi @@ -29,11 +29,15 @@ echo "$PATH" set -eo pipefail +# macOS path uses the docker provider via Docker Desktop (no KVM). +# Multipass and Lima provisioners aren't supported by y-cluster yet; once +# they ship in the binary we can either add cluster-configs/local-{lima,multipass} +# or run them through the same docker config here. +CONFIG=cluster-configs/local-docker + cleanup() { - local provisioner - provisioner=$(y-cluster-local-detect 2>/dev/null) || return 0 - echo "# Cleaning up $provisioner cluster ..." - y-cluster-provision-$provisioner --teardown || true + echo "# Cleaning up cluster ..." + y-cluster teardown -c "$CONFIG" || true # y-script-lint:disable=or-true # best-effort cleanup in EXIT trap } trap cleanup EXIT @@ -43,15 +47,13 @@ cleanup lsof -iTCP:80 -iTCP:443 -sTCP:LISTEN -P -n >/dev/null 2>&1 && echo "port 80 and 443 must be available for local cluster vm to bind to" && exit 1 -y-cluster-provision-k3d -y-cluster-validate-ystack --context=local +y-cluster provision -c "$CONFIG" -cleanup -y-cluster-provision-lima -y-cluster-validate-ystack --context=local +# Label nodes that don't yet have a cluster identity. +kubectl --context=local label nodes -l '!yolean.se/cluster' yolean.se/cluster=local + +y-cluster yconverge --context=local -k k3s/20-gateway/ -cleanup -y-cluster-provision-multipass y-cluster-validate-ystack --context=local echo "Acceptance tests completed" diff --git a/e2e/agents-clusterautomation-acceptance-osx-arm64.sh b/e2e/agents-clusterautomation-acceptance-osx-arm64.sh index 3491ab92..ada320ad 100755 --- a/e2e/agents-clusterautomation-acceptance-osx-arm64.sh +++ b/e2e/agents-clusterautomation-acceptance-osx-arm64.sh @@ -7,10 +7,6 @@ if [[ "$ENV_IS_CLEAN" != "true" ]]; then echo " Mirroring a fresh interactive terminal..." # We pass a basic PATH so path_helper and your scripts have a starting point. - # We use -ilc: - # -l: Login (loads /etc/zprofile, ~/.zprofile) - # -i: Interactive (bypasses '[[ -z "$PS1" ]] && return' guards) - # -c: Command (executes this script) exec env -i \ HOME="$HOME" \ USER="$USER" \ @@ -29,20 +25,39 @@ echo "$PATH" set -eo pipefail +# macOS arm64: Docker Desktop runs amd64 images via emulation today, so +# this test exercises the same flow as -osx-amd64. +CONFIG=cluster-configs/local-docker + cleanup() { - local provisioner - provisioner=$(y-cluster-local-detect 2>/dev/null) || return 0 - echo "# Cleaning up $provisioner cluster ..." - y-cluster-provision-$provisioner --teardown || true + echo "# Cleaning up cluster ..." + y-cluster teardown -c "$CONFIG" || true # y-script-lint:disable=or-true # best-effort cleanup in EXIT trap } trap cleanup EXIT -# --- acceptance tests begin here --- - cleanup lsof -iTCP:80 -iTCP:443 -sTCP:LISTEN -P -n >/dev/null 2>&1 && echo "port 80 and 443 must be available for local cluster vm to bind to" && exit 1 -y-cluster-provision-k3d + +y-cluster provision -c "$CONFIG" + +kubectl --context=local label nodes -l '!yolean.se/cluster' yolean.se/cluster=local + +y-cluster yconverge --context=local -k k3s/20-gateway/ + +# Progressive convergence +y-cluster yconverge --context=local -k k3s/60-builds-registry/ +y-cluster yconverge --context=local -k k3s/40-kafka/ +y-cluster yconverge --context=local -k k3s/62-buildkit/ +y-cluster yconverge --context=local -k k3s/61-prod-registry/ +y-cluster yconverge --context=local -k k3s/50-monitoring/ + +# Idempotency +y-cluster yconverge --context=local -k k3s/62-buildkit/ +y-cluster yconverge --context=local -k k3s/50-monitoring/ +y-cluster yconverge --context=local -k k3s/61-prod-registry/ +y-cluster yconverge --context=local -k k3s/40-kafka/ + y-cluster-validate-ystack --context=local echo "Acceptance tests completed" diff --git a/gateway/gateway.yaml b/gateway/gateway.yaml index f924565e..dc069e1b 100644 --- a/gateway/gateway.yaml +++ b/gateway/gateway.yaml @@ -5,12 +5,11 @@ metadata: labels: yolean.se/module-part: gateway spec: - gatewayClassName: traefik + gatewayClassName: y-cluster listeners: - name: http protocol: HTTP - # Traefik helm chart's web entrypoint container port (exposed as 80 via Service) - port: 8000 + port: 80 allowedRoutes: namespaces: from: All diff --git a/k3s/00-namespace-ystack/yconverge.cue b/k3s/00-namespace-ystack/yconverge.cue new file mode 100644 index 00000000..e78dc7da --- /dev/null +++ b/k3s/00-namespace-ystack/yconverge.cue @@ -0,0 +1,7 @@ +package namespace_ystack + +import "yolean.se/ystack/yconverge/verify" + +step: verify.#Step & { + checks: [] +} diff --git a/k3s/01-namespace-blobs/yconverge.cue b/k3s/01-namespace-blobs/yconverge.cue new file mode 100644 index 00000000..2be32ca0 --- /dev/null +++ b/k3s/01-namespace-blobs/yconverge.cue @@ -0,0 +1,7 @@ +package namespace_blobs + +import "yolean.se/ystack/yconverge/verify" + +step: verify.#Step & { + checks: [] +} diff --git a/k3s/02-namespace-kafka/yconverge.cue b/k3s/02-namespace-kafka/yconverge.cue new file mode 100644 index 00000000..5ee5cc2a --- /dev/null +++ b/k3s/02-namespace-kafka/yconverge.cue @@ -0,0 +1,7 @@ +package namespace_kafka + +import "yolean.se/ystack/yconverge/verify" + +step: verify.#Step & { + checks: [] +} diff --git a/k3s/03-namespace-monitoring/yconverge.cue b/k3s/03-namespace-monitoring/yconverge.cue new file mode 100644 index 00000000..dfe009ca --- /dev/null +++ b/k3s/03-namespace-monitoring/yconverge.cue @@ -0,0 +1,7 @@ +package namespace_monitoring + +import "yolean.se/ystack/yconverge/verify" + +step: verify.#Step & { + checks: [] +} diff --git a/k3s/09-y-kustomize-secrets-init/y-kustomize.blobs.setup-bucket-job.yaml b/k3s/09-y-kustomize-secrets-init/y-kustomize.blobs.setup-bucket-job.yaml deleted file mode 100644 index 364012e9..00000000 --- a/k3s/09-y-kustomize-secrets-init/y-kustomize.blobs.setup-bucket-job.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: y-kustomize.blobs.setup-bucket-job -type: Opaque diff --git a/k3s/09-y-kustomize-secrets-init/y-kustomize.kafka.setup-topic-job.yaml b/k3s/09-y-kustomize-secrets-init/y-kustomize.kafka.setup-topic-job.yaml deleted file mode 100644 index 66ab2c42..00000000 --- a/k3s/09-y-kustomize-secrets-init/y-kustomize.kafka.setup-topic-job.yaml +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: y-kustomize.kafka.setup-topic-job -type: Opaque diff --git a/k3s/10-gateway-api/traefik-gateway-provider.yaml b/k3s/10-gateway-api/traefik-gateway-provider.yaml deleted file mode 100644 index c14c49ea..00000000 --- a/k3s/10-gateway-api/traefik-gateway-provider.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: helm.cattle.io/v1 -kind: HelmChartConfig -metadata: - name: traefik - namespace: kube-system -spec: - valuesContent: |- - providers: - kubernetesGateway: - enabled: true diff --git a/k3s/11-monitoring-operator/kustomization.yaml b/k3s/11-monitoring-operator/kustomization.yaml index fe1e4dfd..682dcdda 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/11-monitoring-operator/yconverge.cue b/k3s/11-monitoring-operator/yconverge.cue new file mode 100644 index 00000000..5cd6a67d --- /dev/null +++ b/k3s/11-monitoring-operator/yconverge.cue @@ -0,0 +1,17 @@ +package monitoring_operator + +import ( + "yolean.se/ystack/yconverge/verify" + "yolean.se/ystack/k3s/03-namespace-monitoring:namespace_monitoring" +) + +_dep_ns: namespace_monitoring.step + +step: verify.#Step & { + checks: [{ + kind: "rollout" + resource: "deploy/prometheus-operator" + namespace: "default" + timeout: "120s" + }] +} diff --git a/k3s/20-gateway/yconverge.cue b/k3s/20-gateway/yconverge.cue new file mode 100644 index 00000000..3bf5f310 --- /dev/null +++ b/k3s/20-gateway/yconverge.cue @@ -0,0 +1,30 @@ +package gateway + +import ( + "yolean.se/ystack/yconverge/verify" + "yolean.se/ystack/k3s/00-namespace-ystack:namespace_ystack" +) + +// Gateway API CRDs and the `y-cluster` GatewayClass come from +// y-cluster provision (Envoy Gateway is bundled). This base only +// applies the consumer Gateway resource that references the class. + +_dep_ns: namespace_ystack.step + +step: verify.#Step & { + checks: [ + { + kind: "exec" + command: "y-k8s-ingress-hosts --context=$CONTEXT -write || echo 'WARNING: /etc/hosts update failed (may need manual sudo)'" + timeout: "10s" + description: "update /etc/hosts for gateway routes" + }, + { + kind: "wait" + resource: "Gateway.gateway.networking.k8s.io/ystack" + namespace: "ystack" + for: "condition=Programmed" + timeout: "60s" + }, + ] +} diff --git a/k3s/29-y-kustomize/yconverge.cue b/k3s/29-y-kustomize/yconverge.cue new file mode 100644 index 00000000..71319214 --- /dev/null +++ b/k3s/29-y-kustomize/yconverge.cue @@ -0,0 +1,30 @@ +package y_kustomize + +import "yolean.se/ystack/yconverge/verify" + +// y-kustomize watches secrets via API -- no namespace/Gateway +// dependencies. The /health probe resolves "y-kustomize" via the +// /etc/hosts entry written below; the address is host-loopback when +// `y-cluster serve` runs on the host bound to 127.0.0.1:8944, or +// the cluster ingress IP when the in-cluster Deployment is up. + +step: verify.#Step & { + checks: [ + // /etc/hosts must be updated before the /health probe -- the probe + // resolves "y-kustomize" via the file we just wrote. + { + kind: "exec" + command: "y-k8s-ingress-hosts --context=$CONTEXT -write || echo 'WARNING: /etc/hosts update failed (may need manual sudo)'" + timeout: "10s" + description: "update /etc/hosts for y-kustomize HTTPRoute" + }, + // /health is reachable whether the in-cluster Deployment is running + // OR `y-cluster serve` runs on the host bound to 127.0.0.1:8944. + { + kind: "exec" + command: "for i in $(seq 1 30); do curl -sSf --max-time 2 http://y-kustomize:8944/health >/dev/null && break; sleep 2; done && curl -sSf --max-time 5 http://y-kustomize:8944/health >/dev/null" + timeout: "60s" + description: "y-kustomize /health responds (in-cluster Deployment or host-local y-cluster serve)" + }, + ] +} diff --git a/k3s/30-blobs-minio-disabled/yconverge.cue b/k3s/30-blobs-minio-disabled/yconverge.cue new file mode 100644 index 00000000..f8ba675e --- /dev/null +++ b/k3s/30-blobs-minio-disabled/yconverge.cue @@ -0,0 +1,7 @@ +package blobs_minio_disabled + +import "yolean.se/ystack/yconverge/verify" + +step: verify.#Step & { + checks: [] +} diff --git a/k3s/30-blobs/yconverge.cue b/k3s/30-blobs/yconverge.cue new file mode 100644 index 00000000..0caad489 --- /dev/null +++ b/k3s/30-blobs/yconverge.cue @@ -0,0 +1,17 @@ +package blobs + +import ( + "yolean.se/ystack/yconverge/verify" + "yolean.se/ystack/k3s/01-namespace-blobs:namespace_blobs" +) + +_dep_ns: namespace_blobs.step + +step: verify.#Step & { + checks: [{ + kind: "rollout" + resource: "deploy/versitygw" + namespace: "blobs" + timeout: "60s" + }] +} diff --git a/k3s/31-blobs-y-kustomize/kustomization.yaml b/k3s/31-blobs-y-kustomize/kustomization.yaml new file mode 100644 index 00000000..291a8e46 --- /dev/null +++ b/k3s/31-blobs-y-kustomize/kustomization.yaml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=https://json.schemastore.org/kustomization.json +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: ystack +resources: +- ../../blobs-versitygw/setup-bucket-prep-y-kustomize +- ../../blobs-versitygw/setup-bucket-y-kustomize diff --git a/k3s/31-blobs-y-kustomize/yconverge.cue b/k3s/31-blobs-y-kustomize/yconverge.cue new file mode 100644 index 00000000..9cc3bf39 --- /dev/null +++ b/k3s/31-blobs-y-kustomize/yconverge.cue @@ -0,0 +1,20 @@ +package blobs_y_kustomize + +import ( + "yolean.se/ystack/yconverge/verify" + "yolean.se/ystack/k3s/30-blobs:blobs" + "yolean.se/ystack/k3s/29-y-kustomize:y_kustomize" +) + +_dep_blobs: blobs.step +_dep_kustomize: y_kustomize.step + +step: verify.#Step & { + // y-kustomize watches secrets via API — no restart needed. + checks: [{ + kind: "exec" + command: "curl -sSf --connect-timeout 2 --max-time 5 http://y-kustomize:8944/v1/blobs/setup-bucket-job/base-for-annotations.yaml >/dev/null" + timeout: "30s" + description: "y-kustomize serving blobs bases" + }] +} diff --git a/k3s/40-kafka/kustomization.yaml b/k3s/40-kafka/kustomization.yaml index 10195997..5d921e78 100644 --- a/k3s/40-kafka/kustomization.yaml +++ b/k3s/40-kafka/kustomization.yaml @@ -1,7 +1,9 @@ # yaml-language-server: $schema=https://json.schemastore.org/kustomization.json apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization +namespace: kafka resources: - ../../kafka/base +- ../../kafka/topic-job-rbac components: - ../../kafka/redpanda-image diff --git a/k3s/40-kafka/yconverge.cue b/k3s/40-kafka/yconverge.cue new file mode 100644 index 00000000..05d10f43 --- /dev/null +++ b/k3s/40-kafka/yconverge.cue @@ -0,0 +1,25 @@ +package kafka + +import ( + "yolean.se/ystack/yconverge/verify" + "yolean.se/ystack/k3s/02-namespace-kafka:namespace_kafka" +) + +_dep_ns: namespace_kafka.step + +step: verify.#Step & { + 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" + }, + ] +} diff --git a/k3s/09-y-kustomize-secrets-init/kustomization.yaml b/k3s/41-kafka-y-kustomize/kustomization.yaml similarity index 67% rename from k3s/09-y-kustomize-secrets-init/kustomization.yaml rename to k3s/41-kafka-y-kustomize/kustomization.yaml index 74657401..a2152022 100644 --- a/k3s/09-y-kustomize-secrets-init/kustomization.yaml +++ b/k3s/41-kafka-y-kustomize/kustomization.yaml @@ -3,5 +3,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: ystack resources: -- y-kustomize.blobs.setup-bucket-job.yaml -- y-kustomize.kafka.setup-topic-job.yaml +- ../../kafka/setup-topic-prep-y-kustomize +- ../../kafka/setup-topic-y-kustomize diff --git a/k3s/41-kafka-y-kustomize/yconverge.cue b/k3s/41-kafka-y-kustomize/yconverge.cue new file mode 100644 index 00000000..6c419483 --- /dev/null +++ b/k3s/41-kafka-y-kustomize/yconverge.cue @@ -0,0 +1,28 @@ +package kafka_y_kustomize + +import ( + "yolean.se/ystack/yconverge/verify" + "yolean.se/ystack/k3s/40-kafka:kafka" + "yolean.se/ystack/k3s/29-y-kustomize:y_kustomize" +) + +_dep_kafka: kafka.step +_dep_kustomize: y_kustomize.step + +step: verify.#Step & { + // y-kustomize watches secrets via API — no restart needed. + checks: [ + { + kind: "exec" + command: "curl -sSf --connect-timeout 2 --max-time 5 http://y-kustomize:8944/v1/kafka/setup-topic-job/base-for-annotations.yaml >/dev/null" + timeout: "30s" + description: "y-kustomize serving kafka bases" + }, + { + kind: "exec" + command: "curl -sSf --connect-timeout 2 --max-time 5 http://y-kustomize:8944/v1/blobs/setup-bucket-job/base-for-annotations.yaml >/dev/null" + timeout: "30s" + description: "y-kustomize serving blobs bases" + }, + ] +} diff --git a/k3s/50-monitoring/yconverge.cue b/k3s/50-monitoring/yconverge.cue new file mode 100644 index 00000000..9b8a3a9f --- /dev/null +++ b/k3s/50-monitoring/yconverge.cue @@ -0,0 +1,17 @@ +package monitoring + +import ( + "yolean.se/ystack/yconverge/verify" + "yolean.se/ystack/k3s/11-monitoring-operator:monitoring_operator" +) + +_dep_operator: monitoring_operator.step + +step: verify.#Step & { + checks: [{ + kind: "rollout" + resource: "deploy/kube-state-metrics" + namespace: "monitoring" + timeout: "60s" + }] +} diff --git a/k3s/60-builds-registry/kustomization.yaml b/k3s/60-builds-registry/kustomization.yaml index d54ad388..095132a3 100644 --- a/k3s/60-builds-registry/kustomization.yaml +++ b/k3s/60-builds-registry/kustomization.yaml @@ -11,6 +11,7 @@ resources: - ../../registry/generic - ../../registry/gateway - ../../blobs-versitygw/defaultsecret +- ../../registry/builds-prep - ../../registry/builds-bucket - ../../registry/builds-topic diff --git a/k3s/60-builds-registry/yconverge.cue b/k3s/60-builds-registry/yconverge.cue new file mode 100644 index 00000000..553f83f6 --- /dev/null +++ b/k3s/60-builds-registry/yconverge.cue @@ -0,0 +1,29 @@ +package builds_registry + +import ( + "yolean.se/ystack/yconverge/verify" + "yolean.se/ystack/k3s/31-blobs-y-kustomize:blobs_y_kustomize" + "yolean.se/ystack/k3s/41-kafka-y-kustomize:kafka_y_kustomize" + "yolean.se/ystack/k3s/29-y-kustomize:y_kustomize" +) + +_dep_blobs: blobs_y_kustomize.step +_dep_kafka: kafka_y_kustomize.step +_dep_kustomize: y_kustomize.step + +step: verify.#Step & { + 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/yconverge.cue b/k3s/61-prod-registry/yconverge.cue new file mode 100644 index 00000000..5285b073 --- /dev/null +++ b/k3s/61-prod-registry/yconverge.cue @@ -0,0 +1,12 @@ +package prod_registry + +import ( + "yolean.se/ystack/yconverge/verify" + "yolean.se/ystack/k3s/00-namespace-ystack:namespace_ystack" +) + +_dep_ns: namespace_ystack.step + +step: verify.#Step & { + checks: [] +} diff --git a/k3s/62-buildkit/yconverge.cue b/k3s/62-buildkit/yconverge.cue new file mode 100644 index 00000000..f8709636 --- /dev/null +++ b/k3s/62-buildkit/yconverge.cue @@ -0,0 +1,17 @@ +package buildkit + +import ( + "yolean.se/ystack/yconverge/verify" + "yolean.se/ystack/k3s/60-builds-registry:builds_registry" +) + +_dep_registry: builds_registry.step + +step: verify.#Step & { + checks: [{ + kind: "exec" + command: "kubectl --context=$CONTEXT -n ystack get statefulset buildkitd" + timeout: "10s" + description: "buildkitd statefulset exists" + }] +} diff --git a/k3s/README.md b/k3s/README.md index 64c67132..50bc45a5 100644 --- a/k3s/README.md +++ b/k3s/README.md @@ -9,8 +9,9 @@ Converge principles: `1*` bases use `--server-side=true --force-conflicts` (required for large CRDs). - Between digit groups (0→1, 1→2, etc.), wait for all deployment rollouts. - After `1*`, validate that CRDs are registered and served. -- Before `6*`, verify [y-kustomize api](../y-kustomize/openapi/openapi.yaml) serves real content - (secrets from `3*` and `4*` need time to propagate to mounted volumes). +- Before `6*`, verify y-kustomize serves real content via + `curl http://y-kustomize:8944/openapi.yaml` (live spec from y-cluster serve; + secrets from `3*` and `4*` need time to propagate to the watch). Each base is applied with `kubectl apply -k` — no label selectors, no multi-pass. diff --git a/kafka/setup-topic-prep-y-kustomize/base-for-annotations.yaml b/kafka/setup-topic-prep-y-kustomize/base-for-annotations.yaml new file mode 100644 index 00000000..7d5024d0 --- /dev/null +++ b/kafka/setup-topic-prep-y-kustomize/base-for-annotations.yaml @@ -0,0 +1,35 @@ +# Per-namespace prerequisites for the setup-topic Job served at +# /v1/kafka/setup-topic-job/. Pulled by site-apply bases at +# http://y-kustomize:8944/v1/kafka/setup-topic-prep/base-for-annotations.yaml +# once per consumer namespace. +# +# Mirrors blobs-versitygw/setup-bucket-prep-y-kustomize/. We don't ship a +# `bootstrap` Secret here -- topics in sitevalues already carry their +# bootstrap address via site-chart's settings-sitevalues template, and +# topics not in sitevalues can pass bootstrap via the +# yolean.se/kafka-bootstrap annotation on their per-topic kustomization. +apiVersion: v1 +kind: ServiceAccount +metadata: + name: setup-topic +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: setup-topic +rules: +- apiGroups: [""] + resources: ["secrets"] + verbs: ["create", "get", "update"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: setup-topic +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: setup-topic +subjects: +- kind: ServiceAccount + name: setup-topic diff --git a/kafka/setup-topic-prep-y-kustomize/kustomization.yaml b/kafka/setup-topic-prep-y-kustomize/kustomization.yaml new file mode 100644 index 00000000..cf9e386f --- /dev/null +++ b/kafka/setup-topic-prep-y-kustomize/kustomization.yaml @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=https://json.schemastore.org/kustomization.json +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Produces a Secret y-kustomize.kafka.setup-topic-prep whose data key +# base-for-annotations.yaml is a manifest list (ServiceAccount + Role + +# RoleBinding) for the setup-topic Job's per-namespace prerequisites. +# y-cluster serve picks up this Secret and serves it at +# /v1/kafka/setup-topic-prep/base-for-annotations.yaml. +secretGenerator: +- name: y-kustomize.kafka.setup-topic-prep + options: + disableNameSuffixHash: true + labels: + yolean.se/module-part: y-kustomize + files: + - base-for-annotations.yaml diff --git a/kafka/y-kustomize/y-kustomize-bases/kafka/setup-topic-job/setup-topic-job.yaml b/kafka/setup-topic-y-kustomize/base-for-annotations.yaml similarity index 50% rename from kafka/y-kustomize/y-kustomize-bases/kafka/setup-topic-job/setup-topic-job.yaml rename to kafka/setup-topic-y-kustomize/base-for-annotations.yaml index dd1f0d81..a4d20888 100644 --- a/kafka/y-kustomize/y-kustomize-bases/kafka/setup-topic-job/setup-topic-job.yaml +++ b/kafka/setup-topic-y-kustomize/base-for-annotations.yaml @@ -1,10 +1,4 @@ -apiVersion: v1 -kind: Secret -metadata: - name: kafka-bootstrap -stringData: - broker: y-bootstrap.kafka.svc.cluster.local:9092 ---- +# yaml-language-server: $schema=https://github.com/yannh/kubernetes-json-schema/raw/master/v1.30.7/job.json apiVersion: batch/v1 kind: Job metadata: @@ -23,7 +17,12 @@ spec: retention.ms=-1 yolean.se/kafka-topic-partitions: "1" yolean.se/kafka-topic-replicas: "-1" + yolean.se/kafka-secret-name: "" + yolean.se/setup-job-version: "1" spec: + nodeSelector: + yolean.se/cluster: local + serviceAccountName: setup-topic restartPolicy: Never activeDeadlineSeconds: 3600 containers: @@ -45,6 +44,43 @@ spec: else rpk topic --brokers $KAFKA_BOOTSTRAP create "$TOPIC_NAME" --partitions "$TOPIC_PARTITIONS" --replicas "$TOPIC_REPLICAS" $(config_args --topic-config) fi + # Create consumer-facing secret if yolean.se/kafka-secret-name is set + if [ -n "$SECRET_NAME" ]; then + KUBE=https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT + TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + NS=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace) + CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt + SECRET_JSON=$(cat <&2 + exit 1 + fi + echo "Secret $SECRET_NAME created/updated with bootstrap=$KAFKA_BOOTSTRAP topicName=$TOPIC_NAME" + fi command: - /bin/bash - -cex @@ -69,6 +105,14 @@ spec: valueFrom: fieldRef: fieldPath: metadata.annotations['yolean.se/kafka-topic-replicas'] + - name: SECRET_NAME + valueFrom: + fieldRef: + fieldPath: metadata.annotations['yolean.se/kafka-secret-name'] + - name: SETUP_JOB_VERSION + valueFrom: + fieldRef: + fieldPath: metadata.annotations['yolean.se/setup-job-version'] resources: requests: cpu: 250m diff --git a/kafka/setup-topic-y-kustomize/kustomization.yaml b/kafka/setup-topic-y-kustomize/kustomization.yaml new file mode 100644 index 00000000..0c050772 --- /dev/null +++ b/kafka/setup-topic-y-kustomize/kustomization.yaml @@ -0,0 +1,16 @@ +# yaml-language-server: $schema=https://json.schemastore.org/kustomization.json +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Produces a Secret y-kustomize.kafka.setup-topic-job whose data key +# base-for-annotations.yaml is a Job spec for kafka topic setup. +# y-cluster serve picks up this Secret and serves it at +# /v1/kafka/setup-topic-job/base-for-annotations.yaml. +secretGenerator: +- name: y-kustomize.kafka.setup-topic-job + options: + disableNameSuffixHash: true + labels: + yolean.se/module-part: y-kustomize + files: + - base-for-annotations.yaml diff --git a/kafka/topic-job-rbac/kustomization.yaml b/kafka/topic-job-rbac/kustomization.yaml new file mode 100644 index 00000000..2f137422 --- /dev/null +++ b/kafka/topic-job-rbac/kustomization.yaml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=https://json.schemastore.org/kustomization.json +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- serviceaccount.yaml +- role.yaml +- rolebinding.yaml diff --git a/kafka/topic-job-rbac/role.yaml b/kafka/topic-job-rbac/role.yaml new file mode 100644 index 00000000..187736a5 --- /dev/null +++ b/kafka/topic-job-rbac/role.yaml @@ -0,0 +1,8 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: setup-topic +rules: +- apiGroups: [""] + resources: ["secrets"] + verbs: ["create", "get", "update"] diff --git a/kafka/topic-job-rbac/rolebinding.yaml b/kafka/topic-job-rbac/rolebinding.yaml new file mode 100644 index 00000000..39e76a1a --- /dev/null +++ b/kafka/topic-job-rbac/rolebinding.yaml @@ -0,0 +1,11 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: setup-topic +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: setup-topic +subjects: +- kind: ServiceAccount + name: setup-topic diff --git a/kafka/topic-job-rbac/serviceaccount.yaml b/kafka/topic-job-rbac/serviceaccount.yaml new file mode 100644 index 00000000..1127c231 --- /dev/null +++ b/kafka/topic-job-rbac/serviceaccount.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: setup-topic diff --git a/kafka/validate-topic/kustomization.yaml b/kafka/validate-topic/kustomization.yaml index bcc3c511..84089dc9 100644 --- a/kafka/validate-topic/kustomization.yaml +++ b/kafka/validate-topic/kustomization.yaml @@ -3,6 +3,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: kafka resources: -- http://y-kustomize.ystack.svc.cluster.local/v1/kafka/setup-topic-job/base-for-annotations.yaml +- http://y-kustomize:8944/v1/kafka/setup-topic-job/base-for-annotations.yaml commonAnnotations: yolean.se/kafka-topic-name: y-cluster-validate-ystack + yolean.se/kafka-secret-name: topic-validate-ystack diff --git a/kafka/y-kustomize/kustomization.yaml b/kafka/y-kustomize/kustomization.yaml deleted file mode 100644 index 36b5dd24..00000000 --- a/kafka/y-kustomize/kustomization.yaml +++ /dev/null @@ -1,13 +0,0 @@ -# yaml-language-server: $schema=https://json.schemastore.org/kustomization.json -apiVersion: kustomize.config.k8s.io/v1beta1 -kind: Kustomization -namespace: ystack - -secretGenerator: -- name: y-kustomize.kafka.setup-topic-job - options: - disableNameSuffixHash: true - labels: - yolean.se/module-part: config - files: - - base-for-annotations.yaml=y-kustomize-bases/kafka/setup-topic-job/setup-topic-job.yaml diff --git a/monitoring/TODO.md b/monitoring/TODO.md new file mode 100644 index 00000000..15225f90 --- /dev/null +++ b/monitoring/TODO.md @@ -0,0 +1,25 @@ +# Monitoring infrastructure setup TODO + +Tracks remaining work to fully converge the monitoring stack on vanilla Prometheus v3. +Ref: PR #67 review comments. + +## Converge prerequisite for e2e + +The `httproute prometheus-now` validation check requires the full converge sequence. +Run `y-cluster-converge-ystack --context=local` (or the relevant context) to apply all +steps including `09-prometheus-httproute`. The validate script only asserts state — it +does not create resources. + +## Remaining tasks + +- [ ] Drop `monitoring/prometheus-operator/` once all clusters run vanilla Prometheus +- [ ] Drop `monitoring/kube-state-metrics/` (operator CRD variant) in favor of `kube-state-metrics-now/` +- [ ] Drop `monitoring/node-exporter/node-exporter-podmonitor.yaml` — the PodMonitor CRD + is only used by the operator; vanilla Prometheus discovers via the `metrics` port convention +- [ ] Update `k3s/30-monitoring-operator/` — either remove or gate behind a feature flag +- [ ] Migrate `monitoring/grafana/grafana-service.yaml` annotations (`prometheus.io/scrape`) + to also expose a port named `metrics` for consistency with the pod SD convention +- [ ] Fix `k3s/09-prometheus-httproute/kustomization.yaml` — uses deprecated `bases:` key, + should be `resources:` +- [ ] Add persistent volume for Prometheus data (currently `emptyDir {}`) +- [ ] Wire up Alertmanager to the converge and validate scripts diff --git a/registry/builds-bucket/kustomization.yaml b/registry/builds-bucket/kustomization.yaml index 287618bb..6157dfe5 100644 --- a/registry/builds-bucket/kustomization.yaml +++ b/registry/builds-bucket/kustomization.yaml @@ -4,6 +4,7 @@ kind: Kustomization namespace: ystack namePrefix: builds-registry- resources: -- http://y-kustomize.ystack.svc.cluster.local/v1/blobs/setup-bucket-job/base-for-annotations.yaml +- http://y-kustomize:8944/v1/blobs/setup-bucket-job/base-for-annotations.yaml commonAnnotations: yolean.se/bucket-name: ystack-builds-registry + yolean.se/secret-name: builds-registry-bucket diff --git a/registry/builds-prep/kustomization.yaml b/registry/builds-prep/kustomization.yaml new file mode 100644 index 00000000..0bb0c91c --- /dev/null +++ b/registry/builds-prep/kustomization.yaml @@ -0,0 +1,17 @@ +# yaml-language-server: $schema=https://json.schemastore.org/kustomization.json +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +# Per-namespace prerequisites for the setup-bucket and setup-topic Jobs +# in builds-bucket and builds-topic. Pulled into the ystack namespace +# (via k3s/60-builds-registry/kustomization.yaml) once. Both prep URLs +# carry ServiceAccount + Role + RoleBinding (and bucket-side a default +# `bucket` Secret) that the per-Job kustomizations rely on but do not +# carry themselves (avoids per-Job duplication of cluster-singleton SA +# resources). + +namespace: ystack + +resources: +- http://y-kustomize:8944/v1/blobs/setup-bucket-prep/base-for-annotations.yaml +- http://y-kustomize:8944/v1/kafka/setup-topic-prep/base-for-annotations.yaml diff --git a/registry/builds-topic/kustomization.yaml b/registry/builds-topic/kustomization.yaml index 11322b9c..130e6ca9 100644 --- a/registry/builds-topic/kustomization.yaml +++ b/registry/builds-topic/kustomization.yaml @@ -3,6 +3,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: ystack resources: -- http://y-kustomize.ystack.svc.cluster.local/v1/kafka/setup-topic-job/base-for-annotations.yaml +- http://y-kustomize:8944/v1/kafka/setup-topic-job/base-for-annotations.yaml commonAnnotations: yolean.se/kafka-topic-name: ystack.builds-registry.stream.json + yolean.se/kafka-secret-name: topic-builds-registry diff --git a/registry/generic,minio/bucket-create-ystack-builds.yaml b/registry/generic,minio/bucket-create-ystack-builds.yaml index 6bc05182..a9ac2b08 100644 --- a/registry/generic,minio/bucket-create-ystack-builds.yaml +++ b/registry/generic,minio/bucket-create-ystack-builds.yaml @@ -20,7 +20,7 @@ spec: name: minio key: secretkey - name: MINIO_HOST - value: http://y-s3-api.blobs.svc.cluster.local + value: http://y-s3-api.blobs.svc.cluster.local:9000 - name: MINIO_REGION value: us-east-1 - name: BUCKET_NAME diff --git a/registry/generic,minio/deployment.yaml b/registry/generic,minio/deployment.yaml index efb0f21b..d2aac0e5 100644 --- a/registry/generic,minio/deployment.yaml +++ b/registry/generic,minio/deployment.yaml @@ -21,7 +21,7 @@ spec: name: minio key: secretkey - name: REGISTRY_STORAGE_S3_REGIONENDPOINT - value: http://y-s3-api.blobs.svc.cluster.local + value: http://y-s3-api.blobs.svc.cluster.local:9000 - name: REGISTRY_STORAGE_S3_REGION value: us-east-1 - name: REGISTRY_STORAGE_S3_BUCKET diff --git a/registry/generic,versitygw/bucket-create-ystack-builds.yaml b/registry/generic,versitygw/bucket-create-ystack-builds.yaml index e6f5845b..a144d92c 100644 --- a/registry/generic,versitygw/bucket-create-ystack-builds.yaml +++ b/registry/generic,versitygw/bucket-create-ystack-builds.yaml @@ -22,7 +22,7 @@ spec: - name: BUCKET_NAME value: ystack-builds-registry - name: S3_ENDPOINT - value: http://y-s3-api.blobs.svc.cluster.local + value: http://y-s3-api.blobs.svc.cluster.local:9000 command: - sh - -ce diff --git a/registry/generic,versitygw/deployment.yaml b/registry/generic,versitygw/deployment.yaml index efb0f21b..d2aac0e5 100644 --- a/registry/generic,versitygw/deployment.yaml +++ b/registry/generic,versitygw/deployment.yaml @@ -21,7 +21,7 @@ spec: name: minio key: secretkey - name: REGISTRY_STORAGE_S3_REGIONENDPOINT - value: http://y-s3-api.blobs.svc.cluster.local + value: http://y-s3-api.blobs.svc.cluster.local:9000 - name: REGISTRY_STORAGE_S3_REGION value: us-east-1 - name: REGISTRY_STORAGE_S3_BUCKET diff --git a/runner.Dockerfile b/runner.Dockerfile index 984fcc17..e71231a8 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 diff --git a/y-kustomize/TODO_VALIDATE.md b/y-kustomize/TODO_VALIDATE.md deleted file mode 100644 index 3e5523a1..00000000 --- a/y-kustomize/TODO_VALIDATE.md +++ /dev/null @@ -1,63 +0,0 @@ -# y-kustomize validation - -## Design - -The `y-kustomize/openapi/` directory is a kustomize base that produces: - -1. A Secret `y-kustomize-openapi` containing: - - `openapi.yaml` — the OpenAPI 3.1 spec - - `validate.sh` — a test script - -2. A Job `y-kustomize-openapitest` using - `ghcr.io/yolean/curl-yq:387f24cd8a6098c1dafcdb4e5fd368b13af65ca3` - that runs `validate.sh`. - -## SWS hosting - -The `y-kustomize-openapi` secret is mounted as an optional volume in the -SWS deployment, serving the spec at a discovery path such as -`/openapi.yaml`. - -## Validation script - -The script: - -1. Waits for the openapi spec to be available at the discovery URL, - confirming y-kustomize is serving and the spec secret is mounted. -2. Parses the spec with `yq` to extract all paths. -3. For each `get` endpoint in the spec: - - Fetches the URL and asserts HTTP 200. - - For `base-for-annotations.yaml` endpoints, validates that the - response parses as YAML and contains expected resource kinds - (Secret, Job). -4. Reports pass/fail per endpoint. - -Endpoints backed by optional secrets that are not yet created (e.g. -`/v1/kafka/setup-topic-job/base-for-annotations.yaml` before kafka is -installed) are expected to return 404 and should not fail the test. - -## Converge integration - -Add after the `09-y-kustomize` step in `y-cluster-converge-ystack`: - -```bash -apply_base 09-y-kustomize-openapitest -k -n ystack wait job/y-kustomize-openapitest --for=condition=complete --timeout=60s -echo "# Validated: y-kustomize API spec test passed" -``` - -This runs before any consumer (like `10-versitygw` or -`20-builds-registry-versitygw`) depends on y-kustomize. - -After `10-versitygw` creates the blobs secret and y-kustomize picks it -up, the test could optionally run again to validate the newly available -endpoint. This is not yet designed. - -## TODO - -- [ ] Create `y-kustomize/openapi/validate.sh` -- [ ] Create `y-kustomize/openapi/kustomization.yaml` with secretGenerator - and Job resource -- [ ] Add `y-kustomize-openapi` volume mount to `y-kustomize/deployment.yaml` -- [ ] Add `k3s/09-y-kustomize-openapitest/` referencing the openapi base -- [ ] Add the converge step diff --git a/y-kustomize/deployment.yaml b/y-kustomize/deployment.yaml deleted file mode 100644 index cfab2dc3..00000000 --- a/y-kustomize/deployment.yaml +++ /dev/null @@ -1,54 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: y-kustomize - labels: - app: y-kustomize - yolean.se/module-part: gateway -spec: - replicas: 1 - selector: - matchLabels: - app: y-kustomize - template: - metadata: - labels: - app: y-kustomize - spec: - containers: - - name: sws - image: ghcr.io/yolean/static-web-server:2.41.0 - args: - - --port=8787 - - --root=/srv - - --directory-listing=false - - --health - - --log-level=info - - --log-remote-address - - --ignore-hidden-files=false - - --disable-symlinks=false - ports: - - containerPort: 8787 - name: http - readinessProbe: - httpGet: - path: /health - port: 8787 - resources: - requests: - cpu: 5m - memory: 8Mi - limits: - memory: 32Mi - volumeMounts: - - name: base-blobs-setup-bucket-job - mountPath: /srv/v1/blobs/setup-bucket-job - - name: base-kafka-setup-topic-job - mountPath: /srv/v1/kafka/setup-topic-job - volumes: - - name: base-blobs-setup-bucket-job - secret: - secretName: y-kustomize.blobs.setup-bucket-job - - name: base-kafka-setup-topic-job - secret: - secretName: y-kustomize.kafka.setup-topic-job diff --git a/y-kustomize/incluster/y-cluster-serve.yaml b/y-kustomize/incluster/y-cluster-serve.yaml new file mode 100644 index 00000000..1a815af6 --- /dev/null +++ b/y-kustomize/incluster/y-cluster-serve.yaml @@ -0,0 +1,4 @@ +port: 8944 +type: y-kustomize-incluster +inCluster: + labelSelector: yolean.se/module-part=y-kustomize diff --git a/y-kustomize/kustomization.yaml b/y-kustomize/kustomization.yaml index f029df14..088f4fbd 100644 --- a/y-kustomize/kustomization.yaml +++ b/y-kustomize/kustomization.yaml @@ -3,6 +3,18 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: ystack resources: -- deployment.yaml -- service.yaml -- httproute.yaml +- y-kustomize-rbac.yaml +- y-kustomize-deployment.yaml +- y-kustomize-service.yaml +- y-kustomize-httproute.yaml + +# In-cluster serve config. The current deployment image is the legacy +# y-kustomize binary which doesn't read this ConfigMap. After the +# migration documented in YSTACK_MIGRATION.md, the deployment will mount +# this at /etc/y-cluster-serve. +configMapGenerator: +# Hash suffix kept on purpose: when the in-cluster serve config changes, +# the new ConfigMap name causes a Deployment rollout that picks it up. +- name: y-kustomize-serve + files: + - incluster/y-cluster-serve.yaml diff --git a/y-kustomize/openapi/openapi.yaml b/y-kustomize/openapi/openapi.yaml deleted file mode 100644 index b1691714..00000000 --- a/y-kustomize/openapi/openapi.yaml +++ /dev/null @@ -1,84 +0,0 @@ -openapi: 3.1.0 -info: - title: y-kustomize - version: v1 - description: | - In-cluster HTTP server providing kustomize base resources for - infrastructure setup jobs. Consumers reference these URLs in their - kustomization.yaml `resources` field. - - Each base-for-annotations.yaml is a multi-document YAML file containing: - 1. A Secret with consumer credentials and endpoint URL - 2. A Job that creates/configures the resource and is idempotent - - Consumers customize via kustomize namePrefix (which prefixes the - Secret name) and JSON patches (to set resource-specific values - like bucket name or topic name via annotations). - - The Secret uses stable names (no hash suffix) so workloads in the - namespace can reference it after the setup job completes. - - Implementations may serve different content — for example a - production implementation might return a CRD-based resource that - provisions per-namespace credentials, while a dev implementation - returns a Job with shared credentials. - -servers: -- url: http://y-kustomize.ystack.svc.cluster.local - -paths: - /v1/blobs/setup-bucket-job/base-for-annotations.yaml: - get: - operationId: getBlobsSetupBucketJob - summary: Kustomize base for S3 bucket setup - description: | - Returns a multi-document YAML containing: - - A Secret named `bucket` with keys `endpoint`, `accesskey`, `secretkey` - - A Job named `setup-bucket` that creates a bucket at the S3 endpoint - - The Job expects these values to be patched by the consumer: - - `BUCKET_NAME` env var (default: `default`) - - The Secret provides consumer-facing credentials for accessing the - bucket after setup. These may differ from the admin credentials - the Job uses to create the bucket. - responses: - "200": - description: Multi-document YAML (Secret + Job) - content: - application/yaml: - schema: - type: string - - /v1/kafka/setup-topic-job/base-for-annotations.yaml: - get: - operationId: getKafkaSetupTopicJob - summary: Kustomize base for Kafka topic setup - description: | - Returns a multi-document YAML containing: - - A Secret named `topic` with keys `bootstrap` and any credentials - - A Job named `setup-topic` that creates and configures a topic - - The Job is configured via annotations: - - `yolean.se/kafka-topic-name` (required) - - `yolean.se/kafka-topic-config` (key=value pairs) - - `yolean.se/kafka-topic-partitions` (default: "1") - - `yolean.se/kafka-topic-replicas` (default: "-1") - - The Secret provides consumer-facing connection details for - producing to or consuming from the topic after setup. - responses: - "200": - description: Multi-document YAML (Secret + Job) - content: - application/yaml: - schema: - type: string - - /health: - get: - operationId: getHealth - summary: Health check - responses: - "200": - description: Server is healthy diff --git a/y-kustomize/y-cluster-serve.yaml b/y-kustomize/y-cluster-serve.yaml new file mode 100644 index 00000000..8350acec --- /dev/null +++ b/y-kustomize/y-cluster-serve.yaml @@ -0,0 +1,16 @@ +port: 8944 +# y-cluster serve runs `kustomize build` on each source dir, finds the +# Secrets it produces, and serves their data keys at /v1/{group}/{name}/{key} +# where the Secret name is y-kustomize.{group}.{name}. +# +# This config exists for downstream users running +# `y-cluster serve -c y-kustomize/` locally (developer laptops without +# a cluster, etc.). The ystack acceptance flow now uses the in-cluster +# y-kustomize Deployment exclusively; bin/acceptance-y-kustomize-local +# is the standalone test that verifies this config independently. +type: y-kustomize-local +sources: +- dir: ../kafka/setup-topic-y-kustomize +- dir: ../kafka/setup-topic-prep-y-kustomize +- dir: ../blobs-versitygw/setup-bucket-y-kustomize +- dir: ../blobs-versitygw/setup-bucket-prep-y-kustomize diff --git a/y-kustomize/y-kustomize-deployment.yaml b/y-kustomize/y-kustomize-deployment.yaml new file mode 100644 index 00000000..30adc1f7 --- /dev/null +++ b/y-kustomize/y-kustomize-deployment.yaml @@ -0,0 +1,55 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: y-kustomize + labels: + app: y-kustomize +spec: + replicas: 1 + selector: + matchLabels: + app: y-kustomize + template: + metadata: + labels: + app: y-kustomize + spec: + serviceAccountName: y-kustomize + securityContext: + runAsNonRoot: true + runAsUser: 65532 + containers: + - name: y-kustomize + image: ghcr.io/yolean/y-cluster:v0.3.7@sha256:4b1bb1202e2318de403c1254629fad6e7bac6a26e71ece9fd8eff2ce00891200 + command: ["/usr/local/bin/y-cluster"] + args: + - serve + - --foreground + - --state-dir=/tmp/y-cluster-state + - -c + - /etc/y-cluster-serve + ports: + - containerPort: 8944 + name: http + readinessProbe: + httpGet: + path: /health + port: 8944 + livenessProbe: + httpGet: + path: /health + port: 8944 + volumeMounts: + - name: cfg + mountPath: /etc/y-cluster-serve + readOnly: true + resources: + requests: + cpu: 5m + memory: 16Mi + limits: + memory: 32Mi + volumes: + - name: cfg + configMap: + name: y-kustomize-serve diff --git a/y-kustomize/httproute.yaml b/y-kustomize/y-kustomize-httproute.yaml similarity index 55% rename from y-kustomize/httproute.yaml rename to y-kustomize/y-kustomize-httproute.yaml index 5e8e3318..d171fa05 100644 --- a/y-kustomize/httproute.yaml +++ b/y-kustomize/y-kustomize-httproute.yaml @@ -1,3 +1,5 @@ +# This HTTPRoute registers the y-kustomize hostname with y-k8s-ingress-hosts +# for /etc/hosts generation. Traffic uses ServiceLB on port 8944 directly. apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute metadata: @@ -8,8 +10,8 @@ spec: parentRefs: - name: ystack hostnames: - - y-kustomize.ystack.svc.cluster.local + - y-kustomize rules: - backendRefs: - name: y-kustomize - port: 80 + port: 8944 diff --git a/y-kustomize/y-kustomize-rbac.yaml b/y-kustomize/y-kustomize-rbac.yaml new file mode 100644 index 00000000..a0352e01 --- /dev/null +++ b/y-kustomize/y-kustomize-rbac.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: y-kustomize +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: y-kustomize +rules: +- apiGroups: [""] + resources: ["secrets"] + verbs: ["list", "watch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: y-kustomize +subjects: +- kind: ServiceAccount + name: y-kustomize +roleRef: + kind: Role + name: y-kustomize + apiGroup: rbac.authorization.k8s.io diff --git a/y-kustomize/service.yaml b/y-kustomize/y-kustomize-service.yaml similarity index 68% rename from y-kustomize/service.yaml rename to y-kustomize/y-kustomize-service.yaml index 7ea2d39d..a4e5292b 100644 --- a/y-kustomize/service.yaml +++ b/y-kustomize/y-kustomize-service.yaml @@ -4,11 +4,11 @@ metadata: name: y-kustomize labels: app: y-kustomize - yolean.se/module-part: gateway spec: + type: LoadBalancer selector: app: y-kustomize ports: - name: http - port: 80 - targetPort: 8787 + port: 8944 + targetPort: 8944 diff --git a/yconverge/itest/cluster-prod/db/kustomization.yaml b/yconverge/itest/cluster-prod/db/kustomization.yaml new file mode 100644 index 00000000..575a1403 --- /dev/null +++ b/yconverge/itest/cluster-prod/db/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: 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 00000000..3a66a37f --- /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/cluster-qa/db/kustomization.yaml b/yconverge/itest/cluster-qa/db/kustomization.yaml new file mode 100644 index 00000000..e7e809fa --- /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-configmap/configmap.yaml b/yconverge/itest/example-configmap/configmap.yaml new file mode 100644 index 00000000..1f0e5e9c --- /dev/null +++ b/yconverge/itest/example-configmap/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: itest-config +data: + key: value diff --git a/yconverge/itest/example-configmap/kustomization.yaml b/yconverge/itest/example-configmap/kustomization.yaml new file mode 100644 index 00000000..a29fc9b2 --- /dev/null +++ b/yconverge/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/yconverge/itest/example-configmap/yconverge.cue b/yconverge/itest/example-configmap/yconverge.cue new file mode 100644 index 00000000..be155404 --- /dev/null +++ b/yconverge/itest/example-configmap/yconverge.cue @@ -0,0 +1,17 @@ +package example_configmap + +import ( + "yolean.se/ystack/yconverge/verify" + "yolean.se/ystack/yconverge/itest/example-namespace:example_namespace" +) + +_dep_ns: example_namespace.step + +step: verify.#Step & { + checks: [{ + kind: "exec" + command: "kubectl --context=$CONTEXT -n itest get configmap itest-config" + timeout: "10s" + description: "configmap exists" + }] +} 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 00000000..a1b08a48 --- /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 00000000..13910d8f --- /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 00000000..62864bc9 --- /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 00000000..0a06bfe9 --- /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/distributed/yconverge.cue b/yconverge/itest/example-db/distributed/yconverge.cue new file mode 100644 index 00000000..8cdb7265 --- /dev/null +++ b/yconverge/itest/example-db/distributed/yconverge.cue @@ -0,0 +1,12 @@ +package example_db_distributed + +import "yolean.se/ystack/yconverge/verify" + +step: verify.#Step & { + checks: [{ + kind: "wait" + resource: "statefulset/database" + for: "jsonpath={.status.currentReplicas}=3" + timeout: "30s" + }] +} 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 00000000..bab604e0 --- /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/k3s/40-kafka-ystack/kustomization.yaml b/yconverge/itest/example-db/namespace/kustomization.yaml similarity index 85% rename from k3s/40-kafka-ystack/kustomization.yaml rename to yconverge/itest/example-db/namespace/kustomization.yaml index 163632b8..e8102663 100644 --- a/k3s/40-kafka-ystack/kustomization.yaml +++ b/yconverge/itest/example-db/namespace/kustomization.yaml @@ -1,5 +1,6 @@ # yaml-language-server: $schema=https://json.schemastore.org/kustomization.json apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization + resources: -- ../../kafka/y-kustomize +- 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 00000000..99b63e75 --- /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 00000000..fda61129 --- /dev/null +++ b/yconverge/itest/example-db/single/yconverge.cue @@ -0,0 +1,20 @@ +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" + }, + { + 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-disabled/configmap.yaml b/yconverge/itest/example-disabled/configmap.yaml new file mode 100644 index 00000000..16a78576 --- /dev/null +++ b/yconverge/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/yconverge/itest/example-disabled/kustomization.yaml b/yconverge/itest/example-disabled/kustomization.yaml new file mode 100644 index 00000000..a29fc9b2 --- /dev/null +++ b/yconverge/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/yconverge/itest/example-disabled/yconverge.cue b/yconverge/itest/example-disabled/yconverge.cue new file mode 100644 index 00000000..8de2101b --- /dev/null +++ b/yconverge/itest/example-disabled/yconverge.cue @@ -0,0 +1,12 @@ +package example_disabled + +import "yolean.se/ystack/yconverge/verify" + +step: verify.#Step & { + checks: [{ + kind: "exec" + command: "false" + timeout: "5s" + description: "should never run" + }] +} diff --git a/yconverge/itest/example-indirect/kustomization.yaml b/yconverge/itest/example-indirect/kustomization.yaml new file mode 100644 index 00000000..49829b97 --- /dev/null +++ b/yconverge/itest/example-indirect/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- ../example-configmap diff --git a/yconverge/itest/example-namespace/kustomization.yaml b/yconverge/itest/example-namespace/kustomization.yaml new file mode 100644 index 00000000..c313b540 --- /dev/null +++ b/yconverge/itest/example-namespace/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: +- namespace.yaml diff --git a/yconverge/itest/example-namespace/namespace.yaml b/yconverge/itest/example-namespace/namespace.yaml new file mode 100644 index 00000000..a751051b --- /dev/null +++ b/yconverge/itest/example-namespace/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: itest diff --git a/yconverge/itest/example-namespace/yconverge.cue b/yconverge/itest/example-namespace/yconverge.cue new file mode 100644 index 00000000..c99ee4af --- /dev/null +++ b/yconverge/itest/example-namespace/yconverge.cue @@ -0,0 +1,12 @@ +package example_namespace + +import "yolean.se/ystack/yconverge/verify" + +step: verify.#Step & { + checks: [{ + kind: "wait" + resource: "Namespace/itest" + for: "jsonpath={.status.phase}=Active" + timeout: "10s" + }] +} diff --git a/yconverge/itest/example-replace-dependent/configmap.yaml b/yconverge/itest/example-replace-dependent/configmap.yaml new file mode 100644 index 00000000..17a783eb --- /dev/null +++ b/yconverge/itest/example-replace-dependent/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: example-replace-dependent +data: + depends-on: example-replace-job diff --git a/k3s/30-blobs-ystack/kustomization.yaml b/yconverge/itest/example-replace-dependent/kustomization.yaml similarity index 80% rename from k3s/30-blobs-ystack/kustomization.yaml rename to yconverge/itest/example-replace-dependent/kustomization.yaml index ac86556c..7543eeaf 100644 --- a/k3s/30-blobs-ystack/kustomization.yaml +++ b/yconverge/itest/example-replace-dependent/kustomization.yaml @@ -1,5 +1,8 @@ # yaml-language-server: $schema=https://json.schemastore.org/kustomization.json apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization + +namespace: default + resources: -- ../../blobs-versitygw/y-kustomize +- configmap.yaml diff --git a/yconverge/itest/example-replace-dependent/yconverge.cue b/yconverge/itest/example-replace-dependent/yconverge.cue new file mode 100644 index 00000000..708ec7ea --- /dev/null +++ b/yconverge/itest/example-replace-dependent/yconverge.cue @@ -0,0 +1,17 @@ +package example_replace_dependent + +import ( + "yolean.se/ystack/yconverge/verify" + "yolean.se/ystack/yconverge/itest/example-replace:example_replace" +) + +_dep_replace: example_replace.step + +step: verify.#Step & { + checks: [{ + kind: "exec" + command: "kubectl --context=$CONTEXT -n default get configmap example-replace-dependent" + timeout: "10s" + description: "dependent configmap exists after replace step" + }] +} diff --git a/yconverge/itest/example-replace/job.yaml b/yconverge/itest/example-replace/job.yaml new file mode 100644 index 00000000..63edc04d --- /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/k3s/10-gateway-api/kustomization.yaml b/yconverge/itest/example-replace/kustomization.yaml similarity index 82% rename from k3s/10-gateway-api/kustomization.yaml rename to yconverge/itest/example-replace/kustomization.yaml index 195509f2..37b594f5 100644 --- a/k3s/10-gateway-api/kustomization.yaml +++ b/yconverge/itest/example-replace/kustomization.yaml @@ -1,5 +1,8 @@ # yaml-language-server: $schema=https://json.schemastore.org/kustomization.json apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization + +namespace: default + resources: -- traefik-gateway-provider.yaml +- job.yaml diff --git a/yconverge/itest/example-replace/yconverge.cue b/yconverge/itest/example-replace/yconverge.cue new file mode 100644 index 00000000..130acb44 --- /dev/null +++ b/yconverge/itest/example-replace/yconverge.cue @@ -0,0 +1,12 @@ +package example_replace + +import "yolean.se/ystack/yconverge/verify" + +step: verify.#Step & { + checks: [{ + kind: "exec" + command: "kubectl --context=$CONTEXT -n default get job example-replace-job" + timeout: "10s" + description: "replace-mode Job exists" + }] +} diff --git a/yconverge/itest/example-serverside/configmap.yaml b/yconverge/itest/example-serverside/configmap.yaml new file mode 100644 index 00000000..b3f5159f --- /dev/null +++ b/yconverge/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/yconverge/itest/example-serverside/kustomization.yaml b/yconverge/itest/example-serverside/kustomization.yaml new file mode 100644 index 00000000..b05b1265 --- /dev/null +++ b/yconverge/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/yconverge/itest/example-with-dependency/configmap.yaml b/yconverge/itest/example-with-dependency/configmap.yaml new file mode 100644 index 00000000..578b3839 --- /dev/null +++ b/yconverge/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/yconverge/itest/example-with-dependency/kustomization.yaml b/yconverge/itest/example-with-dependency/kustomization.yaml new file mode 100644 index 00000000..a29fc9b2 --- /dev/null +++ b/yconverge/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/yconverge/itest/example-with-dependency/yconverge.cue b/yconverge/itest/example-with-dependency/yconverge.cue new file mode 100644 index 00000000..c31ead37 --- /dev/null +++ b/yconverge/itest/example-with-dependency/yconverge.cue @@ -0,0 +1,17 @@ +package example_with_dependency + +import ( + "yolean.se/ystack/yconverge/verify" + "yolean.se/ystack/yconverge/itest/example-configmap:example_configmap" +) + +_dep_config: example_configmap.step + +step: verify.#Step & { + checks: [{ + kind: "exec" + command: "kubectl --context=$CONTEXT -n itest get configmap itest-dependent" + timeout: "10s" + description: "dependent configmap exists" + }] +} diff --git a/yconverge/itest/test.sh b/yconverge/itest/test.sh new file mode 100755 index 00000000..675b4223 --- /dev/null +++ b/yconverge/itest/test.sh @@ -0,0 +1,292 @@ +#!/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. + +Flags: + --keep keep the kwok cluster running after tests + --teardown remove a kept cluster and exit + +Requires: docker, kubectl, y-cue, y-cluster +' && 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 + +# 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_tolerant yconverge-itest + 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" + +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:" + echo " KUBECONFIG=$ITEST_KUBECONFIG kubectl --context=$CTX get ns" + return + fi + echo "[cue itest] Cleaning up ..." + _docker_rm_tolerant "$CONTAINER_NAME" + rm -f "$ITEST_KUBECONFIG" +} +trap cleanup EXIT + +echo "[cue itest] yconverge framework integration tests" + +# --- lint (zero failures required) --- + +echo "[cue itest] Linting scripts ..." + +# --- start 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 +PORT=$(docker port "$CONTAINER_NAME" 8080 | head -1 | cut -d: -f2) + +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 + +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; } + +# 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" + +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 + +# --- schema validation --- + +echo "" +echo "[cue itest] CUE schema validation" +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/ +y-cue vet ./yconverge/itest/example-db/distributed/ + +# --- apply with auto-checks --- + +echo "" +echo "[cue itest] Apply with auto-checks (namespace)" +y-cluster yconverge --context="$CTX" -k yconverge/itest/example-namespace/ + +echo "" +echo "[cue itest] Apply with checks (configmap depends on namespace)" +y-cluster yconverge --context="$CTX" -k yconverge/itest/example-configmap/ + +echo "" +echo "[cue itest] Transitive dependency (depends on configmap which depends on namespace)" +y-cluster yconverge --context="$CTX" -k yconverge/itest/example-with-dependency/ + +# --- dependency ordering: checks must complete before downstream steps start --- + +echo "" +echo "[cue itest] Verify dependency checks serialize before downstream steps" +_DEP_OUT=$(mktemp /tmp/yconverge-itest-deps.XXXXXX) +y-cluster yconverge --context="$CTX" -k yconverge/itest/example-with-dependency/ 2>&1 | tee "$_DEP_OUT" +# namespace check must complete before configmap step begins +_ns_check=$(grep -n 'condition met' "$_DEP_OUT" | head -1 | cut -d: -f1) +_cm_step=$(grep -n 'yconverge dependency .*example-configmap' "$_DEP_OUT" | cut -d: -f1) +[ "$_ns_check" -lt "$_cm_step" ] \ + || { echo "[cue itest] FAIL: namespace check (line $_ns_check) must complete before configmap step (line $_cm_step)"; exit 1; } +# configmap check must complete before with-dependency step begins. +# yconverge no longer echoes check descriptions; the first +# "yconverge check ... exec" line in the output is example-configmap's +# (example-namespace's check is kind=wait, not exec). +_cm_check=$(grep -n 'yconverge check.*exec' "$_DEP_OUT" | head -1 | cut -d: -f1) +_wd_step=$(grep -n 'yconverge target .*example-with-dependency' "$_DEP_OUT" | cut -d: -f1) +[ "$_cm_check" -lt "$_wd_step" ] \ + || { echo "[cue itest] FAIL: configmap check (line $_cm_check) must complete before with-dependency step (line $_wd_step)"; exit 1; } +rm -f "$_DEP_OUT" + +# --- indirection with namespace from referenced base --- + +echo "" +echo "[cue itest] Indirection: yconverge.cue and namespace from referenced base" +y-cluster yconverge --context="$CTX" -k yconverge/itest/example-indirect/ + +# --- idempotent re-converge --- + +echo "" +echo "[cue itest] Idempotent re-apply" +y-cluster yconverge --context="$CTX" -k yconverge/itest/example-namespace/ +y-cluster yconverge --context="$CTX" -k yconverge/itest/example-configmap/ + +# --- converge-mode labels --- + +echo "" +echo "[cue itest] Serverside-force label (other selectors match nothing)" +y-cluster yconverge --context="$CTX" --skip-checks -k yconverge/itest/example-serverside/ +y-cluster yconverge --context="$CTX" --skip-checks -k yconverge/itest/example-serverside/ + +echo "" +echo "[cue itest] replace-mode under --dry-run=server must not delete anything" +y-cluster 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) +y-cluster 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" + +# --- dep edge through a replace-mode resource --- +# +# example-replace-dependent imports example-replace as a CUE dep, so a run +# of `yconverge -k example-replace-dependent/` must walk into example-replace +# (a yolean.se/converge-mode=replace Job) BEFORE applying the dependent +# ConfigMap. Two consecutive runs must yield different Job UIDs -- the +# replace path is delete+apply, not SSA, so the second run re-creates. +echo "" +echo "[cue itest] Dep edge through a replace-mode resource" +y-cluster yconverge --context="$CTX" -k yconverge/itest/example-replace-dependent/ +_DEPREP_UID_1=$(kubectl --context="$CTX" -n default get job example-replace-job -o jsonpath='{.metadata.uid}') +y-cluster yconverge --context="$CTX" -k yconverge/itest/example-replace-dependent/ +_DEPREP_UID_2=$(kubectl --context="$CTX" -n default get job example-replace-job -o jsonpath='{.metadata.uid}') +[ "$_DEPREP_UID_1" != "$_DEPREP_UID_2" ] \ + || { echo "[cue itest] FAIL: replace-mode dep edge did not re-create the Job (uid $_DEPREP_UID_1 unchanged)"; exit 1; } +kubectl --context="$CTX" -n default delete job example-replace-job >/dev/null +kubectl --context="$CTX" -n default delete configmap example-replace-dependent >/dev/null + +_OUT=$(mktemp /tmp/yconverge-itest-out.XXXXXX) + +# --- assert: indirection output shows referenced path --- + +echo "" +echo "[cue itest] Indirection output must reference the base directory" +y-cluster yconverge --context="$CTX" -k yconverge/itest/example-indirect/ 2>&1 | tee "$_OUT" +# yconverge progress lines reference the base by relpath without a +# `/yconverge.cue` suffix; example-indirect's kustomization.yaml +# pulls example-configmap as a kustomize resource, and the dep +# walker must surface that as a yconverge progress line. +grep -q 'yconverge dependency .*example-configmap' "$_OUT" + +# --- negative: --skip-checks suppresses check invocation --- + +echo "" +echo "[cue itest] --skip-checks must not produce [yconverge] output" +y-cluster yconverge --context="$CTX" --skip-checks -k yconverge/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 with error message" +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 +! y-cluster 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" + +# --- prod/qa kustomize example --- +# +# DISABLED until y-cluster grows "apply once at the top, run nested-base +# checks in depth-first order" semantics. v0.3.3's dep walker treats every +# CUE-imported base as a standalone apply step; but example-db/{single, +# distributed} carry a sentinel namespace (ONLY_apply_through_cluster_variant) +# that requires the cluster-prod/cluster-qa overlay to override. Applied +# standalone they fail with "namespaces ONLY_apply_through_cluster_variant +# not found". +# +# The example-db/checks pure-CUE library used to factor the parameterized +# #DbChecks across single/distributed; that import-only-for-shared-defs +# pattern also breaks v0.3.3 (the dep walker tries to traverse pure-CUE +# packages as kustomize bases). Inlined the checks for now (see +# example-db/{single,distributed}/yconverge.cue). +# +# kubectl yconverge --context="$CTX" -k yconverge/itest/example-db/namespace/ +# kubectl yconverge --context="$CTX" -k yconverge/itest/cluster-prod/db/ +# kubectl --context="$CTX" -n db delete pdb database +# kubectl yconverge --context="$CTX" -k yconverge/itest/cluster-qa/db/ + +echo "" +echo "[cue itest] All tests passed" diff --git a/yconverge/verify/schema.cue b/yconverge/verify/schema.cue new file mode 100644 index 00000000..20055449 --- /dev/null +++ b/yconverge/verify/schema.cue @@ -0,0 +1,44 @@ +package verify + +// 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: { + // Checks that must pass after apply. + // 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 + for: string + 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 + 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 +}