From 5dd29e6ce1a0bd6b5edfee4b6e2ef57fcb3b5846 Mon Sep 17 00:00:00 2001 From: Onur Date: Tue, 24 Feb 2026 12:52:27 +0100 Subject: [PATCH 1/7] docs: add simplest deployment specification --- ...6-02-24-simplest-spritz-deployment-spec.md | 143 ++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 docs/2026-02-24-simplest-spritz-deployment-spec.md diff --git a/docs/2026-02-24-simplest-spritz-deployment-spec.md b/docs/2026-02-24-simplest-spritz-deployment-spec.md new file mode 100644 index 0000000..01ff743 --- /dev/null +++ b/docs/2026-02-24-simplest-spritz-deployment-spec.md @@ -0,0 +1,143 @@ +--- +date: 2026-02-24 +author: Spritz Contributors +title: Simplest Spritz Deployment Specification +tags: [spritz, deployment, architecture] +--- + +## Overview + +This document defines the default Spritz deployment model for the easiest +possible install by a new operator. + +The default must avoid path-routing tricks, custom edge workers, and multi-origin +front-end hosting. + +## Goals + +- Make first deployment possible with one hostname and one Helm install. +- Keep UI and API in the same Kubernetes deployment surface. +- Minimize required configuration values. +- Keep advanced networking patterns optional. + +## Non-goals + +- Optimizing for existing multi-app domain/path routing. +- Requiring provider-specific edge features for default setup. +- Dropbox-grade conflict resolution in default storage mode. + +## Default Deployment Model + +### Topology + +- `spritz-ui` and `spritz-api` run in Kubernetes. +- Single public host, for example `spritz.example.com`. +- Ingress/Gateway routes: + - `/` -> `spritz-ui` + - `/api` -> `spritz-api` + +### Why this is the default + +- No external frontend hosting dependency. +- No cross-origin CORS/env drift for standard installs. +- No edge-worker route forwarding required. +- Easier debugging: one host, one ingress path map. + +## Required Operator Inputs + +The default installation should require only: + +- `host`: public Spritz host (example: `spritz.example.com`) +- `ingressClassName`: ingress class (or gateway class) +- `tls.issuerRef` (or pre-provisioned secret) +- `storageClass`: default PVC storage class + +Everything else should have working defaults. + +## Default Helm Values (Target) + +```yaml +global: + host: spritz.example.com + +ingress: + enabled: true + className: nginx + tls: + enabled: true + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + +ui: + enabled: true + basePath: / + apiBaseUrl: /api + +api: + enabled: true + basePath: /api + +storage: + homePvc: + enabled: true + storageClassName: standard + sharedMounts: + enabled: false +``` + +## Storage and Sync Defaults + +- Default mode is per-devbox persistent home PVC. +- Shared cross-devbox live sync is disabled by default. +- Shared mounts remain available as an opt-in advanced feature. + +Rationale: + +- PVC-only mode has fewer failure modes. +- This is enough for most single-devbox usage. +- Operators can enable shared sync only when they need it. + +## Optional Advanced Mode + +Advanced mode can support: + +- Path mounting under another app host (example: `/spritz`). +- Edge worker route forwarding. +- SNI override and custom origin hostnames. +- Shared live sync across multiple devboxes. + +These are explicitly optional and should be documented separately from the +default install flow. + +## Operational Guardrails + +Even in default mode, add these checks: + +- Health endpoint checks for UI and API. +- TLS handshake check on the configured public host. +- Alert on repeated `5xx` from ingress. + +If advanced mode is enabled, add: + +- DNS drift detection for origin hostnames. +- Edge-to-origin TLS checks. +- Alerting for edge handshake failures. + +## Validation Checklist + +After install: + +1. Open `https://spritz.example.com`. +2. Confirm UI loads from `/`. +3. Confirm API health at `/api/healthz`. +4. Create a devbox and open terminal. +5. Recreate the pod and verify home state persists. + +Advanced mode validation should be a separate checklist. + +## Decision Summary + +- Keep core Spritz architecture. +- Change deployment defaults toward single-host Kubernetes serving. +- Move edge/path-routing complexity behind an optional advanced setup. From 200dc11e64b4a85a146a20f5d32b62e53a53d632 Mon Sep 17 00:00:00 2001 From: Onur Date: Tue, 24 Feb 2026 13:07:48 +0100 Subject: [PATCH 2/7] docs: expand implementation details for simplest deployment spec --- ...6-02-24-simplest-spritz-deployment-spec.md | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/docs/2026-02-24-simplest-spritz-deployment-spec.md b/docs/2026-02-24-simplest-spritz-deployment-spec.md index 01ff743..1738458 100644 --- a/docs/2026-02-24-simplest-spritz-deployment-spec.md +++ b/docs/2026-02-24-simplest-spritz-deployment-spec.md @@ -86,6 +86,56 @@ storage: enabled: false ``` +## Implementation Scope (Exact Changes) + +### Helm Values and Compatibility + +File: `helm/spritz/values.yaml` + +- Add `global.host` with default `spritz.example.com`. +- Add `global.ingress.className` with default `nginx`. +- Add `global.ingress.tls.enabled` (default `true`). +- Add `global.ingress.tls.secretName` (default empty; operator-provided). +- Keep `ui.apiBaseUrl` default `/api`. +- Keep `ui.basePath` default `/`. +- Keep `ui.ingress.enabled` default `true` for single-host installs. +- Keep `operator.homePVC.enabled` default `true`. +- Keep `operator.sharedMounts.enabled` and `api.sharedMounts.enabled` default `false`. + +Legacy keys that remain supported during transition: + +- `ui.ingress.host` +- `ui.ingress.className` +- `ui.ingress.path` +- explicit `ui.apiBaseUrl` + +### Helm Templates + +Files: + +- `helm/spritz/templates/ui-deployment.yaml` +- `helm/spritz/templates/api-deployment.yaml` +- `helm/spritz/templates/ui-api-ingress.yaml` (new) + +Required behavior: + +- Move ingress rendering out of `ui-deployment.yaml` into a dedicated template. +- Render one public ingress object with two ordered paths: + - `/api` -> service `spritz-api` on `.Values.api.service.port` + - `/` -> service `spritz-ui` on `.Values.ui.service.port` +- Source ingress class from `global.ingress.className`, fallback to `ui.ingress.className`. +- Source host from `global.host`, fallback to `ui.ingress.host`. +- Add TLS block when `global.ingress.tls.enabled` is true. +- Keep service names unchanged (`spritz-api`, `spritz-ui`) to avoid rollout risk. + +### API Route Prefix Handling + +File: `api/main.go` + +- Register endpoints on both root and `/api` prefixes. +- Keep existing root routes for backward compatibility. +- Add `/api/healthz` alongside `/healthz` for path-based ingress health checks. + ## Storage and Sync Defaults - Default mode is per-devbox persistent home PVC. @@ -110,6 +160,24 @@ Advanced mode can support: These are explicitly optional and should be documented separately from the default install flow. +## Upgrade Behavior (Existing Installs) + +- This is still prelaunch v1; defaults can be optimized for new installs. +- Existing installs can preserve legacy behavior by pinning old ingress keys. +- No CRD schema change is required for this deployment change. +- Existing Spritz custom resources are not mutated by chart upgrade. +- Home PVC default change applies to newly created Spritz resources after upgrade. + +Compatibility precedence: + +- API URL resolution: + - explicit `ui.apiBaseUrl` wins + - else use `/api` when `ui.basePath` is set + - else use `/api` +- Host/class resolution: + - use `global.host` and `global.ingress.className` when set + - else fallback to `ui.ingress.host` and `ui.ingress.className` + ## Operational Guardrails Even in default mode, add these checks: @@ -136,6 +204,53 @@ After install: Advanced mode validation should be a separate checklist. +## Test Matrix (Must Pass) + +### Helm Render Checks + +Run: + +- `helm template spritz ./helm/spritz` +- `helm template spritz ./helm/spritz --set global.host= --set ui.ingress.host=legacy.example.com` + +Pass criteria: + +- Exactly one public ingress is rendered in default mode. +- Path `/api` routes to `spritz-api`. +- Path `/` routes to `spritz-ui`. +- Default host comes from `global.host`. +- Legacy host fallback works when `global.host` is empty. + +### API Route Checks + +Add tests in: + +- `api/main_routes_test.go` + +Assertions: + +- `GET /healthz` returns 200. +- `GET /api/healthz` returns 200. +- Root and `/api` route variants hit identical auth and handler logic. + +Run: + +- `(cd api && go test ./...)` + +### Smoke and Guardrail Checks + +Run: + +- `./e2e/local-smoke.sh` +- `./scripts/verify-agnostic.sh` +- `npx -y @simpledoc/simpledoc check` + +Pass criteria: + +- Spritz reaches `Ready` in local smoke. +- No provider-specific values are introduced. +- Documentation conventions pass. + ## Decision Summary - Keep core Spritz architecture. From ac8dac007e411c2c533514c971eafa7fd86d588b Mon Sep 17 00:00:00 2001 From: Onur Date: Tue, 24 Feb 2026 13:22:43 +0100 Subject: [PATCH 3/7] feat: default spritz to standalone single-host ingress --- api/main.go | 12 +++- api/main_routes_test.go | 67 ++++++++++++++++++ e2e/local-smoke.sh | 2 +- helm/spritz/templates/ui-api-ingress.yaml | 84 +++++++++++++++++++++++ helm/spritz/templates/ui-deployment.yaml | 23 +------ helm/spritz/values.yaml | 18 +++-- 6 files changed, 175 insertions(+), 31 deletions(-) create mode 100644 api/main_routes_test.go create mode 100644 helm/spritz/templates/ui-api-ingress.yaml diff --git a/api/main.go b/api/main.go index ba5a584..f7806c6 100644 --- a/api/main.go +++ b/api/main.go @@ -178,13 +178,19 @@ func main() { } func (s *server) registerRoutes(e *echo.Echo) { - e.GET("/healthz", s.handleHealthz) - internal := e.Group("/internal/v1", s.internalAuthMiddleware()) + s.registerRoutesAtPrefix(e, "") + s.registerRoutesAtPrefix(e, "/api") +} + +func (s *server) registerRoutesAtPrefix(e *echo.Echo, prefix string) { + group := e.Group(prefix) + group.GET("/healthz", s.handleHealthz) + internal := group.Group("/internal/v1", s.internalAuthMiddleware()) internal.GET("/shared-mounts/owner/:owner/:mount/latest", s.getSharedMountLatest) internal.GET("/shared-mounts/owner/:owner/:mount/revisions/:revision", s.getSharedMountRevision) internal.PUT("/shared-mounts/owner/:owner/:mount/revisions/:revision", s.putSharedMountRevision) internal.PUT("/shared-mounts/owner/:owner/:mount/latest", s.putSharedMountLatest) - secured := e.Group("", s.authMiddleware()) + secured := group.Group("", s.authMiddleware()) secured.GET("/spritzes", s.listSpritzes) secured.POST("/spritzes", s.createSpritz) secured.GET("/spritzes/:name", s.getSpritz) diff --git a/api/main_routes_test.go b/api/main_routes_test.go new file mode 100644 index 0000000..f57c157 --- /dev/null +++ b/api/main_routes_test.go @@ -0,0 +1,67 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/labstack/echo/v4" +) + +func TestRegisterRoutesExposesHealthzUnderRootAndAPI(t *testing.T) { + s := &server{ + auth: authConfig{mode: authModeNone}, + internalAuth: internalAuthConfig{enabled: false}, + terminal: terminalConfig{enabled: false}, + } + e := echo.New() + s.registerRoutes(e) + + rootReq := httptest.NewRequest(http.MethodGet, "/healthz", nil) + rootRec := httptest.NewRecorder() + e.ServeHTTP(rootRec, rootReq) + if rootRec.Code != http.StatusOK { + t.Fatalf("expected /healthz to return 200, got %d", rootRec.Code) + } + + apiReq := httptest.NewRequest(http.MethodGet, "/api/healthz", nil) + apiRec := httptest.NewRecorder() + e.ServeHTTP(apiRec, apiReq) + if apiRec.Code != http.StatusOK { + t.Fatalf("expected /api/healthz to return 200, got %d", apiRec.Code) + } +} + +func TestRegisterRoutesAppliesAuthToRootAndAPIPrefix(t *testing.T) { + s := &server{ + auth: authConfig{ + mode: authModeHeader, + headerID: "X-Spritz-User-Id", + }, + internalAuth: internalAuthConfig{enabled: false}, + terminal: terminalConfig{enabled: false}, + } + e := echo.New() + s.registerRoutes(e) + + rootReq := httptest.NewRequest(http.MethodGet, "/spritzes", nil) + rootRec := httptest.NewRecorder() + e.ServeHTTP(rootRec, rootReq) + if rootRec.Code != http.StatusUnauthorized { + t.Fatalf("expected /spritzes to return 401 without auth, got %d", rootRec.Code) + } + if !strings.Contains(rootRec.Body.String(), "unauthenticated") { + t.Fatalf("expected /spritzes response to mention unauthenticated, got %q", rootRec.Body.String()) + } + + apiReq := httptest.NewRequest(http.MethodGet, "/api/spritzes", nil) + apiRec := httptest.NewRecorder() + e.ServeHTTP(apiRec, apiReq) + if apiRec.Code != http.StatusUnauthorized { + t.Fatalf("expected /api/spritzes to return 401 without auth, got %d", apiRec.Code) + } + if !strings.Contains(apiRec.Body.String(), "unauthenticated") { + t.Fatalf("expected /api/spritzes response to mention unauthenticated, got %q", apiRec.Body.String()) + } +} diff --git a/e2e/local-smoke.sh b/e2e/local-smoke.sh index 85b9231..146291e 100755 --- a/e2e/local-smoke.sh +++ b/e2e/local-smoke.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -euo pipefail -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" CLUSTER_NAME="${SPRITZ_E2E_CLUSTER:-spritz-e2e}" KUBECONFIG_PATH="${SPRITZ_E2E_KUBECONFIG:-${TMPDIR:-/tmp}/spritz-e2e.kubeconfig}" API_PORT="${SPRITZ_E2E_API_PORT:-8090}" diff --git a/helm/spritz/templates/ui-api-ingress.yaml b/helm/spritz/templates/ui-api-ingress.yaml new file mode 100644 index 0000000..ff757b7 --- /dev/null +++ b/helm/spritz/templates/ui-api-ingress.yaml @@ -0,0 +1,84 @@ +{{- if .Values.ui.ingress.enabled }} +{{- if ne .Values.ui.namespace .Values.api.namespace }} +{{- fail "ui.namespace and api.namespace must match when ui.ingress.enabled=true" }} +{{- end }} +{{- $host := default "" .Values.ui.ingress.host -}} +{{- with .Values.global }} + {{- if .host }} + {{- $host = .host -}} + {{- end }} +{{- end }} +{{- $className := default "" .Values.ui.ingress.className -}} +{{- with .Values.global }} + {{- with .ingress }} + {{- if .className }} + {{- $className = .className -}} + {{- end }} + {{- end }} +{{- end }} +{{- $uiPath := default "/" .Values.ui.ingress.path -}} +{{- if eq (trim $uiPath) "" }} + {{- $uiPath = "/" -}} +{{- end }} +{{- if not (hasPrefix "/" $uiPath) }} + {{- $uiPath = printf "/%s" $uiPath -}} +{{- end }} +{{- if and (ne $uiPath "/") (hasSuffix "/" $uiPath) }} + {{- $uiPath = trimSuffix "/" $uiPath -}} +{{- end }} +{{- $apiPath := "/api" -}} +{{- if ne $uiPath "/" }} + {{- $apiPath = printf "%s/api" $uiPath -}} +{{- end }} +{{- $tlsEnabled := false -}} +{{- $tlsSecretName := "" -}} +{{- with .Values.global }} + {{- with .ingress }} + {{- with .tls }} + {{- if hasKey . "enabled" }} + {{- $tlsEnabled = .enabled -}} + {{- end }} + {{- if .secretName }} + {{- $tlsSecretName = .secretName -}} + {{- end }} + {{- end }} + {{- end }} +{{- end }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: spritz-web + namespace: {{ .Values.ui.namespace }} +spec: + {{- if $className }} + ingressClassName: {{ $className }} + {{- end }} + rules: + - {{- if $host }} + host: {{ $host }} + {{- end }} + http: + paths: + - path: {{ $apiPath }} + pathType: Prefix + backend: + service: + name: spritz-api + port: + number: {{ .Values.api.service.port }} + - path: {{ $uiPath }} + pathType: Prefix + backend: + service: + name: spritz-ui + port: + number: {{ .Values.ui.service.port }} + {{- if and $tlsEnabled $host }} + tls: + - hosts: + - {{ $host }} + {{- if $tlsSecretName }} + secretName: {{ $tlsSecretName }} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/spritz/templates/ui-deployment.yaml b/helm/spritz/templates/ui-deployment.yaml index 8e37fec..f5c4841 100644 --- a/helm/spritz/templates/ui-deployment.yaml +++ b/helm/spritz/templates/ui-deployment.yaml @@ -36,7 +36,7 @@ spec: {{- else if .Values.ui.basePath }} value: {{ printf "%s/api" (.Values.ui.basePath | trimSuffix "/") | quote }} {{- else }} - value: {{ printf "http://spritz-api.%s.svc.cluster.local:%d" .Values.api.namespace .Values.api.service.port | quote }} + value: "/api" {{- end }} - name: SPRITZ_UI_BASE_PATH value: {{ .Values.ui.basePath | quote }} @@ -113,24 +113,3 @@ spec: - name: http port: {{ .Values.ui.service.port }} targetPort: {{ .Values.ui.containerPort }} ---- -{{- if .Values.ui.ingress.enabled }} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: spritz-ui - namespace: {{ .Values.ui.namespace }} -spec: - ingressClassName: {{ .Values.ui.ingress.className }} - rules: - - host: {{ .Values.ui.ingress.host }} - http: - paths: - - path: {{ .Values.ui.ingress.path }} - pathType: Prefix - backend: - service: - name: spritz-ui - port: - number: {{ .Values.ui.service.port }} -{{- end }} diff --git a/helm/spritz/values.yaml b/helm/spritz/values.yaml index 85145dd..2a11374 100644 --- a/helm/spritz/values.yaml +++ b/helm/spritz/values.yaml @@ -1,3 +1,11 @@ +global: + host: spritz.example.com + ingress: + className: nginx + tls: + enabled: true + secretName: "" + operator: image: spritz-operator:latest imagePullPolicy: IfNotPresent @@ -11,7 +19,7 @@ operator: homeSizeLimit: 5Gi podNodeSelector: "" homePVC: - enabled: false + enabled: true prefix: spritz-home size: 5Gi accessModes: @@ -181,12 +189,12 @@ ui: port: 80 containerPort: 8080 ingress: - enabled: false - host: spritz.sh + enabled: true + host: spritz.example.com className: nginx path: / - apiBaseUrl: "" - basePath: "" + apiBaseUrl: "/api" + basePath: "/" ownerId: "" assetVersion: "" presets: [] From 82366be3209a81e2495cc5c6ce23e8f786d680e5 Mon Sep 17 00:00:00 2001 From: Onur Date: Tue, 24 Feb 2026 13:31:04 +0100 Subject: [PATCH 4/7] docs: define strict standalone deployment end state --- ...6-02-24-simplest-spritz-deployment-spec.md | 142 ++++++++++-------- 1 file changed, 76 insertions(+), 66 deletions(-) diff --git a/docs/2026-02-24-simplest-spritz-deployment-spec.md b/docs/2026-02-24-simplest-spritz-deployment-spec.md index 1738458..c639b5c 100644 --- a/docs/2026-02-24-simplest-spritz-deployment-spec.md +++ b/docs/2026-02-24-simplest-spritz-deployment-spec.md @@ -10,21 +10,34 @@ tags: [spritz, deployment, architecture] This document defines the default Spritz deployment model for the easiest possible install by a new operator. -The default must avoid path-routing tricks, custom edge workers, and multi-origin -front-end hosting. +The default must avoid path-routing tricks, custom edge workers, multi-origin +front-end hosting, and backward-compatibility branches. + +## Target End State + +- One hostname, one ingress, one Helm install. +- One routing model: + - `/` -> `spritz-ui` + - `/api` -> `spritz-api` +- API served only under `/api/*` (no root API routes). +- UI uses `/api` as its API base in default deployment mode. +- One canonical ingress config surface under `global.ingress`. +- No legacy fallback keys in the default chart path. ## Goals - Make first deployment possible with one hostname and one Helm install. - Keep UI and API in the same Kubernetes deployment surface. - Minimize required configuration values. -- Keep advanced networking patterns optional. +- Keep advanced networking patterns outside the default path. +- Keep defaults stable and production-oriented for standalone installs. ## Non-goals -- Optimizing for existing multi-app domain/path routing. +- Optimizing for existing multi-app domain/path routing in default mode. - Requiring provider-specific edge features for default setup. - Dropbox-grade conflict resolution in default storage mode. +- Preserving old/alternate ingress key paths. ## Default Deployment Model @@ -47,10 +60,11 @@ front-end hosting. The default installation should require only: -- `host`: public Spritz host (example: `spritz.example.com`) -- `ingressClassName`: ingress class (or gateway class) -- `tls.issuerRef` (or pre-provisioned secret) -- `storageClass`: default PVC storage class +- `global.host`: public Spritz host (example: `spritz.example.com`) +- `global.ingress.className`: ingress class +- `global.ingress.tls.enabled`: whether TLS is enabled +- `global.ingress.tls.secretName` (optional): pre-provisioned TLS secret name +- `operator.homePVC.storageClass` (optional): home PVC storage class override Everything else should have working defaults. @@ -59,36 +73,33 @@ Everything else should have working defaults. ```yaml global: host: spritz.example.com - -ingress: - enabled: true - className: nginx - tls: - enabled: true - issuerRef: - name: letsencrypt-prod - kind: ClusterIssuer + ingress: + className: nginx + tls: + enabled: true + secretName: "" ui: - enabled: true - basePath: / + ingress: + enabled: true apiBaseUrl: /api -api: - enabled: true - basePath: /api - -storage: - homePvc: +operator: + homePVC: enabled: true storageClassName: standard + + sharedMounts: + enabled: false + +api: sharedMounts: enabled: false ``` ## Implementation Scope (Exact Changes) -### Helm Values and Compatibility +### Helm Values (Strict v1) File: `helm/spritz/values.yaml` @@ -96,25 +107,21 @@ File: `helm/spritz/values.yaml` - Add `global.ingress.className` with default `nginx`. - Add `global.ingress.tls.enabled` (default `true`). - Add `global.ingress.tls.secretName` (default empty; operator-provided). -- Keep `ui.apiBaseUrl` default `/api`. -- Keep `ui.basePath` default `/`. - Keep `ui.ingress.enabled` default `true` for single-host installs. +- Keep `ui.apiBaseUrl` default `/api`. - Keep `operator.homePVC.enabled` default `true`. - Keep `operator.sharedMounts.enabled` and `api.sharedMounts.enabled` default `false`. - -Legacy keys that remain supported during transition: - -- `ui.ingress.host` -- `ui.ingress.className` -- `ui.ingress.path` -- explicit `ui.apiBaseUrl` +- Remove compatibility-only keys from the default path: + - `ui.ingress.host` + - `ui.ingress.className` + - `ui.ingress.path` + - `ui.basePath` ### Helm Templates Files: - `helm/spritz/templates/ui-deployment.yaml` -- `helm/spritz/templates/api-deployment.yaml` - `helm/spritz/templates/ui-api-ingress.yaml` (new) Required behavior: @@ -123,8 +130,8 @@ Required behavior: - Render one public ingress object with two ordered paths: - `/api` -> service `spritz-api` on `.Values.api.service.port` - `/` -> service `spritz-ui` on `.Values.ui.service.port` -- Source ingress class from `global.ingress.className`, fallback to `ui.ingress.className`. -- Source host from `global.host`, fallback to `ui.ingress.host`. +- Source ingress class only from `global.ingress.className`. +- Source host only from `global.host`. - Add TLS block when `global.ingress.tls.enabled` is true. - Keep service names unchanged (`spritz-api`, `spritz-ui`) to avoid rollout risk. @@ -132,9 +139,21 @@ Required behavior: File: `api/main.go` -- Register endpoints on both root and `/api` prefixes. -- Keep existing root routes for backward compatibility. -- Add `/api/healthz` alongside `/healthz` for path-based ingress health checks. +- Register API and internal endpoints only under `/api`. +- Expose health check at `/api/healthz`. +- Remove root-prefixed API routes from the public server surface. + +### UI Runtime Behavior + +Files: + +- `helm/spritz/templates/ui-deployment.yaml` +- `ui/entrypoint.sh` + +Required behavior: + +- Default runtime API base is `/api`. +- Do not require base-path routing logic for default standalone mode. ## Storage and Sync Defaults @@ -160,29 +179,18 @@ Advanced mode can support: These are explicitly optional and should be documented separately from the default install flow. -## Upgrade Behavior (Existing Installs) - -- This is still prelaunch v1; defaults can be optimized for new installs. -- Existing installs can preserve legacy behavior by pinning old ingress keys. -- No CRD schema change is required for this deployment change. -- Existing Spritz custom resources are not mutated by chart upgrade. -- Home PVC default change applies to newly created Spritz resources after upgrade. - -Compatibility precedence: +## Backward Compatibility Policy -- API URL resolution: - - explicit `ui.apiBaseUrl` wins - - else use `/api` when `ui.basePath` is set - - else use `/api` -- Host/class resolution: - - use `global.host` and `global.ingress.className` when set - - else fallback to `ui.ingress.host` and `ui.ingress.className` +- No backward compatibility contract is required for this prelaunch baseline. +- Remove compatibility paths instead of carrying long-term dual behavior. +- If values are renamed/removed, operators must adopt the new canonical keys. +- No CRD schema change is required for this deployment-focused work. ## Operational Guardrails Even in default mode, add these checks: -- Health endpoint checks for UI and API. +- Health endpoint checks for UI and `/api/healthz`. - TLS handshake check on the configured public host. - Alert on repeated `5xx` from ingress. @@ -199,8 +207,9 @@ After install: 1. Open `https://spritz.example.com`. 2. Confirm UI loads from `/`. 3. Confirm API health at `/api/healthz`. -4. Create a devbox and open terminal. -5. Recreate the pod and verify home state persists. +4. Confirm root API endpoint path is not served (for example `/healthz` is not used as the API health path). +5. Create a devbox via `/api/spritzes` and open terminal. +6. Recreate the pod and verify home state persists. Advanced mode validation should be a separate checklist. @@ -211,7 +220,6 @@ Advanced mode validation should be a separate checklist. Run: - `helm template spritz ./helm/spritz` -- `helm template spritz ./helm/spritz --set global.host= --set ui.ingress.host=legacy.example.com` Pass criteria: @@ -219,7 +227,7 @@ Pass criteria: - Path `/api` routes to `spritz-api`. - Path `/` routes to `spritz-ui`. - Default host comes from `global.host`. -- Legacy host fallback works when `global.host` is empty. +- Ingress class comes from `global.ingress.className`. ### API Route Checks @@ -229,9 +237,10 @@ Add tests in: Assertions: -- `GET /healthz` returns 200. - `GET /api/healthz` returns 200. -- Root and `/api` route variants hit identical auth and handler logic. +- `GET /healthz` is not the canonical health path for API routing. +- Secured API handlers are served under `/api`. +- Root-prefixed API paths are not part of default route surface. Run: @@ -254,5 +263,6 @@ Pass criteria: ## Decision Summary - Keep core Spritz architecture. -- Change deployment defaults toward single-host Kubernetes serving. -- Move edge/path-routing complexity behind an optional advanced setup. +- Use a strict single-host Kubernetes deployment default. +- Keep API under `/api` and UI under `/`. +- Keep edge/path-routing complexity outside the default deployment model. From 9f93e865517cce7dde2f2d40d5d982521581e61d Mon Sep 17 00:00:00 2001 From: Onur Date: Tue, 24 Feb 2026 13:39:10 +0100 Subject: [PATCH 5/7] feat: enforce strict standalone /api deployment mode --- api/main.go | 7 +-- api/main_routes_test.go | 31 +++++------ cli/src/index.ts | 2 +- e2e/local-smoke.sh | 12 ++--- helm/spritz/templates/ui-api-ingress.yaml | 64 +++++------------------ helm/spritz/templates/ui-deployment.yaml | 10 +--- helm/spritz/values.yaml | 4 -- ui/entrypoint.sh | 10 +--- 8 files changed, 37 insertions(+), 103 deletions(-) diff --git a/api/main.go b/api/main.go index f7806c6..ce2a795 100644 --- a/api/main.go +++ b/api/main.go @@ -178,12 +178,7 @@ func main() { } func (s *server) registerRoutes(e *echo.Echo) { - s.registerRoutesAtPrefix(e, "") - s.registerRoutesAtPrefix(e, "/api") -} - -func (s *server) registerRoutesAtPrefix(e *echo.Echo, prefix string) { - group := e.Group(prefix) + group := e.Group("/api") group.GET("/healthz", s.handleHealthz) internal := group.Group("/internal/v1", s.internalAuthMiddleware()) internal.GET("/shared-mounts/owner/:owner/:mount/latest", s.getSharedMountLatest) diff --git a/api/main_routes_test.go b/api/main_routes_test.go index f57c157..b56adb0 100644 --- a/api/main_routes_test.go +++ b/api/main_routes_test.go @@ -18,19 +18,19 @@ func TestRegisterRoutesExposesHealthzUnderRootAndAPI(t *testing.T) { e := echo.New() s.registerRoutes(e) - rootReq := httptest.NewRequest(http.MethodGet, "/healthz", nil) - rootRec := httptest.NewRecorder() - e.ServeHTTP(rootRec, rootReq) - if rootRec.Code != http.StatusOK { - t.Fatalf("expected /healthz to return 200, got %d", rootRec.Code) - } - apiReq := httptest.NewRequest(http.MethodGet, "/api/healthz", nil) apiRec := httptest.NewRecorder() e.ServeHTTP(apiRec, apiReq) if apiRec.Code != http.StatusOK { t.Fatalf("expected /api/healthz to return 200, got %d", apiRec.Code) } + + rootReq := httptest.NewRequest(http.MethodGet, "/healthz", nil) + rootRec := httptest.NewRecorder() + e.ServeHTTP(rootRec, rootReq) + if rootRec.Code != http.StatusNotFound { + t.Fatalf("expected /healthz to return 404, got %d", rootRec.Code) + } } func TestRegisterRoutesAppliesAuthToRootAndAPIPrefix(t *testing.T) { @@ -45,16 +45,6 @@ func TestRegisterRoutesAppliesAuthToRootAndAPIPrefix(t *testing.T) { e := echo.New() s.registerRoutes(e) - rootReq := httptest.NewRequest(http.MethodGet, "/spritzes", nil) - rootRec := httptest.NewRecorder() - e.ServeHTTP(rootRec, rootReq) - if rootRec.Code != http.StatusUnauthorized { - t.Fatalf("expected /spritzes to return 401 without auth, got %d", rootRec.Code) - } - if !strings.Contains(rootRec.Body.String(), "unauthenticated") { - t.Fatalf("expected /spritzes response to mention unauthenticated, got %q", rootRec.Body.String()) - } - apiReq := httptest.NewRequest(http.MethodGet, "/api/spritzes", nil) apiRec := httptest.NewRecorder() e.ServeHTTP(apiRec, apiReq) @@ -64,4 +54,11 @@ func TestRegisterRoutesAppliesAuthToRootAndAPIPrefix(t *testing.T) { if !strings.Contains(apiRec.Body.String(), "unauthenticated") { t.Fatalf("expected /api/spritzes response to mention unauthenticated, got %q", apiRec.Body.String()) } + + rootReq := httptest.NewRequest(http.MethodGet, "/spritzes", nil) + rootRec := httptest.NewRecorder() + e.ServeHTTP(rootRec, rootReq) + if rootRec.Code != http.StatusNotFound { + t.Fatalf("expected /spritzes to return 404, got %d", rootRec.Code) + } } diff --git a/cli/src/index.ts b/cli/src/index.ts index 6b61127..13a94b1 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -34,7 +34,7 @@ type TtyContext = { ttyState: string | null; }; -const defaultApiBase = 'http://localhost:8080'; +const defaultApiBase = 'http://localhost:8080/api'; const requestTimeoutMs = Number.parseInt(process.env.SPRITZ_REQUEST_TIMEOUT_MS || '10000', 10); const headerId = process.env.SPRITZ_API_HEADER_ID || 'X-Spritz-User-Id'; const headerEmail = process.env.SPRITZ_API_HEADER_EMAIL || 'X-Spritz-User-Email'; diff --git a/e2e/local-smoke.sh b/e2e/local-smoke.sh index 146291e..e418dba 100755 --- a/e2e/local-smoke.sh +++ b/e2e/local-smoke.sh @@ -122,14 +122,14 @@ API_PID=$! echo "waiting for API on port ${API_PORT}..." for _ in $(seq 1 "${API_WAIT_SECONDS}"); do - status="$(curl -sS -o /dev/null -w '%{http_code}' "http://localhost:${API_PORT}/healthz" || true)" + status="$(curl -sS -o /dev/null -w '%{http_code}' "http://localhost:${API_PORT}/api/healthz" || true)" if [[ "${status}" == "200" ]]; then break fi sleep 1 done -status="$(curl -sS -o /dev/null -w '%{http_code}' "http://localhost:${API_PORT}/healthz" || true)" +status="$(curl -sS -o /dev/null -w '%{http_code}' "http://localhost:${API_PORT}/api/healthz" || true)" if [[ "${status}" != "200" ]]; then echo "API did not become ready in ${API_WAIT_SECONDS}s" echo "operator log:" @@ -167,13 +167,13 @@ EOF mv "${LOG_DIR}/create.merged.json" "${LOG_DIR}/create.json" fi -curl -sS --fail -X POST "http://localhost:${API_PORT}/spritzes" \ +curl -sS --fail -X POST "http://localhost:${API_PORT}/api/spritzes" \ -H 'Content-Type: application/json' \ --data "@${LOG_DIR}/create.json" >/dev/null echo "waiting for spritz to become Ready..." for _ in {1..30}; do - if curl -sS --fail "http://localhost:${API_PORT}/spritzes/${SPRITZ_NAME}" | grep -q '"phase":"Ready"'; then + if curl -sS --fail "http://localhost:${API_PORT}/api/spritzes/${SPRITZ_NAME}" | grep -q '"phase":"Ready"'; then echo "spritz is Ready" break fi @@ -184,9 +184,9 @@ kubectl get deployment,service -n spritz -l spritz.sh/name="${SPRITZ_NAME}" if [[ -n "${SSH_MODE}" ]]; then echo "ssh info:" - curl -sS --fail "http://localhost:${API_PORT}/spritzes/${SPRITZ_NAME}" | jq '.status.ssh' + curl -sS --fail "http://localhost:${API_PORT}/api/spritzes/${SPRITZ_NAME}" | jq '.status.ssh' fi -curl -sS -X DELETE "http://localhost:${API_PORT}/spritzes/${SPRITZ_NAME}" -o /dev/null -w "deleted (%{http_code})\n" +curl -sS -X DELETE "http://localhost:${API_PORT}/api/spritzes/${SPRITZ_NAME}" -o /dev/null -w "deleted (%{http_code})\n" echo "done (logs in ${LOG_DIR})" diff --git a/helm/spritz/templates/ui-api-ingress.yaml b/helm/spritz/templates/ui-api-ingress.yaml index ff757b7..0547203 100644 --- a/helm/spritz/templates/ui-api-ingress.yaml +++ b/helm/spritz/templates/ui-api-ingress.yaml @@ -2,47 +2,11 @@ {{- if ne .Values.ui.namespace .Values.api.namespace }} {{- fail "ui.namespace and api.namespace must match when ui.ingress.enabled=true" }} {{- end }} -{{- $host := default "" .Values.ui.ingress.host -}} -{{- with .Values.global }} - {{- if .host }} - {{- $host = .host -}} - {{- end }} -{{- end }} -{{- $className := default "" .Values.ui.ingress.className -}} -{{- with .Values.global }} - {{- with .ingress }} - {{- if .className }} - {{- $className = .className -}} - {{- end }} - {{- end }} -{{- end }} -{{- $uiPath := default "/" .Values.ui.ingress.path -}} -{{- if eq (trim $uiPath) "" }} - {{- $uiPath = "/" -}} -{{- end }} -{{- if not (hasPrefix "/" $uiPath) }} - {{- $uiPath = printf "/%s" $uiPath -}} -{{- end }} -{{- if and (ne $uiPath "/") (hasSuffix "/" $uiPath) }} - {{- $uiPath = trimSuffix "/" $uiPath -}} +{{- if not .Values.global.host }} +{{- fail "global.host is required when ui.ingress.enabled=true" }} {{- end }} -{{- $apiPath := "/api" -}} -{{- if ne $uiPath "/" }} - {{- $apiPath = printf "%s/api" $uiPath -}} -{{- end }} -{{- $tlsEnabled := false -}} -{{- $tlsSecretName := "" -}} -{{- with .Values.global }} - {{- with .ingress }} - {{- with .tls }} - {{- if hasKey . "enabled" }} - {{- $tlsEnabled = .enabled -}} - {{- end }} - {{- if .secretName }} - {{- $tlsSecretName = .secretName -}} - {{- end }} - {{- end }} - {{- end }} +{{- if not .Values.global.ingress.className }} +{{- fail "global.ingress.className is required when ui.ingress.enabled=true" }} {{- end }} apiVersion: networking.k8s.io/v1 kind: Ingress @@ -50,35 +14,31 @@ metadata: name: spritz-web namespace: {{ .Values.ui.namespace }} spec: - {{- if $className }} - ingressClassName: {{ $className }} - {{- end }} + ingressClassName: {{ .Values.global.ingress.className }} rules: - - {{- if $host }} - host: {{ $host }} - {{- end }} + - host: {{ .Values.global.host }} http: paths: - - path: {{ $apiPath }} + - path: /api pathType: Prefix backend: service: name: spritz-api port: number: {{ .Values.api.service.port }} - - path: {{ $uiPath }} + - path: / pathType: Prefix backend: service: name: spritz-ui port: number: {{ .Values.ui.service.port }} - {{- if and $tlsEnabled $host }} + {{- if .Values.global.ingress.tls.enabled }} tls: - hosts: - - {{ $host }} - {{- if $tlsSecretName }} - secretName: {{ $tlsSecretName }} + - {{ .Values.global.host }} + {{- if .Values.global.ingress.tls.secretName }} + secretName: {{ .Values.global.ingress.tls.secretName }} {{- end }} {{- end }} {{- end }} diff --git a/helm/spritz/templates/ui-deployment.yaml b/helm/spritz/templates/ui-deployment.yaml index f5c4841..800daa2 100644 --- a/helm/spritz/templates/ui-deployment.yaml +++ b/helm/spritz/templates/ui-deployment.yaml @@ -31,15 +31,7 @@ spec: {{- end }} env: - name: SPRITZ_API_BASE_URL - {{- if .Values.ui.apiBaseUrl }} - value: {{ .Values.ui.apiBaseUrl | quote }} - {{- else if .Values.ui.basePath }} - value: {{ printf "%s/api" (.Values.ui.basePath | trimSuffix "/") | quote }} - {{- else }} - value: "/api" - {{- end }} - - name: SPRITZ_UI_BASE_PATH - value: {{ .Values.ui.basePath | quote }} + value: {{ .Values.ui.apiBaseUrl | default "/api" | quote }} - name: SPRITZ_UI_OWNER_ID value: {{ .Values.ui.ownerId | quote }} - name: SPRITZ_UI_AUTH_MODE diff --git a/helm/spritz/values.yaml b/helm/spritz/values.yaml index 2a11374..056fb1d 100644 --- a/helm/spritz/values.yaml +++ b/helm/spritz/values.yaml @@ -190,11 +190,7 @@ ui: containerPort: 8080 ingress: enabled: true - host: spritz.example.com - className: nginx - path: / apiBaseUrl: "/api" - basePath: "/" ownerId: "" assetVersion: "" presets: [] diff --git a/ui/entrypoint.sh b/ui/entrypoint.sh index e582315..aea2d84 100755 --- a/ui/entrypoint.sh +++ b/ui/entrypoint.sh @@ -2,7 +2,7 @@ set -eu API_BASE_URL="${SPRITZ_API_BASE_URL:-}" -BASE_PATH="${SPRITZ_UI_BASE_PATH:-}" +BASE_PATH="" OWNER_ID="${SPRITZ_UI_OWNER_ID:-}" AUTH_MODE="${SPRITZ_UI_AUTH_MODE:-}" AUTH_TOKEN_STORAGE="${SPRITZ_UI_AUTH_TOKEN_STORAGE:-}" @@ -27,14 +27,8 @@ DEFAULT_REPO_BRANCH="${SPRITZ_UI_DEFAULT_REPO_BRANCH:-}" HIDE_REPO_INPUTS="${SPRITZ_UI_HIDE_REPO_INPUTS:-}" ASSET_VERSION="${SPRITZ_UI_ASSET_VERSION:-}" -BASE_PATH="${BASE_PATH%/}" - if [ -z "$API_BASE_URL" ]; then - if [ -n "$BASE_PATH" ]; then - API_BASE_URL="${BASE_PATH%/}/api" - else - API_BASE_URL="/api" - fi + API_BASE_URL="/api" fi if [ -z "$ASSET_VERSION" ]; then ASSET_VERSION="$(date +%s)" From be4100da7de969836b88085e42373eb5323f03e9 Mon Sep 17 00:00:00 2001 From: Onur Date: Tue, 24 Feb 2026 13:49:04 +0100 Subject: [PATCH 6/7] refactor: remove remaining standalone path-compat plumbing --- api/auth_middleware_test.go | 8 ++++---- docs/2026-02-09-user-config-subset.md | 2 +- ...026-02-24-simplest-spritz-deployment-spec.md | 17 +++++++++++++++++ ui/entrypoint.sh | 6 +----- ui/public/app.js | 10 ++++------ ui/public/config.js | 1 - ui/public/index.html | 6 +++--- 7 files changed, 30 insertions(+), 20 deletions(-) diff --git a/api/auth_middleware_test.go b/api/auth_middleware_test.go index b21bf96..be4cdd9 100644 --- a/api/auth_middleware_test.go +++ b/api/auth_middleware_test.go @@ -19,12 +19,12 @@ func TestAuthMiddlewareRequiresHeader(t *testing.T) { handled := false secured := e.Group("", s.authMiddleware()) - secured.GET("/spritzes", func(c echo.Context) error { + secured.GET("/api/spritzes", func(c echo.Context) error { handled = true return c.JSON(http.StatusOK, map[string]string{"ok": "true"}) }) - req := httptest.NewRequest(http.MethodGet, "/spritzes", nil) + req := httptest.NewRequest(http.MethodGet, "/api/spritzes", nil) rec := httptest.NewRecorder() e.ServeHTTP(rec, req) @@ -48,7 +48,7 @@ func TestAuthMiddlewareSetsPrincipal(t *testing.T) { e := echo.New() secured := e.Group("", s.authMiddleware()) - secured.GET("/spritzes", func(c echo.Context) error { + secured.GET("/api/spritzes", func(c echo.Context) error { p, ok := principalFromContext(c) if !ok { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "missing principal"}) @@ -59,7 +59,7 @@ func TestAuthMiddlewareSetsPrincipal(t *testing.T) { }) }) - req := httptest.NewRequest(http.MethodGet, "/spritzes", nil) + req := httptest.NewRequest(http.MethodGet, "/api/spritzes", nil) req.Header.Set("X-Spritz-User-Id", "user-123") req.Header.Set("X-Spritz-User-Email", "user@example.com") rec := httptest.NewRecorder() diff --git a/docs/2026-02-09-user-config-subset.md b/docs/2026-02-09-user-config-subset.md index f28a3d7..756a5bf 100644 --- a/docs/2026-02-09-user-config-subset.md +++ b/docs/2026-02-09-user-config-subset.md @@ -77,7 +77,7 @@ The server remains the only writer of sensitive spec fields. Recommended endpoints: -- `POST /spritzes` accepts `userConfig` on create. +- `POST /api/spritzes` accepts `userConfig` on create. ## UI Behavior diff --git a/docs/2026-02-24-simplest-spritz-deployment-spec.md b/docs/2026-02-24-simplest-spritz-deployment-spec.md index c639b5c..cf121d9 100644 --- a/docs/2026-02-24-simplest-spritz-deployment-spec.md +++ b/docs/2026-02-24-simplest-spritz-deployment-spec.md @@ -260,6 +260,23 @@ Pass criteria: - No provider-specific values are introduced. - Documentation conventions pass. +## Remaining Work (Now) + +No additional functional cleanup is required for the strict standalone target. + +Current code paths are aligned to: + +- UI at `/` +- API at `/api/*` +- Canonical ingress config under `global.ingress` +- No runtime `basePath` compatibility plumbing in UI assets/entrypoint/chart + +Known pre-existing non-blocker: + +- `./scripts/verify-agnostic.sh` currently fails on + `operator/controllers/home_pvc_test.go` due an existing fixture value + (`"spritz/app"`), unrelated to this deployment model implementation. + ## Decision Summary - Keep core Spritz architecture. diff --git a/ui/entrypoint.sh b/ui/entrypoint.sh index aea2d84..2585a98 100755 --- a/ui/entrypoint.sh +++ b/ui/entrypoint.sh @@ -2,7 +2,6 @@ set -eu API_BASE_URL="${SPRITZ_API_BASE_URL:-}" -BASE_PATH="" OWNER_ID="${SPRITZ_UI_OWNER_ID:-}" AUTH_MODE="${SPRITZ_UI_AUTH_MODE:-}" AUTH_TOKEN_STORAGE="${SPRITZ_UI_AUTH_TOKEN_STORAGE:-}" @@ -40,7 +39,6 @@ escape_sed() { API_BASE_URL_ESCAPED="$(escape_sed "$API_BASE_URL")" OWNER_ID_ESCAPED="$(escape_sed "$OWNER_ID")" -BASE_PATH_ESCAPED="$(escape_sed "$BASE_PATH")" AUTH_MODE_ESCAPED="$(escape_sed "$AUTH_MODE")" AUTH_TOKEN_STORAGE_ESCAPED="$(escape_sed "$AUTH_TOKEN_STORAGE")" AUTH_TOKEN_STORAGE_KEYS_ESCAPED="$(escape_sed "$AUTH_TOKEN_STORAGE_KEYS")" @@ -66,7 +64,6 @@ ASSET_VERSION_ESCAPED="$(escape_sed "$ASSET_VERSION")" sed "s|__SPRITZ_API_BASE_URL__|${API_BASE_URL_ESCAPED}|g" /usr/share/nginx/html/config.js \ | sed "s|__SPRITZ_OWNER_ID__|${OWNER_ID_ESCAPED}|g" \ - | sed "s|__SPRITZ_BASE_PATH__|${BASE_PATH_ESCAPED}|g" \ | sed "s|__SPRITZ_UI_AUTH_MODE__|${AUTH_MODE_ESCAPED}|g" \ | sed "s|__SPRITZ_UI_AUTH_TOKEN_STORAGE__|${AUTH_TOKEN_STORAGE_ESCAPED}|g" \ | sed "s|__SPRITZ_UI_AUTH_TOKEN_STORAGE_KEYS__|${AUTH_TOKEN_STORAGE_KEYS_ESCAPED}|g" \ @@ -91,8 +88,7 @@ sed "s|__SPRITZ_API_BASE_URL__|${API_BASE_URL_ESCAPED}|g" /usr/share/nginx/html/ > /usr/share/nginx/html/config.runtime.js mv /usr/share/nginx/html/config.runtime.js /usr/share/nginx/html/config.js -sed "s|__SPRITZ_BASE_PATH__|${BASE_PATH_ESCAPED}|g" /usr/share/nginx/html/index.html \ - | sed "s|__SPRITZ_UI_ASSET_VERSION__|${ASSET_VERSION_ESCAPED}|g" \ +sed "s|__SPRITZ_UI_ASSET_VERSION__|${ASSET_VERSION_ESCAPED}|g" /usr/share/nginx/html/index.html \ > /usr/share/nginx/html/index.runtime.html mv /usr/share/nginx/html/index.runtime.html /usr/share/nginx/html/index.html diff --git a/ui/public/app.js b/ui/public/app.js index c201e15..44f46e8 100644 --- a/ui/public/app.js +++ b/ui/public/app.js @@ -1,6 +1,5 @@ const config = window.SPRITZ_CONFIG || { apiBaseUrl: '' }; const apiBaseUrl = config.apiBaseUrl || ''; -const basePath = (config.basePath || '').replace(/\/$/, ''); const authConfig = config.auth || {}; const authMode = (authConfig.mode || '').toLowerCase(); const authTokenStorage = (authConfig.tokenStorage || 'localStorage').toLowerCase(); @@ -609,7 +608,7 @@ function shouldRedirectOnUnauthorized() { } function buildReturnToUrl() { - const path = window.location.pathname || basePath || '/'; + const path = window.location.pathname || '/'; const search = window.location.search || ''; const hash = window.location.hash || ''; const returnPath = `${path}${search}${hash}`; @@ -971,8 +970,7 @@ function renderList(items) { } function terminalPagePath(name) { - const prefix = basePath || ''; - return `${prefix}#terminal/${encodeURIComponent(name)}`; + return `#terminal/${encodeURIComponent(name)}`; } function terminalNameFromPath() { @@ -1006,7 +1004,7 @@ function renderTerminalPage(name) { const back = document.createElement('a'); back.className = 'terminal-back'; - back.href = basePath || '/'; + back.href = '/'; back.textContent = 'Back to spritzes'; const status = document.createElement('span'); @@ -1082,7 +1080,7 @@ function loadTerminalAssets() { function assetUrl(path) { const normalized = path.startsWith('/') ? path : `/${path}`; - return `${basePath}${normalized}`; + return normalized; } function loadStylesheet(href) { diff --git a/ui/public/config.js b/ui/public/config.js index bd47ef0..5262139 100644 --- a/ui/public/config.js +++ b/ui/public/config.js @@ -1,7 +1,6 @@ window.SPRITZ_CONFIG = { apiBaseUrl: '__SPRITZ_API_BASE_URL__', ownerId: '__SPRITZ_OWNER_ID__', - basePath: '__SPRITZ_BASE_PATH__', presets: '__SPRITZ_UI_PRESETS__', repoDefaults: { url: '__SPRITZ_UI_DEFAULT_REPO_URL__', diff --git a/ui/public/index.html b/ui/public/index.html index dfbf13e..630a831 100644 --- a/ui/public/index.html +++ b/ui/public/index.html @@ -4,7 +4,7 @@ Spritz - +
@@ -76,7 +76,7 @@

Active spritzes

- - + + From b5f4525fe20f2f3dd72cdd9f8ae5e81da81b3b09 Mon Sep 17 00:00:00 2001 From: Onur Date: Tue, 24 Feb 2026 13:55:42 +0100 Subject: [PATCH 7/7] test: harden standalone checks and agnostic fixtures --- e2e/local-smoke.sh | 12 ++++++++++++ operator/controllers/home_pvc_test.go | 2 +- operator/controllers/spritz_url_test.go | 8 ++++---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/e2e/local-smoke.sh b/e2e/local-smoke.sh index e418dba..7999d45 100755 --- a/e2e/local-smoke.sh +++ b/e2e/local-smoke.sh @@ -139,6 +139,18 @@ if [[ "${status}" != "200" ]]; then exit 1 fi +root_health_status="$(curl -sS -o /dev/null -w '%{http_code}' "http://localhost:${API_PORT}/healthz" || true)" +if [[ "${root_health_status}" != "404" ]]; then + echo "expected root health endpoint to return 404, got ${root_health_status}" + exit 1 +fi + +root_list_status="$(curl -sS -o /dev/null -w '%{http_code}' "http://localhost:${API_PORT}/spritzes" || true)" +if [[ "${root_list_status}" != "404" ]]; then + echo "expected root spritzes endpoint to return 404, got ${root_list_status}" + exit 1 +fi + cat < "${LOG_DIR}/create.json" { "name": "${SPRITZ_NAME}", diff --git a/operator/controllers/home_pvc_test.go b/operator/controllers/home_pvc_test.go index 77169ba..c1d1491 100644 --- a/operator/controllers/home_pvc_test.go +++ b/operator/controllers/home_pvc_test.go @@ -223,7 +223,7 @@ func TestValidateRepoDir(t *testing.T) { }{ {"empty ok", "", false}, {"relative ok", "spritz", false}, - {"relative nested ok", "spritz/app", false}, + {"relative nested ok", "project/app", false}, {"relative up invalid", "../etc", true}, {"relative up nested invalid", "foo/../../etc", true}, {"absolute workspace ok", "/workspace/spritz", false}, diff --git a/operator/controllers/spritz_url_test.go b/operator/controllers/spritz_url_test.go index af922eb..bf34d51 100644 --- a/operator/controllers/spritz_url_test.go +++ b/operator/controllers/spritz_url_test.go @@ -10,11 +10,11 @@ func TestSpritzURLIngressAddsTrailingSlash(t *testing.T) { spritz := &spritzv1.Spritz{} spritz.Spec.Ingress = &spritzv1.SpritzIngress{ Host: "console.example.com", - Path: "/spritz/w/tidy-fjord", + Path: "/workspaces/w/tidy-fjord", } got := spritzURL(spritz) - want := "https://console.example.com/spritz/w/tidy-fjord/" + want := "https://console.example.com/workspaces/w/tidy-fjord/" if got != want { t.Fatalf("expected %q, got %q", want, got) } @@ -38,11 +38,11 @@ func TestSpritzURLIngressKeepsExistingTrailingSlash(t *testing.T) { spritz := &spritzv1.Spritz{} spritz.Spec.Ingress = &spritzv1.SpritzIngress{ Host: "console.example.com", - Path: "/spritz/w/tidy-fjord/", + Path: "/workspaces/w/tidy-fjord/", } got := spritzURL(spritz) - want := "https://console.example.com/spritz/w/tidy-fjord/" + want := "https://console.example.com/workspaces/w/tidy-fjord/" if got != want { t.Fatalf("expected %q, got %q", want, got) }