#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" KUBECONFIG_PATH="${KUBECONFIG_PATH:-/home/philipp/dev/ae/nuri/unrip3/.state/hetzner/kubeconfig.yaml}" NAMESPACE="${PROJECT_NAMESPACE:-orderbooks}" REGISTRY_HOST="${REGISTRY_HOST:-registry.doran.133011.xyz}" PROJECT_NAME="${PROJECT_NAME:-orderbooks}" REGISTRY_SECRET_NAME="${PROJECT_REGISTRY_SECRET_NAME:-orderbooks-registry-creds}" REPO_CLONE_URL="${REPO_CLONE_URL:-https://git.doran.133011.xyz/philipp/orderbooks.git}" GIT_REF="$(git -C "$ROOT_DIR" rev-parse HEAD)" IMAGE_TAG="" OUTPUT_PATH="" SERVER_DRY_RUN=0 SKIP_BUILD=0 usage() { cat <<'EOF' Usage: scripts/deploy/deploy_ws_canary_kaniko.sh [options] Canary-only build/deploy path for orderbooks-ws-recorder. It does not apply or roll deployment-collector.yaml and does not set the orderbooks-collector image. Options: --git-ref SHA Committed Git SHA to build. Default: local HEAD. --image-tag TAG Image tag. Default: ws-canary--. --output PATH Local deploy evidence JSON path. --server-dry-run Do not build. Server-dry-run only the canary apply set. --skip-build Skip Kaniko build and use REGISTRY_HOST/PROJECT_NAME:TAG. --help Show help. EOF } while [[ $# -gt 0 ]]; do case "$1" in --git-ref) GIT_REF="$2"; shift 2 ;; --image-tag) IMAGE_TAG="$2"; shift 2 ;; --output) OUTPUT_PATH="$2"; shift 2 ;; --server-dry-run) SERVER_DRY_RUN=1; shift ;; --skip-build) SKIP_BUILD=1; shift ;; --help) usage; exit 0 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; esac done require() { command -v "$1" >/dev/null 2>&1 || { echo "missing required command: $1" >&2; exit 2; }; } require kubectl require python3 require git export KUBECONFIG="${KUBECONFIG:-$KUBECONFIG_PATH}" [[ -f "$KUBECONFIG" ]] || { echo "missing kubeconfig" >&2; exit 2; } short_sha="$(printf '%s' "$GIT_REF" | cut -c1-12 | tr '[:upper:]' '[:lower:]')" if [[ -z "$IMAGE_TAG" ]]; then IMAGE_TAG="ws-canary-${short_sha}-$(date -u +%Y%m%dT%H%M%SZ | tr '[:upper:]' '[:lower:]')" fi IMAGE="${REGISTRY_HOST}/${PROJECT_NAME}:${IMAGE_TAG}" RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)" if [[ -z "$OUTPUT_PATH" ]]; then OUTPUT_PATH="${ROOT_DIR}/data/manifests/ws_canary_deploy_${RUN_ID}.json" fi mkdir -p "$(dirname "$OUTPUT_PATH")" TMPDIR="$(mktemp -d)" trap 'rm -rf "$TMPDIR"' EXIT REST_IMAGE_BEFORE="$(kubectl -n "$NAMESPACE" get deployment orderbooks-collector -o jsonpath='{.spec.template.spec.containers[0].image}' 2>/dev/null || true)" REST_READY_BEFORE="$(kubectl -n "$NAMESPACE" get deployment orderbooks-collector -o jsonpath='{.status.readyReplicas}/{.spec.replicas}' 2>/dev/null || true)" render_canary() { python3 - "$ROOT_DIR" "$IMAGE" <<'PY_RENDER' import sys from pathlib import Path root=Path(sys.argv[1]) image=sys.argv[2] files=[ 'deploy/k8s/base/namespace.yaml', 'deploy/k8s/base/configmap.yaml', 'deploy/k8s/base/pvc.yaml', 'deploy/k8s/base/cronjob-uploader.yaml', 'deploy/k8s/base/deployment-ws-recorder.yaml', ] for index, rel in enumerate(files): if index: print('---') text=(root/rel).read_text() text=text.replace('registry.doran.133011.xyz/orderbooks:bootstrap', image) print(text.rstrip()) PY_RENDER } if [[ "$SERVER_DRY_RUN" -eq 1 ]]; then render_canary | kubectl apply --dry-run=server -f - cat >"$OUTPUT_PATH" </dev/null 2>&1; then # ls-remote by raw SHA may not match refs; accept if the commit is reachable from main. remote_main="$(GIT_TERMINAL_PROMPT=0 git ls-remote "$REPO_CLONE_URL" refs/heads/main | awk '{print $1}')" if [[ "$remote_main" != "$GIT_REF" ]]; then echo "git ref is not confirmed on Forgejo main; push the source commit first" >&2 exit 3 fi fi BUILD_JOB="orderbooks-ws-build-${short_sha}" BUILD_JOB="$(printf '%s' "$BUILD_JOB" | tr -cs 'a-z0-9-' '-' | sed 's/^-//;s/-$//' | cut -c1-63)" if [[ "$SKIP_BUILD" -eq 0 ]]; then kubectl -n "$NAMESPACE" delete job "$BUILD_JOB" --ignore-not-found=true >/dev/null cat >"$TMPDIR/build-job.yaml" <- git clone --depth=1 --branch main "${REPO_CLONE_URL}" /workspace && cd /workspace && git checkout --detach "${GIT_REF}" volumeMounts: - name: workspace mountPath: /workspace containers: - name: kaniko image: gcr.io/kaniko-project/executor:v1.23.2-debug args: - --context=/workspace - --dockerfile=/workspace/Dockerfile - --destination=${IMAGE} - --cache=false volumeMounts: - name: workspace mountPath: /workspace - name: registry-creds mountPath: /kaniko/.docker EOF_JOB kubectl apply -f "$TMPDIR/build-job.yaml" >/dev/null kubectl -n "$NAMESPACE" wait --for=condition=Complete --timeout=20m "job/${BUILD_JOB}" >/dev/null fi BUILD_LOG_TAIL="$(kubectl -n "$NAMESPACE" logs "job/${BUILD_JOB}" --tail=120 2>/dev/null || true)" render_canary | kubectl apply -f - >/dev/null kubectl -n "$NAMESPACE" rollout status deployment/orderbooks-ws-recorder --timeout=300s >/dev/null WS_IMAGE_AFTER="$(kubectl -n "$NAMESPACE" get deployment orderbooks-ws-recorder -o jsonpath='{.spec.template.spec.containers[0].image}')" WS_READY_AFTER="$(kubectl -n "$NAMESPACE" get deployment orderbooks-ws-recorder -o jsonpath='{.status.readyReplicas}/{.spec.replicas}')" REST_IMAGE_AFTER="$(kubectl -n "$NAMESPACE" get deployment orderbooks-collector -o jsonpath='{.spec.template.spec.containers[0].image}')" REST_READY_AFTER="$(kubectl -n "$NAMESPACE" get deployment orderbooks-collector -o jsonpath='{.status.readyReplicas}/{.spec.replicas}')" WRITE_EVIDENCE_PY="$TMPDIR/write-evidence.py" cat >"$WRITE_EVIDENCE_PY" <<'PY_WRITE' import datetime as dt, json, sys from pathlib import Path (path, run_id, git_ref, image, build_job, ws_image_after, ws_ready_after, rest_image_before, rest_ready_before, rest_image_after, rest_ready_after)=sys.argv[1:12] manifest={ 'schema_name':'ws_canary_deploy_evidence', 'schema_version':1, 'run_id':run_id, 'written_at_utc':dt.datetime.now(dt.UTC).replace(microsecond=0).isoformat().replace('+00:00','Z'), 'mode':'live_canary_deploy', 'status':'PASS', 'git_ref':git_ref, 'image':image, 'build_job':build_job, 'build_log_tail':sys.stdin.read()[-6000:], 'resources_applied':['namespace.yaml','configmap.yaml','pvc.yaml','cronjob-uploader.yaml','deployment-ws-recorder.yaml'], 'deployment_collector_applied':False, 'ws_recorder':{'image_after':ws_image_after,'ready_after':ws_ready_after}, 'rest_collector':{'image_before':rest_image_before,'ready_before':rest_ready_before,'image_after':rest_image_after,'ready_after':rest_ready_after,'unchanged':rest_image_before==rest_image_after and rest_ready_before==rest_ready_after}, } Path(path).write_text(json.dumps(manifest, indent=2, sort_keys=True)+'\n') PY_WRITE printf '%s' "$BUILD_LOG_TAIL" | python3 "$WRITE_EVIDENCE_PY" "$OUTPUT_PATH" "$RUN_ID" "$GIT_REF" "$IMAGE" "$BUILD_JOB" "$WS_IMAGE_AFTER" "$WS_READY_AFTER" "$REST_IMAGE_BEFORE" "$REST_READY_BEFORE" "$REST_IMAGE_AFTER" "$REST_READY_AFTER" echo "WS_CANARY_DEPLOY_EVIDENCE=$OUTPUT_PATH" echo "WS_CANARY_IMAGE=$IMAGE" echo "WS_CANARY_DEPLOY=PASS"