diff --git a/.github/workflows/hypershell.yml b/.github/workflows/hypershell.yml deleted file mode 100644 index 9e4d122..0000000 --- a/.github/workflows/hypershell.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: HyperShell - -on: - push: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: hypershell-${{ github.repository }}-${{ github.ref }} - cancel-in-progress: false - -jobs: - canonical-remote: - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v4 - with: - # This job builds and runs repository-controlled code; don't leave the - # GITHUB_TOKEN in the local git config for it to reach. - persist-credentials: false - - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - - name: Build harness - run: CGO_ENABLED=0 go build -ldflags '-s -w -X main.version=ci' -o harness . - - # Platform bootstrap adds this service-account subject to the gateway's - # default workspace once. No administrator credential belongs in the repo. - - name: Run canonical remote lifecycle - env: - HYPERSHELL_GATEWAY: ${{ secrets.HYPERSHELL_GATEWAY }} - HYPERSHELL_OIDC_ISSUER: ${{ secrets.HYPERSHELL_OIDC_ISSUER }} - HYPERSHELL_OIDC_AUDIENCE: ${{ secrets.HYPERSHELL_OIDC_AUDIENCE }} - HYPERSHELL_SANDBOX_SA_ID: ${{ secrets.HYPERSHELL_SANDBOX_SA_ID }} - OPENSHELL_OIDC_CLIENT_SECRET: ${{ secrets.HYPERSHELL_SANDBOX_SA_SECRET }} - HYPERSHELL_SANDBOX_NAME: sdk-smoke-${{ github.run_id }}-${{ github.run_attempt }} - run: | - output="$(./harness apply "$HYPERSHELL_SANDBOX_NAME" \ - --file test/hypershell-workflow.yaml)" - echo "$output" - grep -q 'canonical-sdk-ok' <<<"$output" diff --git a/Makefile b/Makefile index 27bcfde..904a41a 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ OPENSHELL_VERSION := $(shell cat .openshell-version 2>/dev/null) IMAGE := $(REGISTRY):sandbox-$(VERSION) .PHONY: all cli openshell \ - vet lint test test-local test-kind test-remote test-all \ + vet lint test test-local test-kind test-remote test-hypershell test-all \ dev-sandbox dev-push tag clean help ## ── CLI ────────────────────────────────────────────────────────────── @@ -102,6 +102,13 @@ test-remote: cli dev-push @echo "" HARNESS_OS_IMAGE=$(IMAGE) ./test/test-flow.sh openshift +## Managed HyperShell: canonical remote lifecycle against the real gateway. +## Runs LOCALLY only (the OIDC issuer is VPN-only, unreachable from CI runners). +## Point HYPERSHELL_SA_ENV at a git-excluded SA env file; must be on the RH VPN. +test-hypershell: cli + @test -n "$${HYPERSHELL_SA_ENV}" || { echo "ERROR: set HYPERSHELL_SA_ENV=path/to/sa.env"; exit 1; } + ./test/hypershell-lifecycle.sh + ## All: unit + local + kind + remote test-all: test test-local test-kind test-remote diff --git a/test/hypershell-lifecycle.sh b/test/hypershell-lifecycle.sh new file mode 100755 index 0000000..de492bf --- /dev/null +++ b/test/hypershell-lifecycle.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Canonical remote lifecycle against a managed HyperShell gateway, run LOCALLY. +# +# Drives the same path CI would: build the CLI, `harness apply` a throwaway +# sandbox via the OIDC service account, exec the agent, and assert the marker +# `canonical-sdk-ok`. The sandbox is keep:false, so the gateway deletes it when +# the run ends (create -> exec -> auto-delete). +# +# Why local, not GitHub Actions: the managed gateway is public, but the OIDC +# issuer (Keycloak) resolves to private RFC1918 IPs (the ROSA cluster apps +# ingress), so a public GitHub-hosted runner times out on issuer discovery +# during the client-credentials flow. You must run this from inside the Red Hat +# network (VPN). A pre-flight below checks issuer reachability and says so. +# +# Credentials come from a git-excluded SA env file (never committed). Point +# HYPERSHELL_SA_ENV at it. It must define (see hypershell-service-account-*.env): +# HYPERSHELL_GATEWAY, OPENSHELL_OIDC_ISSUER, OPENSHELL_OIDC_AUDIENCE, +# OPENSHELL_OIDC_CLIENT_ID, OPENSHELL_OIDC_CLIENT_SECRET +# +# Usage: +# HYPERSHELL_SA_ENV=./hypershell-service-account-user.env ./test/hypershell-lifecycle.sh +# make test-hypershell HYPERSHELL_SA_ENV=./hypershell-service-account-user.env +set -uo pipefail + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +WORKFLOW_FILE="$ROOT_DIR/test/hypershell-workflow.yaml" + +# CI-safe path: this test is deliberately VPN- and credential-gated (see the +# header) and has no runnable CI mode — a public runner cannot reach the OIDC +# issuer. When CI is set, skip cleanly with a zero exit instead of failing on +# the missing SA env below, so a generic test/**.sh runner stays green. +if [[ -n "${CI:-}" ]]; then + echo "SKIP: hypershell-lifecycle is VPN/credential-gated and does not run in CI." + exit 0 +fi + +: "${HYPERSHELL_SA_ENV:?set HYPERSHELL_SA_ENV=path/to/sa.env (git-excluded SA credentials)}" +[[ -f "$HYPERSHELL_SA_ENV" ]] || { echo "ERROR: SA env file not found: $HYPERSHELL_SA_ENV" >&2; exit 1; } +[[ -f "$WORKFLOW_FILE" ]] || { echo "ERROR: workflow file not found: $WORKFLOW_FILE" >&2; exit 1; } + +# Load SA credentials (subshell-safe: only the vars we map are re-exported). +set -a; # shellcheck disable=SC1090 +. "$HYPERSHELL_SA_ENV"; set +a + +# Map the SA file's OPENSHELL_OIDC_* names onto the vars the config expands. +export HYPERSHELL_GATEWAY="${HYPERSHELL_GATEWAY:-}" +export HYPERSHELL_OIDC_ISSUER="${OPENSHELL_OIDC_ISSUER:-}" +export HYPERSHELL_OIDC_AUDIENCE="${OPENSHELL_OIDC_AUDIENCE:-}" +export HYPERSHELL_SANDBOX_SA_ID="${OPENSHELL_OIDC_CLIENT_ID:-}" +export OPENSHELL_OIDC_CLIENT_SECRET="${OPENSHELL_OIDC_CLIENT_SECRET:-}" + +miss=() +[[ -n "$HYPERSHELL_GATEWAY" ]] || miss+=(HYPERSHELL_GATEWAY) +[[ -n "$HYPERSHELL_OIDC_ISSUER" ]] || miss+=(OPENSHELL_OIDC_ISSUER) +[[ -n "$HYPERSHELL_OIDC_AUDIENCE" ]] || miss+=(OPENSHELL_OIDC_AUDIENCE) +[[ -n "$HYPERSHELL_SANDBOX_SA_ID" ]] || miss+=(OPENSHELL_OIDC_CLIENT_ID) +[[ -n "$OPENSHELL_OIDC_CLIENT_SECRET" ]] || miss+=(OPENSHELL_OIDC_CLIENT_SECRET) +((${#miss[@]}==0)) || { echo "ERROR: $HYPERSHELL_SA_ENV is missing: ${miss[*]}" >&2; exit 1; } + +secret_state() { [[ -n "${1:-}" ]] && printf 'set (%d chars)' "${#1}" || printf 'MISSING'; } +echo "=== HyperShell local lifecycle ===" +printf ' %-16s %s\n' gateway "$HYPERSHELL_GATEWAY" +printf ' %-16s %s\n' issuer "$HYPERSHELL_OIDC_ISSUER" +printf ' %-16s %s\n' audience "$HYPERSHELL_OIDC_AUDIENCE" +printf ' %-16s %s\n' "SA client" "$HYPERSHELL_SANDBOX_SA_ID" +printf ' %-16s %s\n' "SA secret" "$(secret_state "$OPENSHELL_OIDC_CLIENT_SECRET")" + +# Pre-flight: the OIDC issuer is VPN-only. Fail early with a clear message +# rather than after a 30s token-discovery timeout inside harness. +well_known="${HYPERSHELL_OIDC_ISSUER%/}/.well-known/openid-configuration" +if ! curl -fsS --max-time 8 -o /dev/null "$well_known" 2>/dev/null; then + echo "ERROR: OIDC issuer unreachable: $well_known" >&2 + echo " The issuer is on private (VPN-only) IPs. Are you on the Red Hat network?" >&2 + exit 1 +fi +echo " issuer reachable: yes" + +# Always rebuild the CLI from the checked-out source so this run never validates +# a stale harness binary left at the repo root by a prior build. +HARNESS_BIN="$ROOT_DIR/harness" +echo "building harness ..." +( cd "$ROOT_DIR" && CGO_ENABLED=0 go build -ldflags '-s -w -X main.version=dev' -o harness . ) \ + || { echo "ERROR: harness build failed" >&2; exit 1; } + +# Gateway caps sandbox names at 19 chars. "hsl-" (4) + epoch (10) + "-" (1) + +# 3 hex (3) = 18, staying under the cap while adding a random suffix so +# concurrent runs don't collide on one-second timestamp resolution. +name="hsl-$(date +%s)-$(printf '%03x' $((RANDOM % 4096)))" + +# keep:false means the gateway auto-deletes the sandbox on normal completion, +# but a Ctrl-C (SIGINT/SIGTERM) kills harness before its deferred delete runs +# and would leak the sandbox. Trap those signals and delete it ourselves. +# shellcheck disable=SC2329 # invoked indirectly via the trap below +cleanup() { echo "interrupted; deleting $name ..." >&2; "$HARNESS_BIN" delete "$name" >/dev/null 2>&1 || true; exit 130; } +trap cleanup INT TERM + +echo "=== apply $name ===" +out="$("$HARNESS_BIN" apply "$name" --file "$WORKFLOW_FILE" 2>&1)"; rc=$? +echo "$out" +if [[ $rc -eq 0 ]] && grep -q 'canonical-sdk-ok' <<<"$out"; then + echo "RESULT: PASS (canonical-sdk-ok; sandbox auto-deleted)" + exit 0 +fi +echo "RESULT: FAIL (apply exit=$rc; marker not found)" >&2 +exit 1