Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
51 changes: 30 additions & 21 deletions scripts/kind/build-images.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,50 +5,59 @@ 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() {
if [[ -n "${WORKTREE_DIR}" ]] && [[ -d "${WORKTREE_DIR}" ]]; then
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

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))"
if [[ "${BUILD_SOURCE}" == "baseline" ]]; then
header "Building Baseline Images from origin/main"
trap cleanup_worktree EXIT

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"

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..."
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_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}"
done
rm -rf "${tmpdir}"
success "Images loaded into Kind"
fi
38 changes: 35 additions & 3 deletions scripts/kind/up.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down Expand Up @@ -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}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] The transformer name: fields below are built from _registry (${IMAGE_REGISTRY:-quay.io/...}), but the base manifests hardcode the quay.io/.../hypershell-*-main names in deploy/kind/kustomization.yaml:183-187 regardless of IMAGE_REGISTRY. If a developer overrides IMAGE_REGISTRY, the images transformer no longer matches any image in the base, so substitution silently no-ops and pods keep the unreachable registry refs — defeating LOCAL_IMAGES. Match the base's actual (hardcoded) image name for name:, keeping newName/newTag from *_local.

cat > "${_kustomize_dir}/kustomization.yaml" <<EOF
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../kind
images:
- name: ${_registry}/hypershell-api-server-main
newName: ${api_server_local%%:*}
newTag: ${api_server_local##*:}
- name: ${_registry}/hypershell-control-plane-main
newName: ${control_plane_local%%:*}
newTag: ${control_plane_local##*:}
- name: ${_registry}/hypershell-web-console-main
newName: ${web_console_local%%:*}
newTag: ${web_console_local##*:}
EOF
kustomize build "${_kustomize_dir}" | kube apply -f -
rm -rf "${_kustomize_dir}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] This rm -rf only runs on the happy path. Under set -euo pipefail, a failing kustomize build or kube apply above exits before cleanup, leaving deploy/.local-images/kustomization.yaml in the working tree. .gitignore ignores .local/ but not .local-images, so it can be committed by accident. Suggest a trap ... EXIT cleanup (like build-images.sh does for its worktree), or mktemp -d outside the source tree, or add deploy/.local-images/ to .gitignore.

else
info "Applying Kind manifests via kustomize..."
kustomize build deploy/kind | kube apply -f -
fi

info "Waiting for PostgreSQL..."
kube wait --for=condition=available deployment/hypershell-postgres -n "${KIND_NAMESPACE}" --timeout=300s
Expand Down Expand Up @@ -286,9 +310,12 @@ echo ""
# started before Keycloak was serving keys it is stuck in CrashLoopBackoff;
# restart it now that Keycloak is ready so a fresh pod (with no backoff delay)
# comes up on the first try instead of waiting out the backoff timer.
_api_restarted=""
_cp_restarted=""
if ! is_swapped api-server; then
info "Restarting API server now that Keycloak serves JWKS..."
kube rollout restart deployment/hypershell-api-server -n "${KIND_NAMESPACE}"
_api_restarted=true
fi

# The controller's gRPC watch streams must connect to a running API server.
Expand All @@ -303,6 +330,7 @@ fi
if ! is_swapped control-plane; then
info "Restarting control plane to establish watch streams..."
kube rollout restart deployment/hypershell-controller -n "${KIND_NAMESPACE}"
_cp_restarted=true
kube wait --for=condition=available deployment/hypershell-controller -n "${KIND_NAMESPACE}" --timeout=120s
fi

Expand Down Expand Up @@ -353,6 +381,10 @@ if [[ "${FORCE_ROLLOUT}" == "true" ]]; then
dep="${pair%%:*}"
comp="${pair##*:}"
if ! is_swapped "${comp}"; then
case "${dep}" in
hypershell-api-server) [[ -n "${_api_restarted}" ]] && continue ;;
hypershell-controller) [[ -n "${_cp_restarted}" ]] && continue ;;
esac
kube rollout restart "deployment/${dep}" -n "${KIND_NAMESPACE}"
fi
done
Expand Down
45 changes: 35 additions & 10 deletions specs/platform/local-development.spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -486,6 +486,8 @@ The system SHALL support both Podman and Docker as container engines. The engine

All image names and tags used across Makefile targets, Kind load commands, and Kubernetes manifests SHALL resolve to the same artifacts. This reinforces the cross-cutting convention in `specs/standards/platform/cross-cutting.spec.md`.

When `LOCAL_IMAGES=true`, the deployed image refs are the `localhost/` names (e.g. `localhost/hypershell-controller:dev`), not the registry refs. The substitution happens at kustomize apply time via an `images` transformer, so manifests reach the cluster with the correct refs -- no post-apply patching, no wasted rollout.

### Requirement: Security Context Compliance

All containers in the Kind deployment manifests SHALL set restricted security contexts per `specs/standards/security/security.spec.md`: `runAsNonRoot: true`, `capabilities.drop: ["ALL"]`, and `allowPrivilegeEscalation: false`.
Expand Down Expand Up @@ -585,29 +587,50 @@ 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), load them into the Kind cluster, and apply manifests with `localhost/` image refs so that pods start with local images on the first rollout. This ensures the deployed images match the scripts, manifests, and seed data on the current branch -- and `kubectl describe pod` shows the actual local image name (e.g. `localhost/hypershell-controller:dev`) rather than a registry ref that could be mistaken for a remote pull. To build from `origin/main` instead (e.g. for baseline comparison), set `BUILD_SOURCE=baseline`.

Image ref substitution SHALL happen at manifest-apply time via a kustomize `images` transformer, not as a post-apply `kubectl set image` patch. This avoids a wasted rollout where pods first start with registry refs (potentially failing to pull) and then get killed and replaced with local refs. The kustomize transformer maps each registry image name to its `localhost/` equivalent before the manifests reach the API server.

| 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`) |

Local image names:

| Component | Registry Ref | Local Image Ref |
|-----------|-------------|----------------|
| API server | `${IMAGE_REGISTRY}/hypershell-api-server-main` | `localhost/hypershell:dev` |
| Control plane | `${IMAGE_REGISTRY}/hypershell-control-plane-main` | `localhost/hypershell-controller:dev` |
| Web console | `${IMAGE_REGISTRY}/hypershell-web-console-main` | `localhost/hypershell-web-console:dev` |

#### 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
- AND images SHALL be loaded into the Kind cluster
- THEN all component images SHALL be built from the working tree with `localhost/` image names
- AND images SHALL be loaded into the Kind cluster via tarball archive
- AND kustomize SHALL apply manifests with a `localhost/` images transformer so pods start with local refs
- AND the cluster SHALL reach a ready state without any registry pulls for platform components
- AND `kubectl get pods -o jsonpath='{.items[*].spec.containers[*].image}'` SHALL show `localhost/` refs for locally-built 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 kustomize SHALL apply manifests with the `localhost/` images transformer
- 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` with `localhost/` image names
- AND images SHALL be loaded into the Kind cluster
- AND kustomize SHALL apply manifests with the `localhost/` images transformer

### 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.
Expand Down Expand Up @@ -668,7 +691,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) |
Expand Down Expand Up @@ -702,12 +726,13 @@ 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 |
| Pre-check cluster existence for idempotency | Check `kind get clusters` for the target name before attempting creation; skip if already present. Avoids `\|\| true` which swallows real failures (Docker not running, resource exhaustion) |
| Images loaded via tarball archive | Compatible with both Podman and Docker; avoids registry dependency |
| Images loaded via tarball archive | Compatible with both Podman and Docker; avoids registry dependency. `kind load docker-image` is Podman-incompatible; `kind load image-archive` works with any container engine |
| Localhost image refs for LOCAL_IMAGES via kustomize transformer | Local builds use `localhost/` image names (e.g. `localhost/hypershell-controller:dev`). A kustomize `images` transformer maps registry refs to localhost refs at manifest-apply time, so pods start with the correct image on the first rollout -- no wasted deploy-then-patch cycle. `kubectl describe pod` shows the actual local image, not a registry ref that could be mistaken for a remote pull |
| Rebuild-and-replace on every swap call | Each `kind-<component>-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-<component>-up` entrypoint for both workflows |
Expand Down
Loading