orderbooks/scripts/deploy/deploy_ws_canary_kaniko.sh
2026-04-19 19:17:56 +02:00

218 lines
8.4 KiB
Bash
Executable file

#!/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-<sha12>-<UTC>.
--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" <<EOF_JSON
{
"schema_name": "ws_canary_deploy_evidence",
"schema_version": 1,
"run_id": "${RUN_ID}",
"mode": "server_dry_run",
"status": "PASS",
"image": "${IMAGE}",
"git_ref": "${GIT_REF}",
"resources_applied": ["namespace.yaml", "configmap.yaml", "pvc.yaml", "cronjob-uploader.yaml", "deployment-ws-recorder.yaml"],
"deployment_collector_applied": false,
"rest_image_before": "${REST_IMAGE_BEFORE}",
"rest_ready_before": "${REST_READY_BEFORE}"
}
EOF_JSON
echo "WS_CANARY_DEPLOY_EVIDENCE=$OUTPUT_PATH"
echo "WS_CANARY_DRY_RUN=PASS"
exit 0
fi
if ! GIT_TERMINAL_PROMPT=0 git ls-remote "$REPO_CLONE_URL" "$GIT_REF" >/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" <<EOF_JOB
apiVersion: batch/v1
kind: Job
metadata:
name: ${BUILD_JOB}
namespace: ${NAMESPACE}
labels:
app.kubernetes.io/name: orderbooks
app.kubernetes.io/component: ws-canary-build
spec:
backoffLimit: 0
ttlSecondsAfterFinished: 3600
template:
spec:
restartPolicy: Never
volumes:
- name: workspace
emptyDir: {}
- name: registry-creds
secret:
secretName: ${REGISTRY_SECRET_NAME}
items:
- key: .dockerconfigjson
path: config.json
initContainers:
- name: checkout
image: alpine/git:2.47.2
command: ["/bin/sh", "-lc"]
args:
- >-
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"