From 27640928892e42a0aa0546a5d1a3a32487661de5 Mon Sep 17 00:00:00 2001 From: Paolo Dettori Date: Mon, 24 Aug 2026 22:13:45 -0400 Subject: [PATCH] fix(kind): pull the published harness image by default (#154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setup-kind.sh built the harness image locally on every run, so a first-time quickstart hit the source build path — and any build breakage (e.g. #154's non-hermetic codegen, fixed separately in #161) blocked install with no workaround beyond --skip-build. Default to pulling the published image (ghcr.io/rossoctl/serverless-harness:latest, a public GHCR package) and loading it into kind, falling back transparently to a local build if the pull is unavailable (offline / image missing). This removes the local Docker build from the first-time path while staying non-regressing: - --build forces a local build (for testing local source changes) - --skip-build reuses a preloaded dev.local tag (unchanged) - --image / SH_IMAGE override the pulled image Image provisioning is extracted into ensure_harness_image() with a SH_SOURCE_ONLY guard so it can be unit-tested without a cluster; the new deploy/knative/tests/setup-kind-image.test.sh mocks docker/kind and asserts the pull / fallback-build / force-build / skip decision paths. The new-Revision build-ts stamp now fires whenever an image is (re)loaded (pull or build), not only on build. Docs (README-kind.md, serverless-harness-demo.md) updated for the new default. Refs #154 Assisted-By: Claude (Anthropic AI) Signed-off-by: Paolo Dettori --- deploy/knative/README-kind.md | 10 ++- deploy/knative/setup-kind.sh | 67 +++++++++++--- deploy/knative/tests/setup-kind-image.test.sh | 90 +++++++++++++++++++ serverless-harness-demo.md | 15 ++-- 4 files changed, 163 insertions(+), 19 deletions(-) create mode 100755 deploy/knative/tests/setup-kind-image.test.sh diff --git a/deploy/knative/README-kind.md b/deploy/knative/README-kind.md index fb80b07..b687ad6 100644 --- a/deploy/knative/README-kind.md +++ b/deploy/knative/README-kind.md @@ -48,16 +48,24 @@ curl -H 'Host: serverless-harness.default.example.com' \ ## Options ``` ---skip-build Do not build/load the harness image (use existing) +--build Force a local harness build from this checkout +--skip-build Do not build/pull the harness image (use existing dev.local tag) +--image Published harness image to pull (default: ghcr.io/rossoctl/serverless-harness:latest) --cluster-name Kind cluster name (default: sh-knative) ``` +By default (no `--build`/`--skip-build`) the script **pulls the published harness image** and +loads it into the cluster, falling back to a local build only if the pull is unavailable +(offline or image missing). Use `--build` to always build from source. + Environment variables: | Variable | Default | Description | |----------|---------|-------------| | `CLUSTER_NAME` | `sh-knative` | Kind cluster name | | `KNATIVE_VERSION` | `v1.14.0` | Knative Serving version | +| `SH_IMAGE` | `ghcr.io/rossoctl/serverless-harness:latest` | Published harness image pulled by default (same as `--image`) | +| `FORCE_BUILD` | `false` | Force a local build (same as `--build`) | | `KEDA_VERSION` | `v2.14.0` | KEDA version | ## Choosing the model diff --git a/deploy/knative/setup-kind.sh b/deploy/knative/setup-kind.sh index 0db289e..4285052 100755 --- a/deploy/knative/setup-kind.sh +++ b/deploy/knative/setup-kind.sh @@ -9,7 +9,15 @@ # - ANTHROPIC_API_KEY env var set # # Usage: -# ./deploy/knative/setup-kind.sh [--skip-build] [--cluster-name ] +# ./deploy/knative/setup-kind.sh [--skip-build] [--build] [--image ] [--cluster-name ] +# +# Harness image (dev.local/serverless-harness:local, referenced by service.yaml): +# default Pull the published image ($SH_IMAGE) and load it into kind; if the pull is +# unavailable (offline / image missing), transparently fall back to a local build. +# A first-time quickstart therefore needs no local Docker build. +# --build Force a local build from this checkout (use when testing local source changes). +# --skip-build Do neither; assume dev.local/serverless-harness:local is already loaded. +# --image / SH_IMAGE= Override the published image to pull. set -euo pipefail @@ -17,18 +25,60 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" CLUSTER_NAME="${CLUSTER_NAME:-sh-knative}" SKIP_BUILD="${SKIP_BUILD:-false}" +FORCE_BUILD="${FORCE_BUILD:-false}" KNATIVE_VERSION="${KNATIVE_VERSION:-v1.14.0}" +# Published harness image pulled by default (public GHCR package under the rossoctl org). +SH_IMAGE="${SH_IMAGE:-ghcr.io/rossoctl/serverless-harness:latest}" +# Local tag the Knative manifests reference; the pulled/built image is (re)tagged to this. +LOCAL_IMAGE="${LOCAL_IMAGE:-dev.local/serverless-harness:local}" # Parse args for arg in "$@"; do case $arg in --skip-build) SKIP_BUILD=true ;; + --build) FORCE_BUILD=true ;; --cluster-name) shift; CLUSTER_NAME="$1" ;; --cluster-name=*) CLUSTER_NAME="${arg#*=}" ;; + --image) shift; SH_IMAGE="$1" ;; + --image=*) SH_IMAGE="${arg#*=}" ;; esac shift 2>/dev/null || true done +# Provide the harness image referenced by service.yaml ($LOCAL_IMAGE). By default we pull the +# published image (fast path — no local build needed for a first-time quickstart) and fall back +# to a local build only if the pull is unavailable. --build forces a local build; --skip-build +# assumes the image is already loaded. Sets HARNESS_IMAGE_LOADED when it (re)loads an image. +ensure_harness_image() { + HARNESS_IMAGE_LOADED=false + if [ "$SKIP_BUILD" = "true" ]; then + echo "--- Skipping harness image build/pull (--skip-build): expecting $LOCAL_IMAGE preloaded ---" + return 0 + fi + if [ "$FORCE_BUILD" != "true" ]; then + echo "--- Pulling published harness image: $SH_IMAGE ---" + if docker pull "$SH_IMAGE"; then + docker tag "$SH_IMAGE" "$LOCAL_IMAGE" + echo "--- Loading pulled image into kind ---" + kind load docker-image "$LOCAL_IMAGE" --name "$CLUSTER_NAME" + HARNESS_IMAGE_LOADED=true + return 0 + fi + echo "--- Pull unavailable ($SH_IMAGE); falling back to a local build ---" + else + echo "--- Building serverless-harness image locally (--build) ---" + fi + docker build --load -t "$LOCAL_IMAGE" "$REPO_ROOT" + echo "--- Loading built image into kind ---" + kind load docker-image "$LOCAL_IMAGE" --name "$CLUSTER_NAME" + HARNESS_IMAGE_LOADED=true +} + +# When sourced by tests (SH_SOURCE_ONLY=1), stop here so only the functions above are exposed. +if [ "${SH_SOURCE_ONLY:-0}" = "1" ]; then + return 0 +fi + echo "=== M4 Knative Setup (cluster: $CLUSTER_NAME) ===" # 1. Create kind cluster if it doesn't exist @@ -124,13 +174,9 @@ kubectl -n default wait --for=condition=Ready pod -l "$POOL_SELECTOR" --timeout= exit 1 } -# 7. Build and load harness image -if [ "$SKIP_BUILD" != "true" ]; then - echo "--- Building serverless-harness image ---" - docker build --load -t dev.local/serverless-harness:local "$REPO_ROOT" - echo "--- Loading image into kind ---" - kind load docker-image dev.local/serverless-harness:local --name "$CLUSTER_NAME" -fi +# 7. Provide the harness image ($LOCAL_IMAGE) referenced by service.yaml (pull by default, +# local build with --build, or reuse a preloaded image with --skip-build). +ensure_harness_image # 8. Create LLM credentials secret (supports direct API key or gateway bridge) if [ "${SH_AUTHBRIDGE:-0}" = "1" ]; then @@ -268,8 +314,9 @@ kubectl apply -f "$SCRIPT_DIR/service.yaml" # The image tag (dev.local/serverless-harness:local) is mutable, so re-applying an unchanged # service spec does NOT roll a new Revision — Knative would keep serving the previous Revision # (pinned to the OLD image digest) and a freshly built image would never be deployed. Force a new -# Revision by stamping a build marker into the template so the rebuilt image is always picked up. -if [ "$SKIP_BUILD" != "true" ]; then +# Revision by stamping a build marker into the template so the (re)loaded image is always picked up. +# Stamp whenever we loaded an image this run (pull or build); skip when --skip-build reused one. +if [ "${HARNESS_IMAGE_LOADED:-false}" = "true" ]; then kubectl -n default patch ksvc serverless-harness --type merge \ -p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"deploy.sh/build-ts\":\"$(date +%s)\"}}}}}" fi diff --git a/deploy/knative/tests/setup-kind-image.test.sh b/deploy/knative/tests/setup-kind-image.test.sh new file mode 100755 index 0000000..69b0063 --- /dev/null +++ b/deploy/knative/tests/setup-kind-image.test.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# deploy/knative/tests/setup-kind-image.test.sh +# +# Unit test for ensure_harness_image() in ../setup-kind.sh — the harness-image +# provisioning decision (pull-by-default, --build force, --skip-build reuse, and +# transparent fallback to a local build when the pull is unavailable). +# +# No cluster required: docker/kind are mocked on PATH and only the call log is +# asserted. Run: bash deploy/knative/tests/setup-kind-image.test.sh +set -uo pipefail + +SCRIPT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/setup-kind.sh" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +export MOCK_LOG="$TMP/calls.log" + +# --- mocks: log every invocation; `docker pull` exit code is controlled by MOCK_PULL_RC --- +mkdir -p "$TMP/bin" +cat > "$TMP/bin/docker" <<'EOF' +#!/usr/bin/env bash +echo "docker $*" >> "$MOCK_LOG" +[ "${1:-}" = "pull" ] && exit "${MOCK_PULL_RC:-0}" +exit 0 +EOF +cat > "$TMP/bin/kind" <<'EOF' +#!/usr/bin/env bash +echo "kind $*" >> "$MOCK_LOG" +exit 0 +EOF +chmod +x "$TMP/bin/docker" "$TMP/bin/kind" +export PATH="$TMP/bin:$PATH" + +# Run ensure_harness_image() in a subshell with the given knobs; capture the call log. +# Args: SKIP_BUILD FORCE_BUILD MOCK_PULL_RC. Captured into named vars first because sourcing +# setup-kind.sh runs its arg-parse loop, which shifts away the subshell's positional params. +run_case() { + : > "$MOCK_LOG" + local sb="$1" fb="$2" prc="$3" + # SKIP_BUILD/FORCE_BUILD/SH_IMAGE/LOCAL_IMAGE/CLUSTER_NAME are consumed by + # ensure_harness_image, which is defined in the sourced setup-kind.sh — shellcheck + # can't follow the source, hence the directives below. + # shellcheck disable=SC2034 + ( + # shellcheck source=/dev/null + SH_SOURCE_ONLY=1 source "$SCRIPT" + SKIP_BUILD="$sb" FORCE_BUILD="$fb" + SH_IMAGE="ghcr.io/rossoctl/serverless-harness:latest" + LOCAL_IMAGE="dev.local/serverless-harness:local" + CLUSTER_NAME="sh-test" + export MOCK_PULL_RC="$prc" + ensure_harness_image + ) >/dev/null +} + +FAILS=0 +# assert_grep / assert_absent +assert_grep() { if grep -q -- "$1" "$MOCK_LOG"; then echo " ok: $2"; else echo " FAIL: $2 (expected /$1/)"; FAILS=$((FAILS+1)); fi; } +assert_absent() { if grep -q -- "$1" "$MOCK_LOG"; then echo " FAIL: $2 (unexpected /$1/)"; FAILS=$((FAILS+1)); else echo " ok: $2"; fi; } + +echo "case 1: default (pull succeeds) -> pull + tag + load, no build" +run_case false false 0 +assert_grep "docker pull ghcr.io/rossoctl/serverless-harness:latest" "pulls the published image" +assert_grep "docker tag" "retags to the local image" +assert_grep "kind load" "loads into kind" +assert_absent "docker build" "does not build locally" + +echo "case 2: default but pull fails -> falls back to local build" +run_case false false 1 +assert_grep "docker pull" "attempts the pull first" +assert_grep "docker build" "falls back to a local build" +assert_grep "kind load" "loads the built image" + +echo "case 3: --build (FORCE_BUILD) -> build only, never pulls" +run_case false true 0 +assert_grep "docker build" "builds locally" +assert_grep "kind load" "loads the built image" +assert_absent "docker pull" "does not pull" + +echo "case 4: --skip-build -> neither pull nor build" +run_case true false 0 +assert_absent "docker" "runs no docker command" +assert_absent "kind" "runs no kind command" + +echo +if [ "$FAILS" -eq 0 ]; then + echo "PASS: all ensure_harness_image cases" +else + echo "FAIL: $FAILS assertion(s) failed" +fi +exit "$FAILS" diff --git a/serverless-harness-demo.md b/serverless-harness-demo.md index 4a6c5fe..f4091c3 100644 --- a/serverless-harness-demo.md +++ b/serverless-harness-demo.md @@ -4,14 +4,13 @@ git clone --recurse-submodules https://github.com/kagenti/serverless-harness.git ./deploy/knative/setup-kind.sh ``` -> To skip the local harness build and use the published image, pull it into the kind -> cluster first under the tag the manifests reference, then pass `--skip-build`: -> ``` -> docker pull ghcr.io/rossoctl/serverless-harness:latest -> docker tag ghcr.io/rossoctl/serverless-harness:latest dev.local/serverless-harness:local -> kind load docker-image dev.local/serverless-harness:local --name sh-knative -> ./deploy/knative/setup-kind.sh --skip-build -> ``` +> By default `setup-kind.sh` **pulls the published harness image** +> (`ghcr.io/rossoctl/serverless-harness:latest`) and loads it into the kind cluster — a +> first-time quickstart needs no local Docker build. If the pull is unavailable (offline, or +> the image is missing) it transparently falls back to building from this checkout. +> - `--build` — force a local build (use when testing local source changes). +> - `--skip-build` — reuse an image you already loaded as `dev.local/serverless-harness:local`. +> > See [`deploy/knative/README-kind.md`](deploy/knative/README-kind.md) for more setup options. setup inferencing