From b0a9ba55f1cd2b72dec399f2668e9c40d303c170 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 10 Jun 2026 15:11:50 -0700 Subject: [PATCH 1/2] X-Smart-Branch-Parent: main From 28d2a51e316ed8890f4313ac93717fafa7bf84a4 Mon Sep 17 00:00:00 2001 From: Robby Cochran Date: Wed, 10 Jun 2026 15:48:50 -0700 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20eliminate=20runner=20=E2=80=94?= =?UTF-8?q?=20all=20targets=20use=20direct=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The in-cluster runner Job existed because the OCP gateway was originally only reachable via cluster-internal DNS. But harness deploy already creates an OpenShift Route with external access and extracts mTLS certs, so the local CLI can talk to the gateway directly. This removes the entire runner path: the runner image, the launch command, the launcher RBAC, and the two-mode (launcher/direct) gateway config. All targets now use the same code path — deploy gateway if needed, register providers from the workstation, render payload locally, create sandbox directly via the openshell CLI. - Delete build/runner/, cmd/launch.go, gateways/ocp/addons/rbac.yaml - Delete upRemote(), defaultRunnerImage(), runnerEnv(), LauncherSection - Route --remote through upLocal with auto-deploy via deployFromConfig - Remove runner job from CI image workflow - Remove createViaRunner/profileHasCustomProviders from create.go - Extract mTLS certs for all non-local gateways (not just launcher mode) - Drop MTLS secret default (only extract when explicitly configured) -883 lines, one code path for all targets. --- .github/workflows/images.yml | 36 ------ .gitignore | 2 - AGENTS.md | 2 +- Makefile | 31 ++--- README.md | 1 - SPEC.md | 23 ++-- TODO.md | 10 +- build/runner/Dockerfile | 13 -- cmd/create.go | 62 +--------- cmd/create_test.go | 20 --- cmd/deploy.go | 8 +- cmd/deploy_test.go | 8 -- cmd/launch.go | 209 -------------------------------- cmd/launch_test.go | 139 --------------------- cmd/up.go | 191 ++--------------------------- cmd/up_test.go | 19 --- dev-harness.sh | 3 +- gateways/ocp/addons/rbac.yaml | 26 ---- gateways/ocp/gateway.toml | 8 +- internal/gateway/config.go | 36 ------ internal/gateway/config_test.go | 87 +++---------- main.go | 1 - sandbox/startup.sh | 2 +- 23 files changed, 54 insertions(+), 883 deletions(-) delete mode 100644 build/runner/Dockerfile delete mode 100644 cmd/launch.go delete mode 100644 cmd/launch_test.go delete mode 100644 gateways/ocp/addons/rbac.yaml diff --git a/.github/workflows/images.yml b/.github/workflows/images.yml index e065449..0e3b410 100644 --- a/.github/workflows/images.yml +++ b/.github/workflows/images.yml @@ -44,39 +44,3 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=registry,ref=${{ env.IMAGE_BASE }}:sandbox-cache cache-to: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && format('type=registry,ref={0}:sandbox-cache,mode=max', env.IMAGE_BASE) || '' }} - - runner: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - name: Build harness binary - run: | - VERSION=$(git describe --tags --always 2>/dev/null || echo dev) - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ - -ldflags "-s -w -X main.version=${VERSION}" \ - -o build/runner/harness . - - uses: docker/setup-buildx-action@v3 - - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - uses: docker/metadata-action@v5 - id: meta - with: - images: ${{ env.IMAGE_BASE }} - tags: | - type=semver,pattern=runner-v{{version}} - type=sha,prefix=runner-,format=short - - uses: docker/build-push-action@v6 - with: - context: build/runner - platforms: linux/amd64 - push: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=registry,ref=${{ env.IMAGE_BASE }}:runner-cache - cache-to: ${{ (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) && format('type=registry,ref={0}:runner-cache,mode=max', env.IMAGE_BASE) || '' }} diff --git a/.gitignore b/.gitignore index 8045a79..0af33bf 100644 --- a/.gitignore +++ b/.gitignore @@ -16,8 +16,6 @@ __pycache__/ # Build artifacts harness harness-openshell -build/runner/harness -build/runner/openshell infracluster/ .claude/ diff --git a/AGENTS.md b/AGENTS.md index 1805b0b..9be95f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -73,13 +73,13 @@ Current workarounds and their upstream tracking: | Custom sandbox image | Adds mcp-atlassian and GWS CLI to community base | Upstreaming MCP integrations | | `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1` | Vertex AI rejects `context_management` beta header | Anthropic/Google to align APIs | | Atlassian `JIRA_URL`/`JIRA_USERNAME` as `--config` material | Provider v2 config keys not injected as env vars yet | OpenShell roadmap | -| In-cluster runner Job | OpenShell doesn't have a native K8s-triggered sandbox creation | Potential future CRD | Previously worked around, now resolved: | Was | Resolution | |-----|-----------| | GWS credential export/upload to sandbox | GWS is now a native provider (`google-workspace`). Gateway manages OAuth refresh via `oauth2-refresh-token` strategy + `request_body_credential_rewrite` on `oauth2.googleapis.com`. Sandbox gets a proxy-resolved placeholder. | +| In-cluster runner Job | Eliminated — all targets now use direct mode. The gateway is accessible via external Route + mTLS, so sandboxes are created from the user's machine. | ## Validation diff --git a/Makefile b/Makefile index 5e2864c..ad61e87 100644 --- a/Makefile +++ b/Makefile @@ -20,11 +20,10 @@ VERSION := $(shell git describe --tags --always 2>/dev/null || echo dev) LDFLAGS := -s -w -X main.version=$(VERSION) DEV_SANDBOX_IMAGE := $(REGISTRY):sandbox-$(VERSION) -DEV_RUNNER_IMAGE := $(REGISTRY):runner-$(VERSION) -.PHONY: all cli cli-runner \ +.PHONY: all cli \ vet lint test test-local test-kind test-remote test-all \ - dev-sandbox dev-runner dev-push clean help + dev-sandbox dev-push clean help ## ── CLI ────────────────────────────────────────────────────────────── @@ -36,13 +35,6 @@ cli: CGO_ENABLED=0 go build -ldflags '$(LDFLAGS)' -o harness . @echo "Built: ./harness ($(VERSION))" -## ── Images ──────────────────────────────────────────────────────────── - -## Cross-compile harness binary for the runner image -cli-runner: - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags '$(LDFLAGS)' -o build/runner/harness . - @echo "Built: build/runner/harness ($(VERSION))" - ## ── Lint targets ───────────────────────────────────────────────────── ## Run go vet @@ -80,10 +72,10 @@ test-kind: cli SANDBOX_IMAGE=$(DEV_SANDBOX_IMAGE) CONTAINER_CLI=$(CONTAINER_CLI) ./test/kind-lifecycle.sh $(if $(KEEP),--keep) ## Remote (OCP): requires KUBECONFIG set -test-remote: cli dev-sandbox dev-runner +test-remote: cli dev-sandbox @test -n "$${KUBECONFIG}" || { echo "ERROR: Set KUBECONFIG for OCP (e.g. export KUBECONFIG=infracluster/kubeconfig)"; exit 1; } @echo "" - SANDBOX_IMAGE=$(DEV_SANDBOX_IMAGE) RUNNER_IMAGE=$(DEV_RUNNER_IMAGE) ./test/test-flow.sh ocp + SANDBOX_IMAGE=$(DEV_SANDBOX_IMAGE) ./test/test-flow.sh ocp ## All: unit + local + kind + remote test-all: test test-local test-kind test-remote @@ -95,27 +87,20 @@ dev-sandbox: $(CONTAINER_CLI) build -t $(DEV_SANDBOX_IMAGE) sandbox/ @echo "Built: $(DEV_SANDBOX_IMAGE)" -## Build dev runner image locally -dev-runner: cli-runner - $(CONTAINER_CLI) build --platform $(PLATFORM) -t $(DEV_RUNNER_IMAGE) build/runner/ - @echo "Built: $(DEV_RUNNER_IMAGE)" - -## Build and push dev images (sandbox: multi-arch, runner: amd64) -dev-push: cli-runner +## Build and push dev sandbox image (multi-arch) +dev-push: @$(CONTAINER_CLI) rmi --force $(DEV_SANDBOX_IMAGE) 2>/dev/null || true @$(CONTAINER_CLI) manifest rm $(DEV_SANDBOX_IMAGE) 2>/dev/null || true $(CONTAINER_CLI) build --platform linux/amd64 --manifest $(DEV_SANDBOX_IMAGE) sandbox/ $(CONTAINER_CLI) build --platform linux/arm64 --manifest $(DEV_SANDBOX_IMAGE) sandbox/ $(CONTAINER_CLI) manifest push $(DEV_SANDBOX_IMAGE) - $(CONTAINER_CLI) build --platform $(PLATFORM) -t $(DEV_RUNNER_IMAGE) build/runner/ - $(CONTAINER_CLI) push $(DEV_RUNNER_IMAGE) - @echo "Pushed: $(DEV_SANDBOX_IMAGE) (multi-arch) $(DEV_RUNNER_IMAGE) (amd64)" + @echo "Pushed: $(DEV_SANDBOX_IMAGE) (multi-arch)" ## ── Convenience targets ─────────────────────────────────────────────── ## Clean built binaries clean: - rm -f harness build/runner/harness build/runner/openshell + rm -f harness @echo "Cleaned binaries" ## Show available targets diff --git a/README.md b/README.md index be8c2c4..dd13474 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,6 @@ See the [OpenShell docs](https://github.com/NVIDIA/OpenShell) for the full secur | `gateways/*/gateway.toml` | Deployment target config: `local/` (Podman), `kind/`, `ocp/` (OpenShift) | | `openshell.toml` | Deployment-level overrides (enabled providers, inference model, chart version) | | `sandbox/Dockerfile` | Sandbox image: OpenShell base + MCP servers + CLI tools | -| `build/runner/Dockerfile` | Runner image: harness binary for in-cluster sandbox creation | | `sandbox/policy.yaml` | Network egress rules applied to sandboxes | ## Commands diff --git a/SPEC.md b/SPEC.md index 071cbe6..0de5d34 100644 --- a/SPEC.md +++ b/SPEC.md @@ -60,8 +60,7 @@ Full flow: deploy gateway, register providers, render agent config, create sandb 4. **Render payload** -- `env.sh` (resolved env vars), `run.sh` (entrypoint wrapper), `task.md` (if set). 5. **Create sandbox** -- `openshell sandbox create` with `--upload` (payload) and the startup command. Retry up to 5 times for supervisor race conditions. -Local: sandbox created directly via the openshell CLI on the user's machine. -Remote: a runner Job is deployed to the cluster (`harness launch`), which creates the sandbox from inside the cluster with mTLS gateway access. +Both local and remote targets create the sandbox directly from the user's machine. Remote gateways are accessed via an external Route endpoint with mTLS authentication. ### `harness create [--agent NAME] [-f FILE] [--name SANDBOX]` @@ -99,10 +98,6 @@ Stop or start a sandbox without deleting it. When NAME is omitted and exactly on Tear down resources. At least one flag required. -### `harness launch` (hidden) - -In-cluster command for the runner Job. Reads agent config from `/etc/openshell/sandbox/agent.yaml` (mounted ConfigMap), configures mTLS gateway, renders payload, creates sandbox, sets up environment. Not meant for direct user invocation. - ## Config Files | File | Purpose | @@ -113,17 +108,16 @@ In-cluster command for the runner Job. Reads agent config from `/etc/openshell/s | `gateways/*/gateway.toml` | Deployment target config with Helm, images, RBAC | | `openshell.toml` | Deployment-level overrides (enabled providers, inference model) | | `sandbox/Dockerfile` | Sandbox image: OpenShell base + MCP servers + CLI tools | -| `build/runner/Dockerfile` | Runner image: harness binary + openshell CLI | | `sandbox/policy.yaml` | Network egress rules applied to sandboxes | ## Image Tags -All images are published to `ghcr.io/robbycochran/harness-openshell`. CI never publishes floating tags (`:latest`, `:sandbox`, `:runner`); the bare `:sandbox` fallback below exists only for local `go build` binaries without version ldflags. +All images are published to `ghcr.io/robbycochran/harness-openshell`. CI never publishes floating tags (`:latest`, `:sandbox`); the bare `:sandbox` fallback below exists only for local `go build` binaries without version ldflags. -| Trigger | Sandbox | Runner | -|---------|---------|--------| -| Release `v0.1.2` | `:sandbox-v0.1.2` | `:runner-v0.1.2` | -| Any push/PR | `:sandbox-` | `:runner-` | +| Trigger | Sandbox | +|---------|---------| +| Release `v0.1.2` | `:sandbox-v0.1.2` | +| Any push/PR | `:sandbox-` | The CLI resolves images from its embedded version (set via `-ldflags` at build time): @@ -131,14 +125,13 @@ The CLI resolves images from its embedded version (set via `-ldflags` at build t - `v0.1.2-5-gabc1234` → `:sandbox-v0.1.2-5-gabc1234` (dev build, matches `make dev-sandbox`) - `dev` → `:sandbox` (bare `go build` without ldflags) -`SANDBOX_IMAGE` and `RUNNER_IMAGE` env vars override the version-based resolution. +`SANDBOX_IMAGE` env var overrides the version-based resolution. ## Environment Variables | Variable | Purpose | |----------|---------| | `SANDBOX_IMAGE` | Override sandbox image (dev/CI builds) | -| `RUNNER_IMAGE` | Override runner image (dev/CI builds) | | `HARNESS_DIR` | Override harness directory detection | | `OPENSHELL_NAMESPACE` | Override K8s namespace (default: `openshell`) | | `OPENSHELL_CLI` | Override openshell binary path | @@ -148,7 +141,7 @@ The CLI resolves images from its embedded version (set via `-ldflags` at build t | `CONFIG_TOML` / `PROVIDERS_TOML` | Override paths to `openshell.toml` / `providers.toml` (preflight) | | `KUBECONFIG` | K8s cluster config for remote targets | -`GATEWAY_ENDPOINT` and `GATEWAY_NAME` are internal — set on the in-cluster runner Job, not by users. +`GATEWAY_NAME` is internal — used by env override in gateway config, not typically set by users. ## Payload diff --git a/TODO.md b/TODO.md index 6cc9953..f70516a 100644 --- a/TODO.md +++ b/TODO.md @@ -10,9 +10,7 @@ - Prerequisite: proto files stabilize (OpenShell is alpha) ### Image registry as gateway config vs env override -- gateway.toml `[images]` section sets sandbox/runner image refs -- `SANDBOX_IMAGE`/`RUNNER_IMAGE` env vars override config (for dev/CI) -- Two sources of truth: gateway.toml hardcodes a registry, env vars override it +- `SANDBOX_IMAGE` env var overrides the version-based image resolution (for dev/CI) - Consider: gateway.toml uses a `registry` field and images are relative to it ### registerProviders should filter by agent's provider list @@ -49,14 +47,12 @@ ## Testing ### Current coverage -- Go unit tests across cmd/ (including launch.go) and all internal/ packages +- Go unit tests across cmd/ and all internal/ packages - 29 bats preflight tests (run in CI via `.github/workflows/ci.yml`) - Integration: local + kind + OCP via `make test-all` ### Gaps - [ ] Integration test for `providers --force` -- [ ] Unit test for the full `runLaunch` orchestration (currently only its - helpers — configureGateway, checkProviders, launchCreateSandbox — are tested) ## Release @@ -66,8 +62,6 @@ ## Deferred (post-0.1) -- [ ] Rename K8s SA `openshell-launcher` -> `openshell-runner` (breaking for deployed OCP clusters) -- [ ] Rename `LauncherSection` -> `RunnerSection` in gateway config TOML - [ ] Gateway-level LLM proxy/logging (gateway.toml `[proxy]` section) - [ ] Multi-agent workflow support (fleet.yaml / workflow.yaml) - [ ] `harness policy suggest` (DenialEvent stream -> policy proposals) diff --git a/build/runner/Dockerfile b/build/runner/Dockerfile deleted file mode 100644 index e332d81..0000000 --- a/build/runner/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM registry.access.redhat.com/ubi9/ubi-minimal:latest - -RUN microdnf install -y openssh-clients tar gzip --nodocs && microdnf clean all - -ARG OPENSHELL_VERSION=0.0.58 -RUN curl -LsSf "https://github.com/NVIDIA/OpenShell/releases/download/v${OPENSHELL_VERSION}/openshell-x86_64-unknown-linux-musl.tar.gz" \ - | tar xz -C /usr/local/bin openshell - -COPY harness /usr/local/bin/harness - -USER 1000 - -ENTRYPOINT ["harness", "launch"] diff --git a/cmd/create.go b/cmd/create.go index 408eac8..22825d4 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -5,7 +5,6 @@ import ( "os" "path/filepath" "slices" - "strings" "time" "github.com/robbycochran/harness-openshell/internal/agent" @@ -38,12 +37,11 @@ func NewCreateCmd(harnessDir, cli string) *cobra.Command { gw := gateway.New(cli) - // 1. Check which gateway is active and whether it's local or remote. + // 1. Check which gateway is active. activeGW, err := activeGatewayInfo(gw) if err != nil { return err } - isLocal := strings.Contains(activeGW.Endpoint, "127.0.0.1") status.Header("Gateway") status.OKf("%s (%s)", activeGW.Name, activeGW.Endpoint) @@ -102,21 +100,8 @@ func NewCreateCmd(harnessDir, cli string) *cobra.Command { } } - // 6. Determine whether the in-cluster runner is needed. - needsRunner := false - if !isLocal { - needsRunner = profileHasCustomProviders(providerNames, allProviders) - } - // 6. Deploy the sandbox status.Header("Creating sandbox") - if needsRunner { - status.Info("Custom providers detected — using in-cluster runner") - gwCfg := loadGatewayConfigForActive(harnessDir, activeGW) - return createViaRunner(harnessDir, gwCfg, gw, agentName, name) - } - - // Render payload and create directly payloadDir, err := os.MkdirTemp("", "harness-payload-") if err != nil { return fmt.Errorf("creating payload dir: %w", err) @@ -167,49 +152,4 @@ func activeGatewayInfo(gw gateway.Gateway) (*gateway.GatewayInfo, error) { return nil, fmt.Errorf("no active gateway — deploy one first: harness deploy") } -func profileHasCustomProviders(providerNames []string, allProviders []preflight.Provider) bool { - custom := make(map[string]bool) - for _, p := range allProviders { - if p.Type == "custom" { - custom[p.Name] = true - } - } - for _, name := range providerNames { - if custom[name] { - return true - } - } - return false -} - -func loadGatewayConfigForActive(harnessDir string, active *gateway.GatewayInfo) *gateway.GatewayConfig { - if active != nil && active.Name != "" { - dir := filepath.Join(harnessDir, "gateways", active.Name) - if cfg, err := gateway.LoadConfig(dir); err == nil { - return cfg - } - } - gwDir := filepath.Join(harnessDir, "gateways") - entries, err := os.ReadDir(gwDir) - if err != nil { - return nil - } - for _, e := range entries { - if !e.IsDir() { - continue - } - cfg, err := gateway.LoadConfig(filepath.Join(gwDir, e.Name())) - if err == nil && !cfg.IsLocal() { - return cfg - } - } - return nil -} - -func createViaRunner(harnessDir string, gwCfg *gateway.GatewayConfig, gw gateway.Gateway, agentPath, sandboxName string) error { - if gwCfg == nil { - return fmt.Errorf("no gateway config found for remote gateway — expected gateways//gateway.toml") - } - return upRemote(harnessDir, gwCfg, gw, agentPath, sandboxName) -} diff --git a/cmd/create_test.go b/cmd/create_test.go index e446664..3d0736a 100644 --- a/cmd/create_test.go +++ b/cmd/create_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/robbycochran/harness-openshell/internal/gateway" - "github.com/robbycochran/harness-openshell/internal/preflight" ) func TestActiveGatewayInfo_ListError(t *testing.T) { @@ -36,23 +35,4 @@ func TestActiveGatewayInfo_RemoteGateway(t *testing.T) { } } -func TestProfileHasCustomProviders_NoCustom(t *testing.T) { - allProviders := []preflight.Provider{ - {Name: "github", Type: "openshell"}, - {Name: "vertex-local", Type: "openshell"}, - } - if profileHasCustomProviders([]string{"github", "vertex-local"}, allProviders) { - t.Error("no custom providers, should return false") - } -} - -func TestProfileHasCustomProviders_WithCustom(t *testing.T) { - allProviders := []preflight.Provider{ - {Name: "github", Type: "openshell"}, - {Name: "gws", Type: "custom"}, - } - if !profileHasCustomProviders([]string{"github", "gws"}, allProviders) { - t.Error("gws is custom, should return true") - } -} diff --git a/cmd/deploy.go b/cmd/deploy.go index 19c080e..31b06e6 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -281,9 +281,9 @@ func deployFromConfig(harnessDir string, gwCfg *gateway.GatewayConfig, gw gatewa return fmt.Errorf("registering gateway %s: %w", gatewayName, err) } - // mTLS cert extraction — only needed for launcher mode (OCP/remote clusters). - // Direct mode (kind, plain k8s) uses HTTP or skips client cert auth. - if gwCfg.UsesLauncher() && gwCfg.Secrets.MTLS != "" { + // mTLS cert extraction — needed for remote clusters (OCP) where the + // gateway is exposed via TLS-passthrough Route. + if !gwCfg.IsLocal() && gwCfg.Secrets.MTLS != "" { home, err := os.UserHomeDir() if err != nil { return fmt.Errorf("determining home directory: %w", err) @@ -306,7 +306,7 @@ func deployFromConfig(harnessDir string, gwCfg *gateway.GatewayConfig, gw gatewa if err := gw.GatewaySelect(gatewayName); err != nil { return fmt.Errorf("selecting gateway %s: %w", gatewayName, err) } - if gwCfg.UsesLauncher() { + if !gwCfg.IsLocal() && gwCfg.Secrets.MTLS != "" { status.OKf("%s registered (certs from cluster)", gatewayName) } else { status.OKf("%s registered", gatewayName) diff --git a/cmd/deploy_test.go b/cmd/deploy_test.go index 9cc00bb..2dcd76d 100644 --- a/cmd/deploy_test.go +++ b/cmd/deploy_test.go @@ -29,7 +29,6 @@ type = "remote" platform = "ocp" service = "route" name = "test-ocp" -mode = "launcher" [chart] oci = "oci://ghcr.io/nvidia/openshell/helm-chart" @@ -43,9 +42,6 @@ values = "values.yaml" [addons] manifests = ["addons/rbac.yaml", "addons/route.yaml"] -[images] -runner = "test-runner:v1" - [ocp] scc-privileged = ["sa1", "sa2"] scc-anyuid = ["sa1"] @@ -53,10 +49,6 @@ scc-anyuid = ["sa1"] [secrets] names = ["secret-a"] mtls = "test-client-tls" - -[launcher] -service-account = "test-launcher-sa" -gateway-endpoint = "https://gw.cluster.local:8080" `), 0o644) os.WriteFile(filepath.Join(gwDir, "helm", "values.yaml"), []byte("image:\n pullPolicy: Always\n"), 0o644) os.WriteFile(filepath.Join(gwDir, "addons", "rbac.yaml"), []byte("# rbac\n"), 0o644) diff --git a/cmd/launch.go b/cmd/launch.go deleted file mode 100644 index f781f1e..0000000 --- a/cmd/launch.go +++ /dev/null @@ -1,209 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "io" - "os" - "os/exec" - "path/filepath" - "strings" - "time" - - "github.com/robbycochran/harness-openshell/internal/agent" - "github.com/spf13/cobra" -) - -func NewLaunchCmd(harnessDir, cli string) *cobra.Command { - return &cobra.Command{ - Use: "launch", - Short: "Run in-cluster: render agent config into a sandbox", - Hidden: true, - RunE: func(cmd *cobra.Command, args []string) error { - return runLaunch(cli) - }, - } -} - -func runLaunch(cli string) error { - endpoint := os.Getenv("GATEWAY_ENDPOINT") - if endpoint == "" { - endpoint = "https://openshell.openshell.svc.cluster.local:8080" - } - if cli == "" { - cli = "openshell" - } - - if err := configureGateway(endpoint, "/secrets/mtls", cli); err != nil { - return fmt.Errorf("gateway config: %w", err) - } - - agentCfg, err := agent.ParseFile("/etc/openshell/sandbox/agent.yaml") - if err != nil { - return err - } - - agentCfg.Image = resolveSandboxImage(agentCfg.Image) - - fmt.Println("=== Sandbox Runner ===") - fmt.Printf(" Name: %s\n", agentCfg.Name) - fmt.Printf(" Image: %s\n", agentCfg.Image) - fmt.Printf(" Entrypoint: %s\n", agentCfg.EffectiveEntrypoint()) - fmt.Printf(" Gateway: %s\n", endpoint) - fmt.Println() - - providerNames := agentCfg.ProviderNames() - registered := checkProviders(providerNames, cli) - - payloadDir := "/tmp/openshell-staging/openshell" - if err := agent.RenderPayload(agentCfg, "/etc/openshell/sandbox", payloadDir); err != nil { - return fmt.Errorf("rendering payload: %w", err) - } - fmt.Printf(" Payload: %s\n", payloadDir) - - if err := launchCreateSandbox(agentCfg, registered, payloadDir, cli); err != nil { - return err - } - - fmt.Printf("\nSandbox '%s' is ready.\n", agentCfg.Name) - fmt.Printf("Connect with: openshell sandbox connect %s\n", agentCfg.Name) - return nil -} - -func configureGateway(endpoint, mtlsDir, cli string) error { - requiredCerts := []string{"ca.crt", "tls.crt", "tls.key"} - var missing []string - for _, name := range requiredCerts { - if _, err := os.Stat(filepath.Join(mtlsDir, name)); err != nil { - missing = append(missing, name) - } - } - if len(missing) > 0 { - fmt.Fprintf(os.Stderr, "WARNING: mTLS certs missing from %s: %v\n", mtlsDir, missing) - fmt.Fprintf(os.Stderr, "WARNING: falling back to INSECURE mode\n") - os.Setenv("OPENSHELL_GATEWAY_ENDPOINT", endpoint) - os.Setenv("OPENSHELL_GATEWAY_INSECURE", "true") - return nil - } - fmt.Println(" mTLS certs found") - - httpEndpoint := strings.Replace(endpoint, "https:", "http:", 1) - - cmd := exec.Command(cli, "gateway", "add", httpEndpoint, "--name", "openshell") - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stdout - if err := cmd.Run(); err != nil { - return fmt.Errorf("gateway add: %w", err) - } - - home := os.Getenv("HOME") - gwDir := filepath.Join(home, ".config", "openshell", "gateways", "openshell") - mtlsDest := filepath.Join(gwDir, "mtls") - if err := os.MkdirAll(mtlsDest, 0o700); err != nil { - return fmt.Errorf("creating mtls dir: %w", err) - } - - metaPath := filepath.Join(gwDir, "metadata.json") - var meta map[string]any - if data, err := os.ReadFile(metaPath); err == nil { - if err := json.Unmarshal(data, &meta); err != nil { - return fmt.Errorf("parsing metadata.json: %w", err) - } - } - if meta == nil { - meta = make(map[string]any) - } - meta["gateway_endpoint"] = endpoint - meta["auth_mode"] = "mtls" - out, err := json.Marshal(meta) - if err != nil { - return fmt.Errorf("marshaling metadata.json: %w", err) - } - if err := os.WriteFile(metaPath, out, 0o644); err != nil { - return fmt.Errorf("writing metadata.json: %w", err) - } - - for _, name := range []string{"ca.crt", "tls.crt", "tls.key"} { - if err := copyFile(filepath.Join(mtlsDir, name), filepath.Join(mtlsDest, name)); err != nil { - return fmt.Errorf("copying %s: %w", name, err) - } - } - - selectCmd := exec.Command(cli, "gateway", "select", "openshell") - selectCmd.Stdout = os.Stdout - selectCmd.Stderr = io.Discard - selectCmd.Run() - fmt.Println(" mTLS gateway configured") - return nil -} - -func checkProviders(providers []string, cli string) []string { - var registered []string - for _, name := range providers { - cmd := exec.Command(cli, "provider", "get", name) - cmd.Stdout = io.Discard - cmd.Stderr = io.Discard - if cmd.Run() == nil { - registered = append(registered, name) - fmt.Printf(" Provider %s: attached\n", name) - } else { - fmt.Printf(" Provider %s: not registered (skipping)\n", name) - } - } - return registered -} - -func launchCreateSandbox(cfg *agent.AgentConfig, providers []string, payloadDir, cli string) error { - fmt.Println("\n=== Creating sandbox ===") - envInit := ". /sandbox/.config/openshell/sandbox.env 2>/dev/null && " + - "cat /sandbox/.config/openshell/sandbox.env >> /sandbox/.bashrc 2>/dev/null; " + - "gh auth setup-git 2>/dev/null; true" - for attempt := 1; attempt <= 5; attempt++ { - args := []string{"sandbox", "create", "--name", cfg.Name, "--no-tty"} - if cfg.Image != "" { - args = append(args, "--from", cfg.Image) - } - for _, p := range providers { - args = append(args, "--provider", p) - } - args = append(args, "--upload", payloadDir+":/sandbox/.config", "--no-git-ignore") - args = append(args, "--", "bash", "-c", envInit) - - cmd := exec.Command(cli, args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - if cmd.Run() == nil { - return nil - } - - fmt.Printf(" Attempt %d failed, retrying in 10s...\n", attempt) - del := exec.Command(cli, "sandbox", "delete", cfg.Name) - del.Stdout = io.Discard - del.Stderr = io.Discard - del.Run() - - if attempt == 5 { - return fmt.Errorf("failed after 5 attempts") - } - time.Sleep(10 * time.Second) - } - return nil // unreachable but required by compiler -} - - -func copyFile(src, dst string) error { - in, err := os.Open(src) - if err != nil { - return err - } - defer in.Close() - out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) - if err != nil { - return err - } - if _, err = io.Copy(out, in); err != nil { - out.Close() - return err - } - return out.Close() -} diff --git a/cmd/launch_test.go b/cmd/launch_test.go deleted file mode 100644 index 707fc0c..0000000 --- a/cmd/launch_test.go +++ /dev/null @@ -1,139 +0,0 @@ -package cmd - -import ( - "encoding/json" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/robbycochran/harness-openshell/internal/agent" -) - -// writeStubCLI writes an executable shell script that records its arguments -// to argsFile (one invocation per line) and exits 0. -func writeStubCLI(t *testing.T, argsFile string) string { - t.Helper() - dir := t.TempDir() - bin := filepath.Join(dir, "openshell") - script := "#!/bin/bash\necho \"$@\" >> " + argsFile + "\nexit 0\n" - if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { - t.Fatal(err) - } - return bin -} - -func TestConfigureGateway_MissingCertsFallsBackInsecure(t *testing.T) { - t.Setenv("OPENSHELL_GATEWAY_ENDPOINT", "") - t.Setenv("OPENSHELL_GATEWAY_INSECURE", "") - mtlsDir := t.TempDir() // no certs inside - - if err := configureGateway("https://gw.example:8080", mtlsDir, "openshell"); err != nil { - t.Fatalf("configureGateway: %v", err) - } - if got := os.Getenv("OPENSHELL_GATEWAY_ENDPOINT"); got != "https://gw.example:8080" { - t.Errorf("OPENSHELL_GATEWAY_ENDPOINT = %q", got) - } - if got := os.Getenv("OPENSHELL_GATEWAY_INSECURE"); got != "true" { - t.Errorf("OPENSHELL_GATEWAY_INSECURE = %q, want true", got) - } -} - -func TestConfigureGateway_MTLS(t *testing.T) { - home := t.TempDir() - t.Setenv("HOME", home) - - mtlsDir := t.TempDir() - for _, name := range []string{"ca.crt", "tls.crt", "tls.key"} { - if err := os.WriteFile(filepath.Join(mtlsDir, name), []byte("cert-"+name), 0o600); err != nil { - t.Fatal(err) - } - } - - argsFile := filepath.Join(t.TempDir(), "args.log") - cli := writeStubCLI(t, argsFile) - - if err := configureGateway("https://gw.example:8080", mtlsDir, cli); err != nil { - t.Fatalf("configureGateway: %v", err) - } - - // gateway add must use the http endpoint (mTLS is configured via metadata). - argsData, err := os.ReadFile(argsFile) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(string(argsData), "gateway add http://gw.example:8080") { - t.Errorf("cli args = %q, want 'gateway add http://...'", argsData) - } - - gwDir := filepath.Join(home, ".config", "openshell", "gateways", "openshell") - metaData, err := os.ReadFile(filepath.Join(gwDir, "metadata.json")) - if err != nil { - t.Fatalf("metadata.json not written: %v", err) - } - var meta map[string]any - if err := json.Unmarshal(metaData, &meta); err != nil { - t.Fatalf("metadata.json invalid: %v", err) - } - if meta["gateway_endpoint"] != "https://gw.example:8080" { - t.Errorf("gateway_endpoint = %v", meta["gateway_endpoint"]) - } - if meta["auth_mode"] != "mtls" { - t.Errorf("auth_mode = %v, want mtls", meta["auth_mode"]) - } - - for _, name := range []string{"ca.crt", "tls.crt", "tls.key"} { - data, err := os.ReadFile(filepath.Join(gwDir, "mtls", name)) - if err != nil { - t.Fatalf("cert %s not copied: %v", name, err) - } - if string(data) != "cert-"+name { - t.Errorf("cert %s content = %q", name, data) - } - } -} - -func TestCheckProviders(t *testing.T) { - dir := t.TempDir() - bin := filepath.Join(dir, "openshell") - // `provider get NAME` succeeds only for github. - script := "#!/bin/bash\n[ \"$3\" = \"github\" ] && exit 0\nexit 1\n" - if err := os.WriteFile(bin, []byte(script), 0o755); err != nil { - t.Fatal(err) - } - - registered := checkProviders([]string{"github", "atlassian"}, bin) - if len(registered) != 1 || registered[0] != "github" { - t.Errorf("registered = %v, want [github]", registered) - } -} - -func TestLaunchCreateSandbox_PassesAgentConfig(t *testing.T) { - argsFile := filepath.Join(t.TempDir(), "args.log") - cli := writeStubCLI(t, argsFile) - - cfg := &agent.AgentConfig{ - Name: "test-agent", - Image: "ghcr.io/test:sandbox", - } - if err := launchCreateSandbox(cfg, []string{"github"}, "/tmp/payload", cli); err != nil { - t.Fatalf("launchCreateSandbox: %v", err) - } - - argsData, err := os.ReadFile(argsFile) - if err != nil { - t.Fatal(err) - } - args := string(argsData) - for _, want := range []string{ - "sandbox create", - "--name test-agent", - "--from ghcr.io/test:sandbox", - "--provider github", - "--upload /tmp/payload:/sandbox/.config", - } { - if !strings.Contains(args, want) { - t.Errorf("cli args missing %q in %q", want, args) - } - } -} diff --git a/cmd/up.go b/cmd/up.go index 8315559..f5a4dfd 100644 --- a/cmd/up.go +++ b/cmd/up.go @@ -1,12 +1,9 @@ package cmd import ( - "context" "fmt" "os" - "os/exec" "path/filepath" - "strings" "time" "github.com/robbycochran/harness-openshell/internal/agent" @@ -53,9 +50,6 @@ func NewUpCmd(harnessDir, cli string) *cobra.Command { gwDir := filepath.Join(harnessDir, "gateways", gwName) gwCfg, _ := gateway.LoadConfig(gwDir) - if remote { - return upRemote(harnessDir, gwCfg, gw, agentPath, sandboxName) - } return upLocal(upLocalOpts{ harnessDir: harnessDir, gw: gw, @@ -116,162 +110,6 @@ type upLocalOpts struct { retrySleep time.Duration } -func upRemote(harnessDir string, gwCfg *gateway.GatewayConfig, gw gateway.Gateway, agentPath, sandboxName string) error { - ctx := context.Background() - namespace := k8s.DefaultNamespace() - kc := k8s.New("", namespace) - clusterRunner := k8s.New("", "") - - // Parse agent config early so we can show context - agentCfg, err := agent.ParseFile(agentPath) - if err != nil { - return err - } - name := agentCfg.Name - if sandboxName != "" { - name = sandboxName - } - - sandboxImage := resolveSandboxImage(agentCfg.Image) - - // Top-level context - status.Infof("Agent: %s (%s)", name, filepath.Base(agentPath)) - status.Infof("Image: %s", sandboxImage) - - // 1. Ensure gateway and namespace - gwReachable := gw.InferenceGet() == nil - _, nsErr := kc.RunKubectl(ctx, "get", "namespace", namespace) - nsExists := nsErr == nil - if !gwReachable || !nsExists { - if gwCfg == nil { - return fmt.Errorf("no active gateway and no gateway config — use: harness deploy ocp") - } - if err := deployFromConfig(harnessDir, gwCfg, gw, kc, clusterRunner); err != nil { - return fmt.Errorf("deploy failed: %w", err) - } - } - - // 2. Ensure providers needed by the agent - providerNames := agentCfg.ProviderNames() - if len(providerNames) > 0 { - _, missing := gateway.ValidateProviders(providerNames, gw) - if len(missing) > 0 { - if err := registerProviders(harnessDir, gw, false, gwCfg, false); err != nil { - return fmt.Errorf("provider registration failed: %w", err) - } - } - } - - // 3. ConfigMap from agent.yaml - out, err := kc.RunKubectl(ctx, "create", "configmap", "sandbox-"+name, - "--from-file=agent.yaml="+agentPath, - "--dry-run=client", "-o", "yaml") - if err != nil { - return fmt.Errorf("creating config configmap: %w", err) - } - if _, err := kc.RunKubectlOpts(ctx, k8s.KubectlOpts{ - Args: []string{"apply", "-f", "-"}, - Stdin: strings.NewReader(out), - }); err != nil { - return fmt.Errorf("applying config configmap: %w", err) - } - - // 4. Clean up old job - jobName := "sandbox-" + name - kc.RunKubectl(ctx, "delete", "job", jobName, "--grace-period=30") - kc.RunKubectl(ctx, "delete", "pod", "-l", "job-name="+jobName, "--grace-period=30") - - // 5. Apply runner Job (gwCfg provides defaults + RUNNER_IMAGE env override) - runnerImage := defaultRunnerImage() - runnerSA := "openshell-launcher" - gatewayEndpoint := "https://openshell.openshell.svc.cluster.local:8080" - mtlsSecret := "openshell-client-tls" - if gwCfg != nil { - if gwCfg.Images.Runner != "" { - runnerImage = gwCfg.Images.Runner - } - runnerSA = gwCfg.Launcher.ServiceAccount - gatewayEndpoint = gwCfg.Launcher.GatewayEndpoint - mtlsSecret = gwCfg.Secrets.MTLS - } - - job := map[string]any{ - "apiVersion": "batch/v1", - "kind": "Job", - "metadata": map[string]any{"name": jobName, "namespace": namespace}, - "spec": map[string]any{ - "backoffLimit": 0, - "template": map[string]any{ - "spec": map[string]any{ - "serviceAccountName": runnerSA, - "restartPolicy": "Never", - "containers": []map[string]any{{ - "name": "runner", - "image": runnerImage, - "imagePullPolicy": "Always", - "command": []string{"harness", "launch"}, - "env": runnerEnv(gatewayEndpoint, sandboxImage), - "volumeMounts": []map[string]any{ - {"name": "config", "mountPath": "/etc/openshell/sandbox", "readOnly": true}, - {"name": "gateway-mtls", "mountPath": "/secrets/mtls", "readOnly": true}, - }, - }}, - "volumes": []map[string]any{ - {"name": "config", "configMap": map[string]any{"name": "sandbox-" + name}}, - {"name": "gateway-mtls", "secret": map[string]any{"secretName": mtlsSecret}}, - }, - }, - }, - }, - } - if err := kc.ApplyYAML(ctx, job); err != nil { - return fmt.Errorf("applying runner job: %w", err) - } - - // 6. Wait for runner pod - status.Header("Sandbox") - status.Info("Waiting for runner...") - kc.RunKubectl(ctx, "wait", "--for=condition=ready", "pod", - "-l", "job-name="+jobName, "--timeout=120s") - - // 7. Tail logs in background - logCmd := exec.CommandContext(ctx, "kubectl", "-n", namespace, - "logs", "-f", "-l", "job-name="+jobName) - logCmd.Stdout = os.Stdout - logCmd.Stderr = os.Stderr - logCmd.Start() - - // 8. Poll job status (15 min timeout) - var jobStatus string - deadline := time.Now().Add(15 * time.Minute) - for time.Now().Before(deadline) { - jobStatus, err = kc.RunKubectl(ctx, "get", "job", jobName, - "-o", "jsonpath={.status.conditions[0].type}") - if err != nil { - return fmt.Errorf("checking runner job status: %w", err) - } - if jobStatus == "Complete" || jobStatus == "Failed" || jobStatus == "SuccessCriteriaMet" { - break - } - time.Sleep(2 * time.Second) - } - - if logCmd.Process != nil { - logCmd.Process.Kill() - logCmd.Wait() - } - - if jobStatus == "Complete" || jobStatus == "SuccessCriteriaMet" { - fmt.Println() - status.OKf("Connect with: harness connect %s", name) - return nil - } - if jobStatus == "" { - return fmt.Errorf("runner job timed out — check: kubectl logs -n %s -l job-name=%s", namespace, jobName) - } - return fmt.Errorf("runner job failed (status: %s) — check: kubectl logs -n %s -l job-name=%s", jobStatus, namespace, jobName) -} - func upLocal(opts upLocalOpts) error { gw := opts.gw @@ -304,9 +142,14 @@ func upLocal(opts upLocalOpts) error { if err := deployLocal(gw); err != nil { return fmt.Errorf("deploy failed: %w", err) } - } else { - if err := gw.InferenceGet(); err != nil { - return fmt.Errorf("no active gateway — use --local or --remote") + } else if gw.InferenceGet() != nil { + if opts.gwCfg == nil { + return fmt.Errorf("no active gateway — use --local or: harness deploy ocp") + } + kc := k8s.New("", k8s.DefaultNamespace()) + clusterRunner := k8s.New("", "") + if err := deployFromConfig(opts.harnessDir, opts.gwCfg, gw, kc, clusterRunner); err != nil { + return fmt.Errorf("deploy failed: %w", err) } } @@ -371,13 +214,6 @@ var Version = "dev" // DefaultAgentConfig holds the embedded default agent YAML, set from main.go. var DefaultAgentConfig []byte -func defaultRunnerImage() string { - if v := os.Getenv("RUNNER_IMAGE"); v != "" { - return v - } - return versionedImage("runner") -} - func versionedImage(name string) string { base := "ghcr.io/robbycochran/harness-openshell" if Version == "" || Version == "dev" { @@ -385,14 +221,3 @@ func versionedImage(name string) string { } return base + ":" + name + "-" + Version } - -func runnerEnv(gatewayEndpoint, sandboxImage string) []map[string]any { - env := []map[string]any{ - {"name": "GATEWAY_ENDPOINT", "value": gatewayEndpoint}, - {"name": "HOME", "value": "/tmp"}, - } - if sandboxImage != "" { - env = append(env, map[string]any{"name": "SANDBOX_IMAGE", "value": sandboxImage}) - } - return env -} diff --git a/cmd/up_test.go b/cmd/up_test.go index 4b3f308..1ed16dc 100644 --- a/cmd/up_test.go +++ b/cmd/up_test.go @@ -9,7 +9,6 @@ import ( "testing" "github.com/robbycochran/harness-openshell/internal/gateway" - "github.com/robbycochran/harness-openshell/internal/preflight" ) func TestUpLocal_NoGateway(t *testing.T) { @@ -167,24 +166,6 @@ func TestActiveGatewayInfo_LocalGateway(t *testing.T) { } } -func TestProfileHasCustomProviders(t *testing.T) { - allProviders := []preflight.Provider{ - {Name: "github", Type: "openshell"}, - {Name: "vertex-local", Type: "openshell"}, - {Name: "gws", Type: "custom"}, - } - - if profileHasCustomProviders([]string{"github"}, allProviders) { - t.Error("github is openshell, not custom") - } - if !profileHasCustomProviders([]string{"github", "gws"}, allProviders) { - t.Error("gws is custom, should return true") - } - if profileHasCustomProviders([]string{}, allProviders) { - t.Error("empty profile should not have custom providers") - } -} - func TestUpLocal_SandboxCreateOpts(t *testing.T) { t.Setenv("SANDBOX_IMAGE", "") dir := setupTestAgent(t) diff --git a/dev-harness.sh b/dev-harness.sh index 0da8187..2d3ca51 100755 --- a/dev-harness.sh +++ b/dev-harness.sh @@ -23,9 +23,8 @@ REGISTRY=${REGISTRY:-ghcr.io/robbycochran/harness-openshell} if [[ " $* " == *" --remote "* ]]; then make -C "$REPO_ROOT" cli dev-push else - make -C "$REPO_ROOT" cli dev-sandbox dev-runner + make -C "$REPO_ROOT" cli dev-sandbox fi export SANDBOX_IMAGE="${REGISTRY}:sandbox-${VERSION}" -export RUNNER_IMAGE="${REGISTRY}:runner-${VERSION}" exec "$REPO_ROOT/harness" "$@" diff --git a/gateways/ocp/addons/rbac.yaml b/gateways/ocp/addons/rbac.yaml deleted file mode 100644 index 78199dd..0000000 --- a/gateways/ocp/addons/rbac.yaml +++ /dev/null @@ -1,26 +0,0 @@ -# Launcher RBAC — namespace set by kubectl -n flag. -apiVersion: v1 -kind: ServiceAccount -metadata: - name: openshell-launcher ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: openshell-launcher -rules: - - apiGroups: [""] - resources: ["configmaps", "secrets"] - verbs: ["get", "list"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: openshell-launcher -subjects: - - kind: ServiceAccount - name: openshell-launcher -roleRef: - kind: Role - name: openshell-launcher - apiGroup: rbac.authorization.k8s.io diff --git a/gateways/ocp/gateway.toml b/gateways/ocp/gateway.toml index 7c9968d..b4d5aea 100644 --- a/gateways/ocp/gateway.toml +++ b/gateways/ocp/gateway.toml @@ -5,7 +5,6 @@ # - KUBECONFIG set or default context pointing to OCP # # Addons applied in order: -# - addons/rbac.yaml — launcher ServiceAccount + Role # - addons/route.yaml — OpenShift Route for external access # - SCC grants for openshell service accounts (via oc CLI) @@ -14,7 +13,6 @@ type = "remote" platform = "ocp" service = "route" name = "openshell-remote-ocp" -mode = "launcher" [providers] enabled = ["github", "vertex-local", "atlassian", "gws"] @@ -29,7 +27,7 @@ url = "https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download values = "values.yaml" [addons] -manifests = ["addons/rbac.yaml", "addons/route.yaml"] +manifests = ["addons/route.yaml"] [ocp] scc-privileged = ["openshell", "openshell-sandbox"] @@ -37,7 +35,3 @@ scc-anyuid = ["openshell"] [secrets] mtls = "openshell-client-tls" - -[launcher] -service-account = "openshell-launcher" -gateway-endpoint = "https://openshell.openshell.svc.cluster.local:8080" diff --git a/internal/gateway/config.go b/internal/gateway/config.go index 30bf91a..e8ac936 100644 --- a/internal/gateway/config.go +++ b/internal/gateway/config.go @@ -14,10 +14,8 @@ type GatewayConfig struct { Chart ChartSection `toml:"chart"` Helm HelmSection `toml:"helm"` Addons AddonsSection `toml:"addons"` - Images ImagesSection `toml:"images"` OCP OCPSection `toml:"ocp"` Secrets SecretsSection `toml:"secrets"` - Launcher LauncherSection `toml:"launcher"` // Dir is the directory containing the gateway.toml (set after parsing). // Used to resolve relative paths (helm values, addon manifests). @@ -29,7 +27,6 @@ type GatewaySection struct { Platform string `toml:"platform"` // "ocp" or "k8s" Service string `toml:"service"` // "route", "nodeport", "loadbalancer" Name string `toml:"name"` // CLI gateway registration name - Mode string `toml:"mode"` // "launcher" or "direct" } type ProvidersSection struct { @@ -55,10 +52,6 @@ type AddonsSection struct { Manifests []string `toml:"manifests"` // relative to / } -type ImagesSection struct { - Runner string `toml:"runner"` -} - type OCPSection struct { SCCPrivileged []string `toml:"scc-privileged"` SCCAnyuid []string `toml:"scc-anyuid"` @@ -69,11 +62,6 @@ type SecretsSection struct { MTLS string `toml:"mtls"` } -type LauncherSection struct { - ServiceAccount string `toml:"service-account"` - GatewayEndpoint string `toml:"gateway-endpoint"` -} - func LoadConfig(dir string) (*GatewayConfig, error) { path := filepath.Join(dir, "gateway.toml") var cfg GatewayConfig @@ -93,29 +81,9 @@ func (c *GatewayConfig) applyDefaults() { if c.Chart.CRD.URL == "" { c.Chart.CRD.URL = "https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download/manifest.yaml" } - // Runner image default left empty -- resolved by defaultRunnerImage() in cmd/up.go - if c.Secrets.MTLS == "" { - c.Secrets.MTLS = "openshell-client-tls" - } - if c.Launcher.ServiceAccount == "" { - c.Launcher.ServiceAccount = "openshell-launcher" - } - if c.Launcher.GatewayEndpoint == "" { - c.Launcher.GatewayEndpoint = "https://openshell.openshell.svc.cluster.local:8080" - } - if c.Gateway.Mode == "" { - if c.Gateway.Type == "local" { - c.Gateway.Mode = "direct" - } else { - c.Gateway.Mode = "launcher" - } - } } func (c *GatewayConfig) applyEnvOverrides() { - if v := os.Getenv("RUNNER_IMAGE"); v != "" { - c.Images.Runner = v - } if v := os.Getenv("GATEWAY_NAME"); v != "" { c.Gateway.Name = v } @@ -129,10 +97,6 @@ func (c *GatewayConfig) IsOCP() bool { return c.Gateway.Platform == "ocp" } -func (c *GatewayConfig) UsesLauncher() bool { - return c.Gateway.Mode == "launcher" -} - // HasProviders returns true if the gateway config specifies its own provider lists, // overriding the global openshell.toml. func (c *GatewayConfig) HasProviders() bool { diff --git a/internal/gateway/config_test.go b/internal/gateway/config_test.go index ca3e77b..bed4ac6 100644 --- a/internal/gateway/config_test.go +++ b/internal/gateway/config_test.go @@ -24,7 +24,6 @@ type = "remote" platform = "ocp" service = "route" name = "my-ocp" -mode = "launcher" [providers] enabled = ["github", "vertex-local"] @@ -40,10 +39,7 @@ url = "https://example.com/crd.yaml" values = "values.yaml" [addons] -manifests = ["addons/rbac.yaml", "addons/route.yaml"] - -[images] -runner = "example.com/runner:v1" +manifests = ["addons/route.yaml"] [ocp] scc-privileged = ["sa1", "sa2"] @@ -52,10 +48,6 @@ scc-anyuid = ["sa1"] [secrets] names = ["secret-a", "secret-b"] mtls = "my-mtls-secret" - -[launcher] -service-account = "my-launcher-sa" -gateway-endpoint = "https://gw.cluster.local:9090" `) cfg, err := LoadConfig(dir) @@ -75,9 +67,6 @@ gateway-endpoint = "https://gw.cluster.local:9090" if cfg.Gateway.Name != "my-ocp" { t.Errorf("name = %q, want my-ocp", cfg.Gateway.Name) } - if cfg.Gateway.Mode != "launcher" { - t.Errorf("mode = %q, want launcher", cfg.Gateway.Mode) - } if len(cfg.Providers.Enabled) != 2 { t.Errorf("providers.enabled = %v, want 2 entries", cfg.Providers.Enabled) } @@ -93,9 +82,6 @@ gateway-endpoint = "https://gw.cluster.local:9090" if cfg.Chart.CRD.URL != "https://example.com/crd.yaml" { t.Errorf("chart.crd.url = %q", cfg.Chart.CRD.URL) } - if cfg.Images.Runner != "example.com/runner:v1" { - t.Errorf("images.runner = %q", cfg.Images.Runner) - } if len(cfg.OCP.SCCPrivileged) != 2 { t.Errorf("ocp.scc-privileged = %v, want 2 entries", cfg.OCP.SCCPrivileged) } @@ -105,12 +91,6 @@ gateway-endpoint = "https://gw.cluster.local:9090" if cfg.Secrets.MTLS != "my-mtls-secret" { t.Errorf("secrets.mtls = %q", cfg.Secrets.MTLS) } - if cfg.Launcher.ServiceAccount != "my-launcher-sa" { - t.Errorf("launcher.service-account = %q", cfg.Launcher.ServiceAccount) - } - if cfg.Launcher.GatewayEndpoint != "https://gw.cluster.local:9090" { - t.Errorf("launcher.gateway-endpoint = %q", cfg.Launcher.GatewayEndpoint) - } if cfg.Dir != dir { t.Errorf("Dir = %q, want %q", cfg.Dir, dir) } @@ -131,19 +111,13 @@ type = "local" if !cfg.IsLocal() { t.Error("IsLocal() = false, want true") } - if cfg.Gateway.Mode != "direct" { - t.Errorf("default mode for local = %q, want direct", cfg.Gateway.Mode) - } // Defaults applied if cfg.Chart.OCI != "oci://ghcr.io/nvidia/openshell/helm-chart" { t.Errorf("default chart.oci = %q", cfg.Chart.OCI) } - if cfg.Secrets.MTLS != "openshell-client-tls" { - t.Errorf("default secrets.mtls = %q", cfg.Secrets.MTLS) - } - if cfg.Launcher.ServiceAccount != "openshell-launcher" { - t.Errorf("default launcher.service-account = %q", cfg.Launcher.ServiceAccount) + if cfg.Secrets.MTLS != "" { + t.Errorf("default secrets.mtls = %q, want empty", cfg.Secrets.MTLS) } } @@ -160,9 +134,6 @@ platform = "k8s" t.Fatal(err) } - if cfg.Gateway.Mode != "launcher" { - t.Errorf("default mode for remote = %q, want launcher", cfg.Gateway.Mode) - } if cfg.IsLocal() { t.Error("IsLocal() = true for remote") } @@ -195,12 +166,8 @@ func TestEnvOverrides(t *testing.T) { [gateway] type = "remote" name = "original-name" - -[images] -runner = "original-runner" `) - t.Setenv("RUNNER_IMAGE", "env-runner:v2") t.Setenv("GATEWAY_NAME", "env-gw-name") cfg, err := LoadConfig(dir) @@ -208,9 +175,6 @@ runner = "original-runner" t.Fatal(err) } - if cfg.Images.Runner != "env-runner:v2" { - t.Errorf("RUNNER_IMAGE override: got %q", cfg.Images.Runner) - } if cfg.Gateway.Name != "env-gw-name" { t.Errorf("GATEWAY_NAME override: got %q", cfg.Gateway.Name) } @@ -222,11 +186,8 @@ func TestEnvOverrides_NotSet(t *testing.T) { [gateway] type = "remote" name = "original-name" - `) - // Ensure env vars are not set - t.Setenv("RUNNER_IMAGE", "") t.Setenv("GATEWAY_NAME", "") cfg, err := LoadConfig(dir) @@ -323,35 +284,28 @@ type = "remote" func TestPredicates(t *testing.T) { tests := []struct { - name string - toml string - isLocal bool - isOCP bool - usesLauncher bool + name string + toml string + isLocal bool + isOCP bool }{ { - name: "local", - toml: "[gateway]\ntype = \"local\"", - - isLocal: true, - isOCP: false, - usesLauncher: false, + name: "local", + toml: "[gateway]\ntype = \"local\"", + isLocal: true, + isOCP: false, }, { - name: "remote ocp launcher", - toml: "[gateway]\ntype = \"remote\"\nplatform = \"ocp\"\nmode = \"launcher\"", - - isLocal: false, - isOCP: true, - usesLauncher: true, + name: "remote ocp", + toml: "[gateway]\ntype = \"remote\"\nplatform = \"ocp\"", + isLocal: false, + isOCP: true, }, { - name: "remote k8s direct", - toml: "[gateway]\ntype = \"remote\"\nplatform = \"k8s\"\nmode = \"direct\"", - - isLocal: false, - isOCP: false, - usesLauncher: false, + name: "remote k8s", + toml: "[gateway]\ntype = \"remote\"\nplatform = \"k8s\"", + isLocal: false, + isOCP: false, }, } @@ -371,9 +325,6 @@ func TestPredicates(t *testing.T) { if cfg.IsOCP() != tt.isOCP { t.Errorf("IsOCP() = %v, want %v", cfg.IsOCP(), tt.isOCP) } - if cfg.UsesLauncher() != tt.usesLauncher { - t.Errorf("UsesLauncher() = %v, want %v", cfg.UsesLauncher(), tt.usesLauncher) - } }) } } diff --git a/main.go b/main.go index 4256819..f133c35 100644 --- a/main.go +++ b/main.go @@ -54,7 +54,6 @@ func main() { cmd.NewTeardownCmd(harnessDir, cli), cmd.NewPreflightCmd(harnessDir, cli), cmd.NewProvidersCmd(harnessDir, cli), - cmd.NewLaunchCmd(harnessDir, cli), cmd.NewStatusCmd(harnessDir, cli), cmd.NewLogsCmd(harnessDir, cli), cmd.NewStopCmd(harnessDir, cli), diff --git a/sandbox/startup.sh b/sandbox/startup.sh index 9f5b6a1..fcc7f68 100644 --- a/sandbox/startup.sh +++ b/sandbox/startup.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Runtime setup for the sandbox. Runs once via harness launch. +# Runtime setup for the sandbox. Runs once at sandbox creation. set -euo pipefail # ── Source env vars from agent config ─────────────────────────────────