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
2 changes: 1 addition & 1 deletion api/specs.md
Original file line number Diff line number Diff line change
Expand Up @@ -871,4 +871,4 @@ The version cap also applies behind API path prefixes. SPDY TLS/header negotiati

Standalone `/modules/*` requests require one explicit registered remote `cluster` selector and cluster-wide module permission on that target. The selected remote operator owns module reconciliation; upload ConfigMaps use the configured operator namespace. Combined installs retain local module ownership and reject remote selectors on this surface. Local node credential minting is incompatible with standalone startup; Kubernetes system-log endpoints return 501 and the dashboard directs administrators to container logs.

See [standalone installation](../docs/standalone-panel.md) and [design](../specs/023-standalone-panel/spec.md).
See [standalone installation](../docs/standalone-panel.md) and [design](../specs/025-standalone-panel/spec.md).
31 changes: 0 additions & 31 deletions specs/023-standalone-panel/spec.md

This file was deleted.

37 changes: 37 additions & 0 deletions specs/025-standalone-panel/OPEN-DECISIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Open Decisions: 025-standalone-panel

Do not treat an item as decided until a maintainer rules on it here.

## OD-1: One API process with `Recreate` — **Settled (valgul, 2026-10-14)**

Keep one API process and the `Recreate` strategy; no rolling updates or multiple replicas. The audit hash chain and user-management safeguards use process-local locks. Multi-replica HA would be a separate future feature.

## OD-2: Telemetry marker for standalone installs — **Settled (valgul, 2026-10-14)**

A follow-up, not part of this feature. No task here.

## OD-3: Enforce `--skip-crds` for the panel-only Helm profile — **Settled (valgul, PR #630 review)**

Documented requirement only in this feature; valgul agreed to open a follow-up issue. No task here.

## OD-4: Legacy panel-key formats — **Settled (valgul, 2026-10-14): keep**

PR #630 merged with two legacy paths still supported: headerless ciphertext (decrypted and re-encrypted with the `gpk1.` header by rotation) and the legacy default key path `/data/panel.key` (on the database volume). This was still an open review thread on PR #630 when it merged.

- **Keep** (as merged): existing early standalone installs keep working; no change.
- **Drop in a follow-up**: remove headerless decryption and make the key path required, after a release note.

Ruling: keep both legacy paths as merged. No follow-up.

## OD-5: Archive this folder as `done_025-standalone-panel` — **Settled (valgul, 2026-10-14): keep `025-`**

Every task is ticked and PR #630 is merged, so Constitution IV and CLAUDE.md rule 16 call for `git mv specs/025-standalone-panel specs/done_025-standalone-panel` in its own `docs:` commit. Constitution I also says a feature is incomplete without a `test/e2e/` test, and standalone mode has none (spec Known gaps).

- **Keep `025-`**: leave it in flight until a standalone E2E test exists, which would be new work under a new decision.
- **Archive now**: rename to `done_025-`; the missing E2E test stays recorded as a known gap.

Ruling: the folder keeps the bare `025-` name until a standalone E2E test exists. Adding that test is new work and needs its own decision.

## Moot

- Splitting the share-links token fix (`834c3ca`) out of PR #630: merged inside PR #630, so there is nothing left to split.
37 changes: 37 additions & 0 deletions specs/025-standalone-panel/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Specification Quality Checklist: Standalone Panel and API

**Purpose**: Validate specification completeness and quality
**Created**: 2026-10-14
**Feature**: [spec.md](../spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [ ] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Notes

- Written retroactively for merged PR #630. Flags, paths and file names appear because the contributor's design note named them and operators configure them; they are the user-facing contract, not implementation choices.
- "Technology-agnostic" stays unchecked: SC-001, SC-005 and SC-006 name the CI jobs and tests that measure them, because this folder documents shipped behaviour.
- SC-001 to SC-006 are met by CI jobs and unit tests. Separately, Constitution I requires a `test/e2e/` test, which does not exist (spec Known gaps, OD-5).
- OD-4 (keep legacy key formats) and OD-5 (keep the `025-` name) are settled; neither changes a requirement of the merged feature.
31 changes: 31 additions & 0 deletions specs/025-standalone-panel/contracts/api-cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Contract: API flags, environment and commands

As merged in PR #630 (`api/cmd/main.go`, `api/cmd/panel_key.go`, `api/cmd/management.go`).

## `api serve` flags

| Flag | Environment | Default | Meaning |
|---|---|---|---|
| `--standalone` | `GAMEPLANE_STANDALONE=true` | off | Store management records in SQL; never read a kubeconfig or in-cluster credentials; register no `local` cluster |
| `--panel-key-file` | `GAMEPLANE_PANEL_KEY_FILE` | `/data/panel.key` (legacy) | Credential encryption key. Compose and Helm set `/keys/panel.key` |
| `--panel-key-provisioned` | `GAMEPLANE_PANEL_KEY_PROVISIONED=true` | off | Key must already exist; never generated or written |
| `--remote-allowed-cidrs` | `GAMEPLANE_REMOTE_ALLOWED_CIDRS` | empty | Comma-separated CIDRs that narrow standalone Kubernetes and gateway destinations. Invalid entries fail startup |

Rules:

- `--standalone` with cluster-ops enabled fails at startup ("cluster-ops requires a local Kubernetes cluster").
- When credentials or a key-state row exist, a missing, unsafe or wrong key fails startup.

## `api rotate-panel-key`

Offline master-key rotation. Stop the API first.

| Flag | Environment | Required | Meaning |
|---|---|---|---|
| `--old-key-file` | `GAMEPLANE_PANEL_KEY_FILE` | yes | Current key; never removed or overwritten |
| `--new-key-file` | | yes | Separately named replacement; generated unless provisioned |
| `--new-key-provisioned` | | no | Replacement must already exist |
| `--db-driver` | `GAMEPLANE_DB_DRIVER` | no (`sqlite`) | `sqlite` or `postgres` |
| | `GAMEPLANE_DB_DSN` | no | Database DSN. Environment only: `--db-dsn` is rejected so credentials never reach argv or help text |

Behaviour: validates the old key against every stored row before creating the new key; re-encrypts all rows and updates the key-state row in one transaction; refuses identical paths or identical key material; a retry after a lost commit is recognised through `previous_key_id`. Afterwards, point `--panel-key-file` (or Helm `api.panelKey.fileName`) at the new file.
36 changes: 36 additions & 0 deletions specs/025-standalone-panel/contracts/deployment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Contract: Helm values and Docker Compose

As merged in PR #630. Operator guide: `docs/standalone-panel.md`.

## Helm values (`charts/gameplane/values.yaml`)

| Value | Default | Meaning |
|---|---|---|
| `operator.enabled` | `true` | `false` removes every operator resource (Deployment, RBAC, namespaces, network policies, monitors, rules, module source, cosign key, Grafana dashboard; agent mTLS certificates in `mtls.yaml` unless the combined API needs them) |
| `api.standalone` | `false` | Passes `--standalone`; drops API RBAC, agent CA and client mounts, cluster-ops, CRD apply hook and MCP server |
| `api.replicas` | `1` | Must stay 1 in standalone (validated) |
| `api.panelKey.fileName` | `panel.key` | Key file under `/keys`; a simple name of at most 128 characters |
| `api.panelKey.existingSecret` | empty | Mount this Secret read-only at `/keys` (mode `0440`) and pass `--panel-key-provisioned` |
| `api.panelKey.secretKey` | `panel.key` | Entry in `existingSecret` |
| `api.panelKey.storage.existingClaim` | empty | Use an existing PVC for `/keys` |
| `api.panelKey.storage.size` | `1Mi` | Size of the generated `gameplane-api-key` PVC (kept on uninstall) |
| `api.panelKey.storage.storageClassName` | empty | Storage class for the generated PVC |
| `api.remoteAllowedCIDRs` | `[]` | Rendered as `--remote-allowed-cidrs` in standalone only |

### Render-time guards (`templates/validate.yaml`)

When `api.standalone=true`, rendering fails unless: the API is enabled; `operator.enabled=false`; `api.replicas=1`; `gateway.enabled` is not set; `panelKey.fileName` is a simple name; `existingSecret` and `storage.existingClaim` are not both set.

### Standalone pod settings (`templates/api.yaml`)

`Recreate` strategy, `automountServiceAccountToken: false`, `fsGroupChangePolicy: OnRootMismatch`; no data PVC when `db.driver=postgres`.

### Panel-only example

`charts/gameplane/examples/standalone-panel-values.yaml`. Install with `--skip-crds`: values cannot stop Helm from installing `crds/` (documented only; follow-up issue owed).

## Docker Compose (`deploy/standalone/`)

- `compose.yaml`: project `gameplane-panel`, services `gameplane-api` and `web` only. Volumes `panel-data` (`/data`) and `panel-keys` (`/keys`). API runs read-only, drops all capabilities, `no-new-privileges`, publishes no port. Web binds `127.0.0.1:${GAMEPLANE_PORT:-8080}`.
- `compose.provisioned-key.yaml`: overlay that mounts `GAMEPLANE_PANEL_KEY_SOURCE_FILE` as secret `panel-key` at `/run/secrets/panel-key` and sets `GAMEPLANE_PANEL_KEY_PROVISIONED=true`.
- Run exactly one `gameplane-api` container (documented; not enforced by Compose).
50 changes: 50 additions & 0 deletions specs/025-standalone-panel/contracts/http-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Contract: HTTP endpoints

As merged in PR #630. All endpoints sit behind session authentication and the RBAC middleware (`api/internal/rbac/rbac.go`). The full API reference is `api/specs.md`.

## `GET /admin/installation`

Any authenticated user. Response `200`:

```json
{ "standalone": true, "localCluster": false, "version": "v0.3.0" }
```

Not reachable before login (Core rule 3).

## `GET /clusters`, `GET /fleet/*`

In standalone mode both succeed with no implicit `local` entry; an empty registry returns empty lists. The cluster view exposes only name, display name, phase, message, version and last check time, plus a boolean `canViewInventory`.

## `POST /clusters`

Requires `cluster:manage` (`GET /clusters` needs only a session). Body `{ "name", "displayName", "kubeconfig" }`.

- Standalone: the kubeconfig must pass `kube.ValidateStandaloneKubeconfig` and the remote access policy (HTTPS, verified TLS, no proxy, safe destination). Registration and credential commit in one transaction. Duplicate name returns `409`.
- Combined: unchanged Secret-then-CR path, with cleanup on failure.

## `PUT /clusters/{name}/kubeconfig`

Requires `cluster:manage`. Body `{ "kubeconfig" }`. `204` on success, `409` when the registration changed concurrently. Publishes a new immutable credential, swaps the reference under UID and previous-value checks, keeps gateway settings and grants, then deletes the old credential.

## `PUT /clusters/{name}/gateway`, `DELETE /clusters/{name}/gateway`

Requires `cluster:manage`. `PUT` body `{ "url", "caCert", "clientCert", "clientKey" }`; the URL must be an HTTPS origin that passes the remote access policy. `204` with no body. `DELETE` clears the gateway and removes only the API-generated gateway Secret.

## `DELETE /clusters/{name}`

Requires `cluster:manage`. Removes the registration and its API-owned kubeconfig and gateway credentials with UID and version preconditions.

## `/modules` and `/modules/*`

- Standalone: exactly one `?cluster=<registered, non-local>` is required (`400` when missing, repeated or `local`). `GET` needs `modules:read` and every other method needs `modules:manage`, as a cluster-wide grant on that cluster (`403` otherwise).
- Combined: local catalog as before; a remote selector returns `501`.
- `modules:read`, `modules:manage`, `templates:read` and `templates:write` are now checked as cluster-wide grants on the target cluster in both modes (`api/internal/auth/perms.go`).

## `GET /admin/system-logs/{component}`

Standalone: `501` with guidance to read container-runtime logs.

## Error and log surface

No response body, error or log line from these endpoints contains kubeconfig, token, certificate or key material. Remote lookup and connection failures use fixed messages.
Loading
Loading