Skip to content
Merged
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
45 changes: 43 additions & 2 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ jobs:
done
openshell gateway list

- name: Run integration tests
run: ./test/test-flow.sh local --full --no-providers --profile=ci
- name: Run integration tests (ci mode — no credentials)
run: ./test/test-flow.sh local --ci

- name: Export gateway logs
if: always()
Expand All @@ -47,3 +47,44 @@ jobs:
with:
name: gateway-logs-local
path: /tmp/gateway-logs/

kind:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Build harness CLI
run: make cli

- name: Install openshell
run: curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh

- uses: helm/kind-action@v1
with:
cluster_name: openshell

- name: Install helm
uses: azure/setup-helm@v4

- name: Deploy gateway to kind
run: ./harness deploy kind

- name: Run integration tests (ci mode — no credentials)
run: ./test/test-flow.sh kind --ci

- name: Export logs
if: always()
run: |
mkdir -p /tmp/kind-logs
kubectl -n openshell logs statefulset/openshell > /tmp/kind-logs/gateway.log 2>/dev/null || true
kubectl -n openshell get all > /tmp/kind-logs/resources.txt 2>&1 || true
openshell gateway list > /tmp/kind-logs/gateways.txt 2>&1 || true

- uses: actions/upload-artifact@v4
if: always()
with:
name: gateway-logs-kind
path: /tmp/kind-logs/
68 changes: 66 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,76 @@ Current workarounds and their upstream tracking:

| Workaround | Why | Upstream |
|------------|-----|----------|
| GWS credential export/upload | gws CLI reads encrypted local files | [#1268](https://github.com/NVIDIA/OpenShell/issues/1268), [#1423](https://github.com/NVIDIA/OpenShell/issues/1423) |
| Custom gateway image | `google-vertex-ai` provider not in released builds yet | Will ship in upstream release |
| `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1` | Vertex AI rejects `context_management` beta header | Anthropic/Google to align APIs |
| Atlassian `JIRA_URL`/`JIRA_USERNAME` as uploaded config | Provider v2 config keys not injected as env vars yet | OpenShell roadmap |
| Atlassian `JIRA_URL`/`JIRA_USERNAME` as `--config` material | Provider v2 config keys not injected as env vars yet | OpenShell roadmap |
| In-cluster launcher 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. |

## Validation

Validation has two modes — **default** and **ci** — that are independent of where
the gateway runs (local Podman, kind, OCP). The mode controls what is tested, not
the target.

### Modes

**`default`** — expects user credentials. Tests the full stack including provider
registration, credential injection, and the GWS OAuth token lifecycle.

**`ci`** — no credentials required. Tests gateway deploy and sandbox lifecycle only.
Runs in GitHub Actions on every PR.

```
--ci flag = --no-providers --profile=ci --full
```

### Make targets

| Target | Gateway | Mode |
|--------|---------|------|
| `make validate-local` | local Podman | default (needs creds) |
| `make validate-local-ci` | local Podman | ci (no creds) |
| `make validate-kind` | kind cluster | default (needs creds) |
| `make validate-kind-ci` | kind cluster | ci (no creds) |

Or directly:
```bash
./test/test-flow.sh local # default mode
./test/test-flow.sh local --ci # ci mode
./test/test-flow.sh kind --ci # used in GitHub Actions
```

### Default mode requirements

- `openshell` gateway running locally (`brew services start openshell`)
- `JIRA_API_TOKEN`, `JIRA_URL`, `JIRA_USERNAME` for Atlassian
- `gcloud auth application-default login` for Vertex AI
- `gws auth login` for Google Workspace
- `GITHUB_TOKEN` for GitHub

Default mode does not run in GitHub Actions today — it requires personal OAuth
credentials. Future: service accounts for Vertex AI and Atlassian can run in GHA;
GWS would need a dedicated OAuth service account.

### What each mode tests

| Check | ci | default |
|-------|----|---------|
| Gateway deploy and rollout | ✓ | ✓ |
| Sandbox create / exec / delete | ✓ | ✓ |
| Provider registration | — | ✓ |
| `GOOGLE_WORKSPACE_CLI_TOKEN` is proxy placeholder | — | ✓ |
| Gmail/Calendar/Drive API via proxy | — | ✓ |
| GWS token rotation survives in sandbox | — | ✓ |
| Inference (Vertex AI) | — | ✓ |
| Atlassian MCP server | — | ✓ |

## Adding a new integration

Before writing custom code:
Expand Down
53 changes: 51 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ DEV_SANDBOX_IMAGE := $(DEV_REGISTRY):$(DEV_TAG)-sandbox
DEV_LAUNCHER_IMAGE := $(DEV_REGISTRY):$(DEV_TAG)-launcher

.PHONY: cli sandbox push-sandbox cli-launcher launcher push-launcher \
vet lint test-unit test test-local test-ocp test-all validate \
vet lint test-unit test test-local test-kind test-ocp test-all validate \
validate-local validate-local-ci validate-kind validate-kind-ci \
dev-sandbox dev-launcher validate-dev clean help

## ── CLI ──────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -84,11 +85,15 @@ test: cli sandbox push-launcher
test-local: cli
./test/test-flow.sh local --full

## kind cluster (full lifecycle — requires: kind create cluster --name openshell)
test-kind: cli
./test/test-flow.sh kind --full

## OCP only (full lifecycle)
test-ocp: cli sandbox push-launcher
./test/test-flow.sh ocp --full

## All combinations: podman + ocp
## All combinations: local + kind + ocp
test-all: cli sandbox push-launcher
./test/test-flow.sh all --full

Expand Down Expand Up @@ -147,6 +152,50 @@ validate-dev: cli dev-sandbox dev-launcher
@echo "=== Integration: OCP (full) ==="
SANDBOX_IMAGE=$(DEV_SANDBOX_IMAGE) LAUNCHER_IMAGE=$(DEV_LAUNCHER_IMAGE) ./test/test-flow.sh ocp --full

## Default validation: unit tests + bats + full integration with user credentials.
## Requires: openshell gateway running locally (brew services start openshell),
## JIRA_API_TOKEN, gcloud ADC, gws auth login.
validate-local: cli
@echo "=== Unit tests ==="
CGO_ENABLED=0 go test ./...
cd sandbox/launcher && go test ./...
@echo ""
@echo "=== Bats ==="
bats test/preflight.bats
@echo ""
@echo "=== Integration: local gateway, default mode ==="
./test/test-flow.sh local --full

## CI validation: unit tests + integration without credentials (local gateway).
## Requires: openshell gateway running locally only.
validate-local-ci: cli
@echo "=== Unit tests ==="
CGO_ENABLED=0 go test ./...
cd sandbox/launcher && go test ./...
@echo ""
@echo "=== Integration: local gateway, ci mode ==="
./test/test-flow.sh local --ci

## Default validation on kind: unit tests + full integration with user credentials.
## Requires: kind create cluster --name openshell
validate-kind: cli
@echo "=== Unit tests ==="
CGO_ENABLED=0 go test ./...
cd sandbox/launcher && go test ./...
@echo ""
@echo "=== Integration: kind gateway, default mode ==="
./test/test-flow.sh kind --full

## CI validation on kind: unit tests + integration without credentials.
## Requires: kind create cluster --name openshell
validate-kind-ci: cli
@echo "=== Unit tests ==="
CGO_ENABLED=0 go test ./...
cd sandbox/launcher && go test ./...
@echo ""
@echo "=== Integration: kind gateway, ci mode ==="
./test/test-flow.sh kind --ci

## ── Convenience targets ───────────────────────────────────────────────

## Clean built binaries
Expand Down
69 changes: 0 additions & 69 deletions cmd/creds.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ package cmd
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"

"github.com/robbycochran/harness-openshell/internal/k8s"
"github.com/robbycochran/harness-openshell/internal/status"
Expand All @@ -22,21 +19,6 @@ func ensureCreds(kc k8s.Runner, namespace string, force bool) error {
status.Section("Setting up cluster credentials")
status.Detailf("Namespace: %s", namespace)

// GWS credentials
status.Section("GWS")
if force && kc.SecretExists(ctx, "openshell-gws") {
kc.RunKubectl(ctx, "delete", "secret", "openshell-gws")
status.Info("Deleted existing secret")
}

if kc.SecretExists(ctx, "openshell-gws") {
status.Info("openshell-gws: exists (use --force to recreate)")
} else {
if err := createGWSSecret(ctx, kc); err != nil {
status.Failf("GWS: %v", err)
}
}

// Atlassian credentials
status.Section("Atlassian")
if force && kc.SecretExists(ctx, "openshell-atlassian") {
Expand All @@ -61,54 +43,3 @@ func ensureCreds(kc k8s.Runner, namespace string, force bool) error {

return nil
}

func createGWSSecret(ctx context.Context, kc k8s.Runner) error {
gwsPath, err := exec.LookPath("gws")
if err != nil {
status.Info("GWS: not installed (skipping)")
return nil
}

check := exec.Command(gwsPath, "auth", "status")
check.Stdout = io.Discard
check.Stderr = io.Discard
if check.Run() != nil {
status.Info("GWS: not authenticated (run 'gws auth login')")
return nil
}

tmpDir, err := os.MkdirTemp("", "harness-gws-")
if err != nil {
return err
}
defer os.RemoveAll(tmpDir)

credFile := filepath.Join(tmpDir, "credentials.json")
out, err := exec.Command(gwsPath, "auth", "export", "--unmasked").Output()
if err != nil {
status.Info("GWS: export failed (skipping)")
return nil
}
if err := os.WriteFile(credFile, out, 0o600); err != nil {
return fmt.Errorf("writing gws credentials: %w", err)
}

args := []string{"create", "secret", "generic", "openshell-gws",
"--from-file=credentials.json=" + credFile}

gwsConfigDir := os.Getenv("GWS_CONFIG_DIR")
if gwsConfigDir == "" {
home, _ := os.UserHomeDir()
gwsConfigDir = filepath.Join(home, ".config", "gws")
}
clientSecret := filepath.Join(gwsConfigDir, "client_secret.json")
if _, err := os.Stat(clientSecret); err == nil {
args = append(args, "--from-file=client_secret.json="+clientSecret)
}

if _, err := kc.RunKubectl(ctx, args...); err != nil {
return fmt.Errorf("creating gws secret: %w", err)
}
status.OK("openshell-gws: created")
return nil
}
6 changes: 3 additions & 3 deletions cmd/creds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ func TestEnsureCreds_ForceDeletesExisting(t *testing.T) {
if err != nil {
t.Fatalf("ensureCreds: %v", err)
}
// Should attempt to delete both secrets
if nsRunner.CallCount("delete secret") != 2 {
t.Errorf("expected 2 secret deletes, got %d: %v",
// Should attempt to delete atlassian secret
if nsRunner.CallCount("delete secret") != 1 {
t.Errorf("expected 1 secret delete, got %d: %v",
nsRunner.CallCount("delete secret"), nsRunner.Calls)
}
}
Expand Down
Loading
Loading