From 327b4624e82a4332f5e926b8ec5da2bfdeeb5d78 Mon Sep 17 00:00:00 2001 From: Angel Marin Date: Wed, 19 Aug 2026 10:47:51 +0200 Subject: [PATCH 1/2] [HYPERSHELL-111] fix(kind): build LOCAL_IMAGES from working tree by default build-images.sh built from origin/main, creating a mismatch with the branch's scripts, manifests, and seed data on feature branches. Also used `kind load docker-image` which fails with Podman. Default BUILD_SOURCE to `worktree` so images match the current branch. Add BUILD_SOURCE=baseline opt-in for origin/main builds. Use `kind load image-archive` (tarball) for Podman compatibility, matching swap-component.sh. Co-Authored-By: Claude Opus 4.6 --- Makefile | 4 ++- scripts/kind/build-images.sh | 44 ++++++++++++++++-------- specs/platform/local-development.spec.md | 24 ++++++++----- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/Makefile b/Makefile index ebcd21a3..fee845e4 100644 --- a/Makefile +++ b/Makefile @@ -84,6 +84,8 @@ help: @echo " All targets operate on KIND_NAMESPACE (default: hypershell-system)." @echo "" @echo " kind-up Create cluster + deploy all components (OIDC enabled)" + @echo " LOCAL_IMAGES=true: build from working tree (default)" + @echo " LOCAL_IMAGES=true BUILD_SOURCE=baseline: build from origin/main" @echo " kind-down Remove namespace and its resources" @echo " kind-teardown Destroy Kind cluster, stop cloud-provider-kind" @echo " kind-status Show cluster info, pods, services, swap state" @@ -282,7 +284,7 @@ test-all: install-js # ============================================================================ export CONTAINER_ENGINE KIND_CLUSTER_NAME KIND_NAMESPACE -export KIND_HOT_RELOAD KIND_HOST_MOUNT_PATH KIND_KEYCLOAK_URL LOCAL_IMAGES +export KIND_HOT_RELOAD KIND_HOST_MOUNT_PATH KIND_KEYCLOAK_URL LOCAL_IMAGES BUILD_SOURCE export KIND_PULL_SECRET KIND_DB_IMAGE export GATEWAY_API_VERSION KIND_VERSION CLOUD_PROVIDER_KIND_REPO CLOUD_PROVIDER_KIND_REF CLOUD_PROVIDER_KIND_BRANCH CERT_MANAGER_VERSION AGENT_SANDBOX_VERSION export IMAGE_REGISTRY IMAGE_TAG KIND_CONFIG diff --git a/scripts/kind/build-images.sh b/scripts/kind/build-images.sh index 6486035c..354d7dce 100755 --- a/scripts/kind/build-images.sh +++ b/scripts/kind/build-images.sh @@ -5,7 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=lib.sh source "${SCRIPT_DIR}/lib.sh" -header "Building Baseline Images from origin/main" +REPO_ROOT="$(git rev-parse --show-toplevel)" +BUILD_SOURCE="${BUILD_SOURCE:-worktree}" WORKTREE_DIR="" cleanup_worktree() { @@ -13,30 +14,39 @@ cleanup_worktree() { git worktree remove --force "${WORKTREE_DIR}" 2>/dev/null || rm -rf "${WORKTREE_DIR}" fi } -trap cleanup_worktree EXIT -info "Fetching origin/main..." -git fetch origin main --quiet +if [[ "${BUILD_SOURCE}" == "baseline" ]]; then + header "Building Baseline Images from origin/main" + trap cleanup_worktree EXIT -WORKTREE_DIR=$(mktemp -d /tmp/hypershell-baseline-XXXXXX) -rm -rf "${WORKTREE_DIR}" -git worktree add --detach "${WORKTREE_DIR}" origin/main --quiet -info "Building from origin/main ($(git -C "${WORKTREE_DIR}" rev-parse --short HEAD))" + info "Fetching origin/main..." + git fetch origin main --quiet + + WORKTREE_DIR=$(mktemp -d /tmp/hypershell-baseline-XXXXXX) + rm -rf "${WORKTREE_DIR}" + git worktree add --detach "${WORKTREE_DIR}" origin/main --quiet + BUILD_DIR="${WORKTREE_DIR}" + info "Building from origin/main ($(git -C "${WORKTREE_DIR}" rev-parse --short HEAD))" +else + header "Building Images from Working Tree" + BUILD_DIR="${REPO_ROOT}" + info "Building from working tree ($(git rev-parse --short HEAD))" +fi info "Building API server..." ${CONTAINER_ENGINE} build -t "${api_server_local}" \ - -f "${WORKTREE_DIR}/components/api-server/Dockerfile" \ + -f "${BUILD_DIR}/components/api-server/Dockerfile" \ --build-arg GIT_VERSION="${build_version}" \ --build-arg BUILD_TIME="${build_time}" \ - "${WORKTREE_DIR}/components/api-server" + "${BUILD_DIR}/components/api-server" info "Building control plane..." ${CONTAINER_ENGINE} build -t "${control_plane_local}" \ - -f "${WORKTREE_DIR}/components/control-plane/Dockerfile" "${WORKTREE_DIR}" + -f "${BUILD_DIR}/components/control-plane/Dockerfile" "${BUILD_DIR}" info "Building web console..." ${CONTAINER_ENGINE} build -t "${web_console_local}" \ - -f "${WORKTREE_DIR}/components/web-console/Dockerfile" "${WORKTREE_DIR}" + -f "${BUILD_DIR}/components/web-console/Dockerfile" "${BUILD_DIR}" success "All images built" @@ -47,8 +57,12 @@ ${CONTAINER_ENGINE} tag "${web_console_local}" "${web_console_ref}" if cluster_exists; then info "Loading images into Kind cluster..." - kind load docker-image "${api_server_ref}" --name "${KIND_CLUSTER_NAME}" - kind load docker-image "${control_plane_ref}" --name "${KIND_CLUSTER_NAME}" - kind load docker-image "${web_console_ref}" --name "${KIND_CLUSTER_NAME}" + tmpdir=$(mktemp -d /tmp/kind-images-XXXXXX) + for img in "${api_server_ref}" "${control_plane_ref}" "${web_console_ref}"; do + archive="${tmpdir}/$(echo "${img}" | tr '/:' '__').tar" + ${CONTAINER_ENGINE} save "${img}" -o "${archive}" + kind load image-archive "${archive}" --name "${KIND_CLUSTER_NAME}" + done + rm -rf "${tmpdir}" success "Images loaded into Kind" fi diff --git a/specs/platform/local-development.spec.md b/specs/platform/local-development.spec.md index 410cc58a..830a8bb9 100644 --- a/specs/platform/local-development.spec.md +++ b/specs/platform/local-development.spec.md @@ -6,7 +6,7 @@ ## Purpose -HyperShell provides a single-command local development environment using Kind (Kubernetes in Docker) clusters. The environment deploys all platform components - API server, control plane, and web console - so developers can test changes end-to-end without external infrastructure. The database is provisioned by the control plane reconciler, not by `kind-up` directly. The tooling is idempotent: running it repeatedly converges to the desired `main` state without errors. For offline or air-gapped environments, `LOCAL_IMAGES=true` builds all baseline images from the local `main` branch instead of pulling from the registry. +HyperShell provides a single-command local development environment using Kind (Kubernetes in Docker) clusters. The environment deploys all platform components - API server, control plane, and web console - so developers can test changes end-to-end without external infrastructure. The database is provisioned by the control plane reconciler, not by `kind-up` directly. The tooling is idempotent: running it repeatedly converges to the desired `main` state without errors. For offline or air-gapped environments, `LOCAL_IMAGES=true` builds all images from the working tree instead of pulling from the registry. To build from `origin/main` instead (e.g. for baseline comparison), set `BUILD_SOURCE=baseline`. Developers selectively swap individual components with local builds using per-component targets. The baseline cluster runs pre-built images pulled from the container registry; individual components are "swapped in" from local source as needed. Selective swapping converges to the current working tree state. @@ -585,29 +585,36 @@ The system SHALL pull baseline images from the container registry at `quay.io/re ### Requirement: Offline Development (LOCAL_IMAGES) -The system SHALL support offline development by building all baseline images from the local repository instead of pulling from the container registry. When `LOCAL_IMAGES=true` is set, `make kind-up` SHALL build every component image from `origin/main` and load them into the Kind cluster. Repeated `kind-up` invocations with `LOCAL_IMAGES=true` SHALL rebuild from `origin/main`, picking up any new commits - analogous to a `git fetch` for images. +The system SHALL support offline development by building all images from the local repository instead of pulling from the container registry. When `LOCAL_IMAGES=true` is set, `make kind-up` SHALL build every component image from the working tree (current branch) and load them into the Kind cluster. This ensures the deployed images match the scripts, manifests, and seed data on the current branch. To build from `origin/main` instead (e.g. for baseline comparison), set `BUILD_SOURCE=baseline`. | Env Var | Default | Description | |---------|---------|-------------| -| `LOCAL_IMAGES` | (unset - pull from registry) | Set to `true` to build baseline images from the local repository instead of pulling from the container registry | +| `LOCAL_IMAGES` | (unset - pull from registry) | Set to `true` to build images locally instead of pulling from the container registry | +| `BUILD_SOURCE` | `worktree` | Image build source: `worktree` (current branch) or `baseline` (`origin/main`) | #### Scenario: First Run - Offline - GIVEN no Kind cluster exists - AND the developer has no access to the container registry - AND `LOCAL_IMAGES=true` is set - WHEN the developer runs `make kind-up` -- THEN all component images SHALL be built from the local repository +- THEN all component images SHALL be built from the working tree - AND images SHALL be loaded into the Kind cluster - AND the cluster SHALL reach a ready state without any registry pulls for platform components -#### Scenario: Subsequent Run - Rebuild from Main +#### Scenario: Subsequent Run - Rebuild from Working Tree - GIVEN a Kind cluster is running with locally-built images - AND `LOCAL_IMAGES=true` is set - WHEN the developer runs `make kind-up` again -- THEN all non-swapped component images SHALL be rebuilt from `origin/main` +- THEN all non-swapped component images SHALL be rebuilt from the working tree - AND updated images SHALL be loaded into the Kind cluster - AND swapped components SHALL be preserved +#### Scenario: Baseline Build from origin/main +- GIVEN `LOCAL_IMAGES=true` and `BUILD_SOURCE=baseline` are set +- WHEN the developer runs `make kind-up` +- THEN all component images SHALL be built from `origin/main` +- AND images SHALL be loaded into the Kind cluster + ### Requirement: Red Hat Hardened Images All container images deployed into the Kind cluster SHALL use [Red Hat Hardened Images](https://images.redhat.com/) (HI). HI images are distroless, CIS-hardened, and signed at build time. @@ -668,7 +675,8 @@ All `kind-*` targets operate on the namespace specified by `KIND_NAMESPACE` (def | `KIND_PULL_SECRET` | (unset) | Path to a Kubernetes pull secret YAML file; applied to the target namespace for HI image access | | `IMAGE_REGISTRY` | `quay.io/redhat-services-prod/hcm-eng-prod-tenant/hypershell-main` | Container registry path for baseline images | | `IMAGE_TAG` | `latest` | Image tag for baseline images | -| `LOCAL_IMAGES` | (unset - pull from registry) | Set to `true` to build baseline images from `origin/main` instead of pulling from registry | +| `LOCAL_IMAGES` | (unset - pull from registry) | Set to `true` to build images locally instead of pulling from registry | +| `BUILD_SOURCE` | `worktree` | Image build source when `LOCAL_IMAGES=true`: `worktree` (current branch) or `baseline` (`origin/main`) | | `CONTAINER_ENGINE` | Auto-detected (Podman preferred) | Container engine (`podman` or `docker`) | | `GATEWAY_API_VERSION` | (pinned in Makefile) | Gateway API CRD release version | | `CLOUD_PROVIDER_KIND_REPO` | (pinned in Makefile) | Git repository URL for cloud-provider-kind fork (BackendTLSPolicy + ALPN h2 support) | @@ -702,7 +710,7 @@ All targets operate on `KIND_NAMESPACE` (default: `hypershell-system`). | Decision | Rationale | |----------|-----------| | Registry pull for baseline images | Faster setup; no local build required for baseline; per-component swap handles local development | -| Per-component swap for iterative development | More ergonomic than blanket rebuild; discoverable via tab-completion; `LOCAL_IMAGES=true` serves a separate purpose - offline baseline builds from `main` when registry access is unavailable | +| Per-component swap for iterative development | More ergonomic than blanket rebuild; discoverable via tab-completion; `LOCAL_IMAGES=true` builds from the working tree by default so images match the branch's scripts and manifests; `BUILD_SOURCE=baseline` optionally builds from `origin/main` for comparison | | Hostname routing via networking Gateway as default | All component services route through the networking Gateway using HTTPRoute resources at `*.hypershell.localhost`. Developers access services by name (`api.hypershell.localhost`) instead of memorizing port numbers. Multi-namespace deployments get distinct hostnames without any per-hostname configuration thanks to wildcard DNS | | CoreDNS for wildcard DNS | A CoreDNS container resolves all `*.localhost` to loopback, eliminating per-hostname `/etc/hosts` management. OS resolver config routes `.localhost` queries to CoreDNS (macOS: `/etc/resolver/localhost`; Linux: `resolvectl`). Multi-namespace hostnames work automatically | | OS-native port forwarding (pfctl/iptables) | Redirects host:443 to cloud-provider-kind's ephemeral port, enabling clean `https://` URLs. Workaround until cloud-provider-kind supports publishing on specific host ports. Graceful fallback: if sudo fails, URLs show the ephemeral port suffix | From 1977ebd978d4e27fadebb2c63769b5619ff1bb65 Mon Sep 17 00:00:00 2001 From: Angel Marin Date: Wed, 19 Aug 2026 11:34:23 +0200 Subject: [PATCH 2/2] [HYPERSHELL-111] fix(kind): use localhost image refs with kustomize transformer Stop tagging local images with registry refs and loading them under quay.io names. Instead, load the localhost/ images directly and use a kustomize images transformer at manifest-apply time to map registry refs to localhost/ equivalents. This way pods start with the correct image on the first rollout -- kubectl describe pod shows localhost/hypershell-controller:dev instead of a registry ref that could be mistaken for a remote pull. Also fix the "empty patch" error during FORCE_ROLLOUT: track which deployments were already restarted by the operational section (JWKS timing, watch-stream timing) and skip them in the FORCE_ROLLOUT loop to avoid duplicate rollout restart calls within the same second. Co-Authored-By: Claude Opus 4.6 --- scripts/kind/build-images.sh | 7 +---- scripts/kind/up.sh | 38 ++++++++++++++++++++++-- specs/platform/local-development.spec.md | 27 +++++++++++++---- 3 files changed, 58 insertions(+), 14 deletions(-) diff --git a/scripts/kind/build-images.sh b/scripts/kind/build-images.sh index 354d7dce..37e75416 100755 --- a/scripts/kind/build-images.sh +++ b/scripts/kind/build-images.sh @@ -50,15 +50,10 @@ ${CONTAINER_ENGINE} build -t "${web_console_local}" \ success "All images built" -info "Tagging images with registry refs..." -${CONTAINER_ENGINE} tag "${api_server_local}" "${api_server_ref}" -${CONTAINER_ENGINE} tag "${control_plane_local}" "${control_plane_ref}" -${CONTAINER_ENGINE} tag "${web_console_local}" "${web_console_ref}" - if cluster_exists; then info "Loading images into Kind cluster..." tmpdir=$(mktemp -d /tmp/kind-images-XXXXXX) - for img in "${api_server_ref}" "${control_plane_ref}" "${web_console_ref}"; do + for img in "${api_server_local}" "${control_plane_local}" "${web_console_local}"; do archive="${tmpdir}/$(echo "${img}" | tr '/:' '__').tar" ${CONTAINER_ENGINE} save "${img}" -o "${archive}" kind load image-archive "${archive}" --name "${KIND_CLUSTER_NAME}" diff --git a/scripts/kind/up.sh b/scripts/kind/up.sh index 8314690a..2891fdc3 100755 --- a/scripts/kind/up.sh +++ b/scripts/kind/up.sh @@ -199,7 +199,6 @@ echo "" FORCE_ROLLOUT="" if [[ "${LOCAL_IMAGES:-}" == "true" ]]; then header "Local Images" - info "Building baseline images from origin/main..." "${SCRIPT_DIR}/build-images.sh" FORCE_ROLLOUT=true echo "" @@ -240,8 +239,33 @@ echo "" # --- Deploy all components via kustomize --- header "Deploying Components" -info "Applying Kind manifests via kustomize..." -kustomize build deploy/kind | kube apply -f - +if [[ "${LOCAL_IMAGES:-}" == "true" ]]; then + info "Applying Kind manifests with localhost image refs..." + _kustomize_dir="deploy/.local-images" + mkdir -p "${_kustomize_dir}" + _registry="${IMAGE_REGISTRY:-quay.io/redhat-services-prod/hcm-eng-prod-tenant/hypershell-main}" + cat > "${_kustomize_dir}/kustomization.yaml" <-up` rebuilds from the working tree and replaces the deployment, even if already swapped; developers iterate by re-running the same target | | Web console as first-class component | Node.js frontend (`components/web-console/`) deployed alongside API server and control plane; supports hot reload via `KIND_HOT_RELOAD` for rapid UI iteration | | Hot reload on by default | Swap targets for supported components (web console) mount host source and run a dev server in an interactive TTY by default; `KIND_HOT_RELOAD=false` opts out to rebuild-and-replace. Keeps the same `kind--up` entrypoint for both workflows |