Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9585e95
chore(cluster): reintroduce provider factory and de-hardcode k3d seam…
oleg-flamingocx Jul 15, 2026
01c9dc1
Merge branch 'main' into develop
oleg-flamingocx Jul 15, 2026
6655c86
fix(argocd): raise controller throughput and repo-server CPU cap for …
oleg-flamingocx Jul 15, 2026
88ced78
Merge branch 'main' into develop
oleg-flamingocx Jul 15, 2026
3b0a64a
feat(cluster): provision EKS clusters via Terraform (#225)
oleg-flamingocx Jul 15, 2026
f2b2bee
feat(cluster): provision GKE clusters via Terraform (#226)
oleg-flamingocx Jul 15, 2026
da67bd3
feat(cluster): cloud UX polish — plan preview, progress, guarded dest…
oleg-flamingocx Jul 16, 2026
60c02ba
chore(cluster): pre-e2e hardening — template validation, tf version g…
oleg-flamingocx Jul 16, 2026
8da596a
fix(ci): failures (#229)
oleg-flamingocx Jul 16, 2026
c4f7b5e
feat(prerequisites): auto-install terraform on Windows via pinned zip…
oleg-flamingocx Jul 16, 2026
e416bba
Merge branch 'main' into develop
oleg-flamingocx Jul 16, 2026
6685637
Merge remote-tracking branch 'origin/main' into develop
oleg-flamingocx Jul 21, 2026
bbb9bc4
chore: go get -u ./... apply
oleg-flamingocx Jul 21, 2026
52ffbae
Merge branch 'main' into develop
oleg-flamingocx Jul 21, 2026
c018eff
feat(cluster): GKE ownership boundary, external discovery, EKS coming…
oleg-flamingocx Jul 21, 2026
fddb6a6
feat(cluster): openframe cluster use — switch kubectl context to any …
oleg-flamingocx Jul 21, 2026
67e1d1e
feat(cluster): single gcloud login flow across all GKE commands (#240)
oleg-flamingocx Jul 21, 2026
4d23305
docs: step-by-step GKE workflow guide (#241)
oleg-flamingocx Jul 21, 2026
67bb70a
fix(cluster): make interrupted cloud creates resumable; tighten safet…
oleg-flamingocx Jul 22, 2026
7245ccf
ci(test): exercise the full cloud-cluster command surface without cre…
oleg-flamingocx Jul 22, 2026
52ecc8d
ci(test): accept either refusal gate on non-interactive cloud delete
oleg-flamingocx Jul 22, 2026
27a7283
ci(test): cloud-surface step fixes — refusal-gate assertion + portabl…
oleg-flamingocx Jul 22, 2026
dc823c5
feat(cluster): dry-run plan preview lists the planned resources (#246)
oleg-flamingocx Jul 22, 2026
c270dff
feat(cluster): show the per-resource plan during a real create too
oleg-flamingocx Jul 22, 2026
d00e0b2
feat(cluster): optional infracost estimate; no hardcoded prices anywh…
oleg-flamingocx Jul 22, 2026
184fa5a
build: emit binaries into build/ instead of the repo root
oleg-flamingocx Jul 22, 2026
09500c3
feat(cluster): offer a verified infracost install in the dry-run prev…
oleg-flamingocx Jul 22, 2026
8161544
feat(cluster): run the one-time infracost auth login inside the CLI (…
oleg-flamingocx Jul 22, 2026
d3e37f0
feat(cluster): terraform-apply-style plan confirmation before a real …
oleg-flamingocx Jul 22, 2026
d182793
feat(cluster): terraform-apply-style plan confirmation before a real …
oleg-flamingocx Jul 22, 2026
b837bce
fix: address external code-review findings (#253)
oleg-flamingocx Jul 22, 2026
fce68b5
fix(prereq): install AWS CLI v2 on Linux, not legacy v1 (#254)
oleg-flamingocx Jul 22, 2026
cbc751d
guard(cluster): scope terraform destroy to a cluster workspace (#265)
oleg-flamingocx Jul 27, 2026
e07eb83
fix(gke): zonal clusters by default so --nodes N means N, not N-per-z…
oleg-flamingocx Jul 28, 2026
77a9612
Merge branch 'main' into develop
oleg-flamingocx Jul 28, 2026
62a0c61
fix(gke): actually release PVC disks on delete — target the real name…
oleg-flamingocx Jul 29, 2026
85c7c15
fix(cluster): stop showing k3d-specific details for GKE clusters (#268)
oleg-flamingocx Jul 29, 2026
fa66ebe
fix(version): identify dev builds from the embedded VCS stamp (#271)
oleg-flamingocx Jul 29, 2026
ee9541a
fix(cluster): show the resume hint when a create is interrupte (#272)
oleg-flamingocx Jul 29, 2026
ab7d0bb
feat(cluster): pick the GCP project from a list in the create wizard …
oleg-flamingocx Jul 29, 2026
6588ca5
fix(download): fail loudly when a pinned archive lacks Tarball/Zip (#…
oleg-flamingocx Jul 29, 2026
302b005
fix(app): make a stuck app-install fail with a diagnostic, not an opa…
oleg-flamingocx Jul 29, 2026
be7e6e8
chore: go get -u ./... apply
oleg-flamingocx Jul 29, 2026
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
137 changes: 134 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ jobs:
env:
# Path to the built binary for this matrix leg. On Windows it forwards the
# whole CLI into WSL; on Linux it runs natively. Used by every test step.
OF_BIN: ./openframe-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }}
OF_BIN: ./build/openframe-${{ matrix.os }}-${{ matrix.arch }}${{ matrix.os == 'windows' && '.exe' || '' }}
# Cluster used by the lifecycle steps; its k3d kube-context is k3d-<name>.
OF_CLUSTER: openframe-test
OF_CONTEXT: k3d-openframe-test
Expand Down Expand Up @@ -165,8 +165,8 @@ jobs:
# published release to auto-download, so build the Linux binary and
# stream it into the default WSL distro's ~/.openframe/bin via stdin —
# no Windows→WSL path translation, which is where the launcher looks.
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o openframe-linux-amd64 .
wsl -- bash -c 'mkdir -p "$HOME/.openframe/bin" && cat > "$HOME/.openframe/bin/openframe" && chmod +x "$HOME/.openframe/bin/openframe"' < openframe-linux-amd64
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o build/openframe-linux-amd64 .
wsl -- bash -c 'mkdir -p "$HOME/.openframe/bin" && cat > "$HOME/.openframe/bin/openframe" && chmod +x "$HOME/.openframe/bin/openframe"' < build/openframe-linux-amd64
fi

# --- Simplest: no cluster, read-only (all platforms, incl. darwin) -----
Expand Down Expand Up @@ -345,6 +345,70 @@ jobs:
echo "==================================================================="
"$OF_BIN" prerequisites check || echo "some prerequisites missing (expected pre-bootstrap)"

# Cloud-cluster command SURFACE, with no cloud and no Docker: the EKS
# coming-soon banner, flag validation, the GKE dry-run's graceful
# degradation, and `cluster use` error paths. Everything here is either
# exit-0 by design or an expected, asserted failure — no resource is
# ever created. Pure, so it runs on every OS (on Windows via the WSL
# forward, exercising the Linux binary's paths).
- name: 'Cluster: cloud command surface (no cloud, no Docker)'
shell: bash
run: |
echo "==================================================================="
echo "=== TEST: cloud cluster surface — banner, validation, dry-run, use"
echo "==================================================================="
fail() { echo "::error::$1"; exit 1; }
# Portable hang guard: macOS runners ship coreutils' gtimeout, not
# timeout. Fall back to no guard rather than failing with exit 127.
run_to() {
local t="$1"; shift
if command -v timeout >/dev/null 2>&1; then timeout "$t" "$@"
elif command -v gtimeout >/dev/null 2>&1; then gtimeout "$t" "$@"
else "$@"; fi
}

echo "--- EKS create is gated: banner + non-zero exit (scripts must not see success)"
if "$OF_BIN" cluster create eks-smoke --type eks --skip-wizard >/tmp/eks.out 2>&1; then
cat /tmp/eks.out; fail "gated eks create must exit non-zero"
fi
grep -qi "coming soon" /tmp/eks.out || { cat /tmp/eks.out; fail "eks banner text missing"; }

echo "--- unknown --type is rejected"
if "$OF_BIN" cluster create x --type minikube --skip-wizard >/tmp/neg.out 2>&1; then
cat /tmp/neg.out; fail "unknown cluster type must be rejected"
fi
grep -q "unknown cluster type" /tmp/neg.out || { cat /tmp/neg.out; fail "missing unknown-type message"; }

echo "--- gke --skip-wizard demands --region and --project"
if "$OF_BIN" cluster create x --type gke --skip-wizard >/tmp/neg.out 2>&1; then
cat /tmp/neg.out; fail "gke without region/project must be rejected"
fi
grep -qE '\-\-(region|project) is required' /tmp/neg.out || { cat /tmp/neg.out; fail "missing required-flag message"; }

echo "--- gke dry-run degrades gracefully without terraform/credentials"
# Two legitimate outcomes on an unauthenticated runner: terraform absent
# -> soft skip (exit 0); terraform present -> the auth flow fails fast
# (non-interactive, exit non-zero). Both must be actionable, never a hang.
set +e
run_to 120 "$OF_BIN" cluster create gke-smoke --type gke \
--project no-such-project --region us-central1 --skip-wizard --dry-run </dev/null >/tmp/gke.out 2>&1
code=$?
set -e
[ "$code" -eq 124 ] && { cat /tmp/gke.out; fail "gke dry-run hung on a prompt"; }
grep -qiE "skipping the plan preview|not authenticated|not installed|application default|not accessible" /tmp/gke.out \
|| { cat /tmp/gke.out; fail "gke dry-run produced neither a skip nor an auth hint (exit $code)"; }
echo "OK (exit $code)"

echo "--- cluster use of an unknown cluster is an actionable error"
set +e
run_to 60 "$OF_BIN" cluster use no-such-cluster </dev/null >/tmp/use.out 2>&1
code=$?
set -e
[ "$code" -eq 124 ] && { cat /tmp/use.out; fail "cluster use hung on a prompt"; }
[ "$code" -eq 0 ] && { cat /tmp/use.out; fail "cluster use of an unknown cluster must fail"; }
grep -qiE "not (known|found) locally|gcloud" /tmp/use.out || { cat /tmp/use.out; fail "missing actionable use error"; }
echo "cloud surface OK"

# Everything below needs a Docker daemon + k3d. It runs on linux and on
# windows (the Windows binary forwards into WSL, where Docker was started
# above), exercising the real cluster lifecycle on both. darwin stays
Expand All @@ -360,6 +424,68 @@ jobs:
"$OF_BIN" cluster list -o json
"$OF_BIN" cluster list -o yaml

# Managed-cloud OWNERSHIP GUARDS, exercised on the real binary against a
# fake GKE workspace (an isolated OPENFRAME_CLUSTERS_DIR with a
# hand-written cluster.json) — no cloud, no terraform, fully
# deterministic. Locks: registry-backed list/status, the typed-confirm
# refusal on non-interactive cloud delete, and use's missing-context
# error. Linux only: one leg suffices and the Windows binary would need
# the fake workspace inside WSL's home.
- name: 'Cluster: managed-cloud guards (fake workspace, no cloud)'
if: matrix.os == 'linux'
shell: bash
env:
OPENFRAME_CLUSTERS_DIR: ${{ runner.temp }}/of-fake-clusters
run: |
echo "==================================================================="
echo "=== TEST: cloud ownership guards on a fake GKE workspace"
echo "==================================================================="
fail() { echo "::error::$1"; exit 1; }
export KUBECONFIG="$RUNNER_TEMP/of-fake-kubeconfig"

mkdir -p "$OPENFRAME_CLUSTERS_DIR/fake-gke"
cat > "$OPENFRAME_CLUSTERS_DIR/fake-gke/cluster.json" <<'EOF'
{"name":"fake-gke","type":"gke","status":"ready","region":"us-central1",
"project":"fake-project","node_count":3,"created_at":"2026-07-22T00:00:00Z",
"endpoint":"https://203.0.113.10","ca_cert":"ZmFrZS1jYQ=="}
EOF

echo "--- the registry cluster appears in list with source=openframe"
"$OF_BIN" cluster list -o json | jq -e \
'.[] | select(.name == "fake-gke")' >/dev/null \
|| fail "fake-gke missing from cluster list -o json"

echo "--- status resolves through the registry (no cloud calls)"
"$OF_BIN" cluster status fake-gke -o json | jq -e \
'.type == "gke"' >/dev/null || fail "status -o json lacks type gke"

echo "--- use without a kubeconfig context fails with the exact hint"
set +e
"$OF_BIN" cluster use fake-gke </dev/null >/tmp/use.out 2>&1
code=$?
set -e
[ "$code" -eq 0 ] && { cat /tmp/use.out; fail "use without a context must fail"; }
grep -q "no kubeconfig context" /tmp/use.out || { cat /tmp/use.out; fail "missing no-context hint"; }

echo "--- non-interactive cloud delete WITHOUT --force refuses"
# Two refusal gates guard this path, and either may fire first: the
# generic deletion confirmation ("confirmation required ... re-run
# with --force") refuses before the cloud-specific typed-confirm
# gate ("refusing to destroy cloud cluster") is reached. Both are
# correct refusals — assert the guarantee (non-zero exit, actionable
# message, workspace survives), not which gate fired.
set +e
timeout 60 "$OF_BIN" cluster delete fake-gke </dev/null >/tmp/del.out 2>&1
code=$?
set -e
[ "$code" -eq 124 ] && { cat /tmp/del.out; fail "cluster delete hung on a prompt"; }
[ "$code" -eq 0 ] && { cat /tmp/del.out; fail "non-interactive cloud delete without --force must refuse"; }
grep -qE "refusing to destroy cloud cluster|confirmation required.*--force" /tmp/del.out || { cat /tmp/del.out; fail "missing an actionable refusal message"; }

echo "--- the workspace (the only pointer to billed resources) survived"
[ -f "$OPENFRAME_CLUSTERS_DIR/fake-gke/cluster.json" ] || fail "refused delete must not remove the workspace"
echo "ownership guards OK"

# --- Medium: dry-run, still no real changes ----------------------------
- name: 'CLI: dry-run create & install'
if: matrix.os != 'darwin'
Expand Down Expand Up @@ -474,6 +600,11 @@ jobs:
if: matrix.os != 'darwin'
shell: bash
timeout-minutes: 40
env:
# Cap the CLI's app-wait below this step's 40m limit so it fails with
# its OWN diagnostic (stuck apps + health messages) instead of being
# killed opaquely by the job timeout.
OPENFRAME_APP_WAIT_TIMEOUT: 35m
run: |
echo "==================================================================="
echo "=== TEST: real app install (non-interactive, full ArgoCD wait)"
Expand Down
41 changes: 27 additions & 14 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# OpenFrame CLI Makefile

.PHONY: all build build-all clean test test-unit test-race test-integration lint fmt vet tidy help
.PHONY: all build build-all clean test test-unit test-race test-integration test-tfvalidate lint fmt vet tidy help

# Variables
BINARY_NAME := openframe
BUILD_DIR := build
# -trimpath drops absolute build paths from the binary (reproducibility), matching
# the release build in .goreleaser.yml.
GO_BUILD := CGO_ENABLED=0 go build -trimpath
Expand All @@ -22,18 +23,20 @@ UNIT_PKGS := . ./cmd/... ./internal/... ./tests/testutil/... ./tests/scripts/...
# Default target
all: build

build: ## Build binary for the current platform
@echo "Building $(BINARY_NAME) for $(GOOS)/$(GOARCH)..."
@$(GO_BUILD) -o $(BINARY_NAME)-$(GOOS)-$(GOARCH)$(BINARY_SUFFIX) .

build-all: ## Cross-compile every release platform (matches .goreleaser.yml)
@echo "Building $(BINARY_NAME) for all release platforms..."
@GOOS=linux GOARCH=amd64 $(GO_BUILD) -o $(BINARY_NAME)-linux-amd64 .
@GOOS=linux GOARCH=arm64 $(GO_BUILD) -o $(BINARY_NAME)-linux-arm64 .
@GOOS=darwin GOARCH=amd64 $(GO_BUILD) -o $(BINARY_NAME)-darwin-amd64 .
@GOOS=darwin GOARCH=arm64 $(GO_BUILD) -o $(BINARY_NAME)-darwin-arm64 .
@GOOS=windows GOARCH=amd64 $(GO_BUILD) -o $(BINARY_NAME)-windows-amd64.exe .
@GOOS=windows GOARCH=arm64 $(GO_BUILD) -o $(BINARY_NAME)-windows-arm64.exe .
build: ## Build binary into $(BUILD_DIR)/ for the current platform
@echo "Building $(BINARY_NAME) for $(GOOS)/$(GOARCH) into $(BUILD_DIR)/..."
@mkdir -p $(BUILD_DIR)
@$(GO_BUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-$(GOOS)-$(GOARCH)$(BINARY_SUFFIX) .

build-all: ## Cross-compile every release platform into $(BUILD_DIR)/ (matches .goreleaser.yml)
@echo "Building $(BINARY_NAME) for all release platforms into $(BUILD_DIR)/..."
@mkdir -p $(BUILD_DIR)
@GOOS=linux GOARCH=amd64 $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 .
@GOOS=linux GOARCH=arm64 $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 .
@GOOS=darwin GOARCH=amd64 $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64 .
@GOOS=darwin GOARCH=arm64 $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64 .
@GOOS=windows GOARCH=amd64 $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe .
@GOOS=windows GOARCH=arm64 $(GO_BUILD) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-arm64.exe .

test-unit: ## Run unit tests (vet on; incl. root main_test.go + tests/testutil)
@echo "Running unit tests..."
Expand All @@ -51,6 +54,15 @@ test-integration: ## Run integration tests (real k3d clusters; needs docker + k3
@echo "Running integration tests (real clusters!)..."
@go test -tags integration -count=1 ./tests/integration/...

# Terraform template validation is opt-in via a build tag: it runs a real
# `terraform init` (downloads the pinned modules + providers — network, ~min)
# and `terraform validate` on the generated EKS/GKE root modules. No cloud
# credentials needed; the strongest pre-e2e check of the templates.
test-tfvalidate: ## Validate generated terraform templates (needs terraform + network)
@echo "Validating terraform templates (downloads providers)..."
@go test -tags tfvalidate -count=1 -timeout 15m -run '^TestTerraformValidate$$' \
./internal/cluster/providers/eks/... ./internal/cluster/providers/gke/...

test: test-unit test-integration ## Run unit + integration tests

lint: ## Run golangci-lint (govet, staticcheck, errcheck, gosec, ineffassign)
Expand All @@ -72,7 +84,8 @@ tidy: ## Check go.mod/go.sum are tidy (fails if `go mod tidy` would change them)
# and deleted them.
clean: ## Remove build artifacts
@rm -f $(BINARY_NAME) \
$(BINARY_NAME)-linux-* $(BINARY_NAME)-darwin-* $(BINARY_NAME)-windows-*
$(BINARY_NAME)-linux-* $(BINARY_NAME)-darwin-* $(BINARY_NAME)-windows-* \
$(BUILD_DIR)/$(BINARY_NAME)-*
@echo "Cleaned build artifacts"

help: ## Show this help
Expand Down
Loading