From 02611610be843eee72ccefa998207498fadc17c0 Mon Sep 17 00:00:00 2001 From: Olof Mattsson Date: Thu, 28 May 2026 20:41:55 +0200 Subject: [PATCH 1/2] test(contract): assert pkg/types request shapes against backend swagger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #97. Adds a unit-test-only contract layer that validates every stackctl request struct against the backend's published OpenAPI (Swagger 2.0) schema. Catches the class of bug shipped four times this week (#95, #98, k8s-sm#264, the BulkOperationResult shape) where unit / integration / e2e tests all stub the backend and decode requests into stackctl's OWN types — so a json-tag drift between stackctl and the backend decodes cleanly in tests but 400s the moment a real backend reads the bytes. What ships: cli/test/contract/testdata/swagger.json Vendored copy of omattsson/k8s-stack-manager:backend/docs/swagger.json. Pinned (not auto-fetched) so backend-side wire changes show up as explicit diffs in this repo. cli/test/contract/refresh-swagger.sh One-shot refresh: `./refresh-swagger.sh [ref]`. Validates the payload is JSON, sha256-compares before overwriting, and tells you which test to re-run. Pinned to a ref so partial backend rollouts don't break stackctl CI. cli/test/contract/contract_test.go Embeds swagger.json via go:embed, walks 9 request structs via reflection, and asserts bidirectionally: 1. Every Go json tag exists as a property in swagger. Catches stale or typoed Go-side fields. 2. Every swagger "required" field has a Go json tag. Catches missing-required-field bugs. 3. Field types align (string/boolean/integer/number/array/object). Covers: CreateClusterRequest, UpdateClusterRequest, BulkInstancesRequest, BulkTemplatesRequest, RegisterRequest, LoginRequest, CreateAPIKeyRequest, ResetPasswordRequest, CreateCleanupPolicyRequest. Plus TestSwaggerVendorIntegrity — sanity floor on the vendored copy so a truncated swagger.json doesn't manifest as a pile of confusing per-case failures. Failure-mode validated with a synthetic drift (matches the pre-fix/bulk-wire-contract bug exactly): Error: Go struct field Ids (json:"ids") has no matching property in swagger schema — typo, or backend doesn't accept this field Error: swagger schema requires field "instance_ids" but the Go struct has no field with that json tag — request will fail validation V1 deliberately skips: - Response types (issue calls out polymorphism for GET endpoints; a separate iteration). - CreateTemplateRequest / UpdateTemplateRequest — backend's swag annotation uses an unexported struct (createTemplateRequest) so no schema is published. Worth a tiny upstream PR to make the type exported + annotated. Co-Authored-By: Claude Opus 4.7 --- cli/test/contract/contract_test.go | 345 + cli/test/contract/refresh-swagger.sh | 48 + cli/test/contract/testdata/swagger.json | 10640 ++++++++++++++++++++++ 3 files changed, 11033 insertions(+) create mode 100644 cli/test/contract/contract_test.go create mode 100755 cli/test/contract/refresh-swagger.sh create mode 100644 cli/test/contract/testdata/swagger.json diff --git a/cli/test/contract/contract_test.go b/cli/test/contract/contract_test.go new file mode 100644 index 0000000..c777e74 --- /dev/null +++ b/cli/test/contract/contract_test.go @@ -0,0 +1,345 @@ +// Package contract validates that stackctl's pkg/types request structs +// match the backend's published OpenAPI (Swagger 2.0) schema. +// +// Why this exists: every other test layer in stackctl (unit, integration, +// e2e) stubs the backend with httptest and decodes request bodies into +// stackctl's OWN types — so a JSON-tag drift between stackctl and the +// backend decodes cleanly in tests but 400s the moment a real backend +// reads it. Four shipped wire-shape bugs (#95, #98, k8s-sm#264, the +// BulkOperationResult shape) all slipped through that blind spot. +// +// The schema is vendored at testdata/swagger.json; refresh it via the +// refresh-swagger.sh script when the backend ships a new field. +package contract + +import ( + _ "embed" + "encoding/json" + "reflect" + "sort" + "strings" + "testing" + + "github.com/omattsson/stackctl/cli/pkg/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +//go:embed testdata/swagger.json +var swaggerJSON []byte + +// swaggerSchema is the minimum subset of OpenAPI 2.0 we need to walk +// definitions. Properties are decoded as a raw map so per-property type +// strings stay accessible without modelling every JSON Schema corner. +type swaggerSchema struct { + Definitions map[string]swaggerDefinition `json:"definitions"` +} + +type swaggerDefinition struct { + Type string `json:"type"` + Required []string `json:"required"` + Properties map[string]swaggerPropertyV2 `json:"properties"` +} + +type swaggerPropertyV2 struct { + Type string `json:"type"` + Format string `json:"format,omitempty"` + Items *itemsRef `json:"items,omitempty"` + Ref string `json:"$ref,omitempty"` + Extras map[string]any `json:"-"` // unused, kept for clarity +} + +type itemsRef struct { + Type string `json:"type"` + Ref string `json:"$ref,omitempty"` +} + +// contractCase pairs a Go request struct with the swagger definition +// it must conform to. ExcludeGoFields is the escape hatch for the +// (rare) case where stackctl intentionally carries a field the backend +// doesn't validate — e.g. write-only credentials the server elides on +// read, or stackctl-side display aliases. +type contractCase struct { + name string + goType any + swaggerDef string + excludeGoTags []string // Go json tags to skip on the "Go ⊂ swagger" check + excludeRequire []string // swagger required fields to skip on the "required ⊂ Go" check +} + +// TestRequestSchemas_MatchBackend asserts that every stackctl request +// type in the table matches the backend's OpenAPI definition both ways: +// +// 1. Every json tag on the Go struct exists as a property in the +// swagger schema. Catches stale or typoed Go-side fields. +// 2. Every "required" field in the swagger schema has a matching Go +// json tag. Catches missing-required-field bugs. +// 3. Field type alignment — Go reflect.Kind maps to the swagger +// property's `type` string. +// +// Failure messages name the drifting field on both sides so the fix +// (rename, add, or update the exclusion list) is one diff away. +func TestRequestSchemas_MatchBackend(t *testing.T) { + t.Parallel() + schema := loadSwagger(t) + + cases := []contractCase{ + { + name: "CreateClusterRequest", + goType: types.CreateClusterRequest{}, + swaggerDef: "handlers.CreateClusterRequest", + }, + { + name: "UpdateClusterRequest", + goType: types.UpdateClusterRequest{}, + swaggerDef: "handlers.UpdateClusterRequest", + }, + { + name: "BulkInstancesRequest", + goType: types.BulkInstancesRequest{}, + swaggerDef: "handlers.BulkOperationRequest", + }, + { + name: "BulkTemplatesRequest", + goType: types.BulkTemplatesRequest{}, + swaggerDef: "handlers.BulkTemplateRequest", + }, + { + name: "RegisterRequest", + goType: types.RegisterRequest{}, + swaggerDef: "handlers.RegisterRequest", + }, + { + name: "LoginRequest", + goType: types.LoginRequest{}, + swaggerDef: "handlers.LoginRequest", + }, + { + name: "CreateAPIKeyRequest", + goType: types.CreateAPIKeyRequest{}, + swaggerDef: "handlers.CreateAPIKeyRequest", + }, + { + name: "ResetPasswordRequest", + goType: types.ResetPasswordRequest{}, + swaggerDef: "handlers.ResetPasswordRequest", + }, + { + name: "CreateCleanupPolicyRequest", + goType: types.CreateCleanupPolicyRequest{}, + swaggerDef: "models.CleanupPolicy", + // models.CleanupPolicy is the backend's read-side type with + // timestamps + id; the write-side stackctl request + // intentionally omits them. They're not "required" in + // swagger either, so no required-side exclusion needed. + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + def, ok := schema.Definitions[tc.swaggerDef] + require.Truef(t, ok, "swagger schema is missing definition %q (refresh swagger.json?)", tc.swaggerDef) + assertFieldsMatch(t, tc.goType, def, tc.excludeGoTags, tc.excludeRequire) + }) + } +} + +func loadSwagger(t *testing.T) swaggerSchema { + t.Helper() + var s swaggerSchema + require.NoError(t, json.Unmarshal(swaggerJSON, &s), "parse vendored swagger.json") + require.NotEmptyf(t, s.Definitions, "swagger.json has no .definitions — refresh script broken?") + return s +} + +// assertFieldsMatch walks every exported field of goType, reads its +// `json:` tag, and asserts the field exists in def.Properties with a +// compatible type. The reverse direction is handled by the required-set +// check at the end. +func assertFieldsMatch(t *testing.T, goType any, def swaggerDefinition, excludeGoTags, excludeRequire []string) { + t.Helper() + + excludeGo := make(map[string]struct{}, len(excludeGoTags)) + for _, n := range excludeGoTags { + excludeGo[n] = struct{}{} + } + excludeReq := make(map[string]struct{}, len(excludeRequire)) + for _, n := range excludeRequire { + excludeReq[n] = struct{}{} + } + + tags := collectJSONTags(reflect.TypeOf(goType)) + + // 1. Go ⊂ swagger.Properties — every Go tag must exist as a + // property in the swagger definition (catches typos, stale fields). + for name, gf := range tags { + if _, skip := excludeGo[name]; skip { + continue + } + prop, ok := def.Properties[name] + if !assert.Truef(t, ok, + "Go struct field %s (json:%q) has no matching property in swagger schema — typo, or backend doesn't accept this field", + gf.GoName, name) { + continue + } + // 3. Type alignment. + assertTypeCompatible(t, name, gf, prop) + } + + // 2. swagger.Required ⊂ Go — every "required" swagger field must + // have a matching json tag in Go (catches missing required fields). + for _, req := range def.Required { + if _, skip := excludeReq[req]; skip { + continue + } + _, ok := tags[req] + assert.Truef(t, ok, + "swagger schema requires field %q but the Go struct has no field with that json tag — request will fail validation", + req) + } +} + +// goFieldInfo is everything assertTypeCompatible needs about a Go field. +type goFieldInfo struct { + GoName string // Go field name (for error messages) + Kind reflect.Kind // base Kind, with Ptr unwrapped + IsSlice bool + ElemKind reflect.Kind // only meaningful when IsSlice +} + +// collectJSONTags returns a map from json tag name to goFieldInfo for +// every exported field of t. Embedded structs are flattened (matches +// json package's behaviour). Fields tagged `json:"-"` are skipped. +func collectJSONTags(t reflect.Type) map[string]goFieldInfo { + out := map[string]goFieldInfo{} + walkFields(t, out) + return out +} + +func walkFields(t reflect.Type, out map[string]goFieldInfo) { + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return + } + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if !f.IsExported() { + continue + } + // Anonymous embedded struct: flatten its fields up to this + // level, matching encoding/json semantics. + if f.Anonymous { + walkFields(f.Type, out) + continue + } + tag := f.Tag.Get("json") + if tag == "" || tag == "-" { + continue + } + name := strings.SplitN(tag, ",", 2)[0] + if name == "" { + continue + } + info := goFieldInfo{GoName: f.Name, Kind: f.Type.Kind()} + // Unwrap pointer: a *string is still a "string" on the wire. + if info.Kind == reflect.Ptr { + info.Kind = f.Type.Elem().Kind() + } + if info.Kind == reflect.Slice || info.Kind == reflect.Array { + info.IsSlice = true + info.ElemKind = f.Type.Elem().Kind() + if info.ElemKind == reflect.Ptr { + info.ElemKind = f.Type.Elem().Elem().Kind() + } + } + out[name] = info + } +} + +// assertTypeCompatible checks that a Go field's reflect.Kind aligns +// with the swagger property's `type` string. We're deliberately +// permissive: int vs int64 both map to "integer" and we accept +// "string" for time.Time (swagger uses format:"date-time"). +func assertTypeCompatible(t *testing.T, fieldName string, gf goFieldInfo, prop swaggerPropertyV2) { + t.Helper() + if prop.Type == "" && prop.Ref != "" { + // Property is a $ref to another schema — likely an object/struct + // composition. We don't recursively validate refs in V1; the + // presence of the field is what matters for catching drift. + return + } + + wantSwaggerType := goKindToSwagger(gf.Kind) + if gf.IsSlice { + wantSwaggerType = "array" + } + + if !assert.Equalf(t, wantSwaggerType, prop.Type, + "field %q: Go kind %s maps to swagger type %q, but schema says %q", + fieldName, gf.Kind, wantSwaggerType, prop.Type) { + return + } + + if gf.IsSlice && prop.Items != nil && prop.Items.Type != "" { + wantElem := goKindToSwagger(gf.ElemKind) + assert.Equalf(t, wantElem, prop.Items.Type, + "field %q: Go slice element kind %s maps to swagger items.type %q, but schema says %q", + fieldName, gf.ElemKind, wantElem, prop.Items.Type) + } +} + +func goKindToSwagger(k reflect.Kind) string { + switch k { + case reflect.String: + return "string" + case reflect.Bool: + return "boolean" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return "integer" + case reflect.Float32, reflect.Float64: + return "number" + case reflect.Slice, reflect.Array: + return "array" + case reflect.Map, reflect.Struct: + return "object" + } + return "" +} + +// TestSwaggerVendorIntegrity is a smoke test that the vendored copy is +// parseable and non-trivial. A 0-byte or html-404 file would otherwise +// fail much later with confusing per-case errors. +func TestSwaggerVendorIntegrity(t *testing.T) { + t.Parallel() + s := loadSwagger(t) + // Sanity floor — the real schema has hundreds of definitions; a + // truncated vendor would dip below this. + require.Greaterf(t, len(s.Definitions), 50, + "vendored swagger.json has only %d definitions — likely truncated. Re-run refresh-swagger.sh.", + len(s.Definitions)) + + // Confirm the handlers we depend on are present. Drift here means + // the backend renamed or removed a handler type without us noticing. + want := []string{ + "handlers.CreateClusterRequest", + "handlers.UpdateClusterRequest", + "handlers.BulkOperationRequest", + "handlers.BulkTemplateRequest", + "handlers.RegisterRequest", + "handlers.LoginRequest", + "handlers.CreateAPIKeyRequest", + "handlers.ResetPasswordRequest", + "models.CleanupPolicy", + } + sort.Strings(want) + for _, name := range want { + _, ok := s.Definitions[name] + assert.Truef(t, ok, "vendored swagger.json is missing definition %q — backend renamed or removed it?", name) + } +} + diff --git a/cli/test/contract/refresh-swagger.sh b/cli/test/contract/refresh-swagger.sh new file mode 100755 index 0000000..123d767 --- /dev/null +++ b/cli/test/contract/refresh-swagger.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# +# refresh-swagger.sh — pull the latest swagger.json from +# omattsson/k8s-stack-manager and refresh the vendored copy used by +# the contract tests. +# +# The contract tests compare stackctl's request types against the +# backend's published OpenAPI schema. When the backend lands a new +# field (or renames one), run this script, re-run the tests, and +# adjust stackctl's pkg/types or the contract test's exclusion list +# until everything aligns. +# +# Usage: +# ./refresh-swagger.sh # fetch from main +# ./refresh-swagger.sh # fetch from a specific tag/branch/sha +# +# Requires: curl, shasum. +set -euo pipefail + +REF="${1:-main}" +URL="https://raw.githubusercontent.com/omattsson/k8s-stack-manager/${REF}/backend/docs/swagger.json" + +cd "$(dirname "$0")/testdata" + +echo "Fetching swagger.json from ${REF}…" >&2 +tmp=$(mktemp) +trap 'rm -f "$tmp"' EXIT +curl -fsSL "$URL" -o "$tmp" + +# Refuse to overwrite if the fetched payload isn't valid JSON — protects +# against fetching a 404 HTML page that happens to be 200 from a CDN. +if ! python3 -c "import json,sys; json.load(open('$tmp'))" 2>/dev/null; then + echo "ERROR: fetched file is not valid JSON, refusing to overwrite" >&2 + exit 1 +fi + +old_sum=$(shasum -a 256 swagger.json | cut -d' ' -f1) +new_sum=$(shasum -a 256 "$tmp" | cut -d' ' -f1) + +if [ "$old_sum" = "$new_sum" ]; then + echo "swagger.json unchanged (sha256 ${old_sum:0:12}…)" >&2 + exit 0 +fi + +mv "$tmp" swagger.json +trap - EXIT +echo "Updated swagger.json (sha256 ${new_sum:0:12}…, was ${old_sum:0:12}…)" >&2 +echo "Now run: go test ./cli/test/contract/..." >&2 diff --git a/cli/test/contract/testdata/swagger.json b/cli/test/contract/testdata/swagger.json new file mode 100644 index 0000000..2eb1cfc --- /dev/null +++ b/cli/test/contract/testdata/swagger.json @@ -0,0 +1,10640 @@ +{ + "produces": [ + "application/json" + ], + "schemes": [ + "http", + "https" + ], + "swagger": "2.0", + "info": { + "description": "This is the API documentation for the backend service", + "title": "Backend API", + "contact": {}, + "version": "1.0" + }, + "host": "localhost:8081", + "basePath": "/", + "paths": { + "/api/v1/admin/cleanup-policies": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns all cleanup policies", + "produces": [ + "application/json" + ], + "tags": [ + "cleanup-policies" + ], + "summary": "List all cleanup policies", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.CleanupPolicy" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Creates a new cleanup policy and reloads the scheduler", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "cleanup-policies" + ], + "summary": "Create a cleanup policy", + "parameters": [ + { + "description": "Cleanup policy", + "name": "policy", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CleanupPolicy" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.CleanupPolicy" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/admin/cleanup-policies/{id}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Updates an existing cleanup policy and reloads the scheduler", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "cleanup-policies" + ], + "summary": "Update a cleanup policy", + "parameters": [ + { + "type": "string", + "description": "Policy ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Cleanup policy", + "name": "policy", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.CleanupPolicy" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.CleanupPolicy" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Deletes a cleanup policy and reloads the scheduler", + "tags": [ + "cleanup-policies" + ], + "summary": "Delete a cleanup policy", + "parameters": [ + { + "type": "string", + "description": "Policy ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/admin/cleanup-policies/{id}/run": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Executes a cleanup policy immediately. Use ?dry_run=true to preview matches without acting.", + "produces": [ + "application/json" + ], + "tags": [ + "cleanup-policies" + ], + "summary": "Run a cleanup policy manually", + "parameters": [ + { + "type": "string", + "description": "Policy ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "boolean", + "description": "Dry run mode", + "name": "dry_run", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/scheduler.CleanupResult" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/admin/notification-channels": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns all notification channels with subscription counts", + "produces": [ + "application/json" + ], + "tags": [ + "notification-channels" + ], + "summary": "List all notification channels", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.notificationChannelWithCount" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Creates a new notification channel", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notification-channels" + ], + "summary": "Create a notification channel", + "parameters": [ + { + "description": "Channel", + "name": "channel", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.createChannelRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.NotificationChannel" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Conflict", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/admin/notification-channels/event-types": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns all known event types that channels can subscribe to", + "produces": [ + "application/json" + ], + "tags": [ + "notification-channels" + ], + "summary": "List all event types", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/admin/notification-channels/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns a notification channel by ID", + "produces": [ + "application/json" + ], + "tags": [ + "notification-channels" + ], + "summary": "Get a notification channel", + "parameters": [ + { + "type": "string", + "description": "Channel ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.NotificationChannel" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Updates an existing notification channel", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notification-channels" + ], + "summary": "Update a notification channel", + "parameters": [ + { + "type": "string", + "description": "Channel ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Channel updates", + "name": "channel", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateChannelRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.NotificationChannel" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Conflict", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Deletes a notification channel and its subscriptions", + "tags": [ + "notification-channels" + ], + "summary": "Delete a notification channel", + "parameters": [ + { + "type": "string", + "description": "Channel ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/admin/notification-channels/{id}/delivery-logs": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns paginated delivery logs for a notification channel", + "produces": [ + "application/json" + ], + "tags": [ + "notification-channels" + ], + "summary": "List delivery logs for a channel", + "parameters": [ + { + "type": "string", + "description": "Channel ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Page size (default 20)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset (default 0)", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/admin/notification-channels/{id}/subscriptions": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns event type subscriptions for a channel", + "produces": [ + "application/json" + ], + "tags": [ + "notification-channels" + ], + "summary": "Get channel subscriptions", + "parameters": [ + { + "type": "string", + "description": "Channel ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Replaces all event type subscriptions for a channel", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notification-channels" + ], + "summary": "Update channel subscriptions", + "parameters": [ + { + "type": "string", + "description": "Channel ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Event types", + "name": "subscriptions", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateSubscriptionsRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/admin/notification-channels/{id}/test": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Sends a test payload to the channel's webhook URL", + "produces": [ + "application/json" + ], + "tags": [ + "notification-channels" + ], + "summary": "Test a notification channel", + "parameters": [ + { + "type": "string", + "description": "Channel ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/admin/orphaned-namespaces": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Lists all Kubernetes namespaces matching the stack-* pattern that have no corresponding stack instance in the database. Pass ?details=true to include resource counts and helm releases per namespace (expensive).", + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "List orphaned namespaces", + "parameters": [ + { + "type": "string", + "description": "Include resource counts and helm releases (true/false)", + "name": "details", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.OrphanedNamespaceResponse" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/admin/orphaned-namespaces/{namespace}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Verifies the namespace is orphaned, uninstalls all Helm releases, and deletes the Kubernetes namespace", + "produces": [ + "application/json" + ], + "tags": [ + "Admin" + ], + "summary": "Delete an orphaned namespace", + "parameters": [ + { + "type": "string", + "description": "Namespace name", + "name": "namespace", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Conflict", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/analytics/overview": { + "get": { + "description": "Returns high-level aggregate counts (templates, definitions, instances, deploys, users)", + "produces": [ + "application/json" + ], + "tags": [ + "analytics" + ], + "summary": "Get platform overview statistics", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.OverviewStats" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/analytics/templates": { + "get": { + "description": "Returns usage analytics for each template including definition count, instance count, deploy counts, and success rate", + "produces": [ + "application/json" + ], + "tags": [ + "analytics" + ], + "summary": "Get per-template usage statistics", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.TemplateStats" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/analytics/users": { + "get": { + "description": "Returns usage analytics per user including instance count, deploy count, and last active time (admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "analytics" + ], + "summary": "Get per-user usage statistics", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.UserStats" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/audit-logs": { + "get": { + "description": "List audit logs with optional filters and pagination. Supports cursor-based pagination for efficient large dataset traversal.", + "produces": [ + "application/json" + ], + "tags": [ + "audit-logs" + ], + "summary": "List audit logs", + "parameters": [ + { + "type": "string", + "description": "Filter by user ID", + "name": "user_id", + "in": "query" + }, + { + "type": "string", + "description": "Filter by entity type", + "name": "entity_type", + "in": "query" + }, + { + "type": "string", + "description": "Filter by entity ID", + "name": "entity_id", + "in": "query" + }, + { + "type": "string", + "description": "Filter by action", + "name": "action", + "in": "query" + }, + { + "type": "string", + "description": "Start date (RFC3339)", + "name": "start_date", + "in": "query" + }, + { + "type": "string", + "description": "End date (RFC3339)", + "name": "end_date", + "in": "query" + }, + { + "type": "integer", + "description": "Page size (default 25)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset (default 0)", + "name": "offset", + "in": "query" + }, + { + "type": "string", + "description": "Cursor from previous page for cursor-based pagination (overrides offset)", + "name": "cursor", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PaginatedAuditLogs" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/audit-logs/export": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Export audit logs as CSV or JSON file download", + "produces": [ + "application/octet-stream" + ], + "tags": [ + "audit-logs" + ], + "summary": "Export audit logs", + "parameters": [ + { + "type": "string", + "description": "Export format: csv or json (default: json)", + "name": "format", + "in": "query" + }, + { + "type": "string", + "description": "Filter by user ID", + "name": "user_id", + "in": "query" + }, + { + "type": "string", + "description": "Filter by entity type", + "name": "entity_type", + "in": "query" + }, + { + "type": "string", + "description": "Filter by entity ID", + "name": "entity_id", + "in": "query" + }, + { + "type": "string", + "description": "Filter by action", + "name": "action", + "in": "query" + }, + { + "type": "string", + "description": "Start date (RFC3339)", + "name": "start_date", + "in": "query" + }, + { + "type": "string", + "description": "End date (RFC3339)", + "name": "end_date", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/login": { + "post": { + "description": "Authenticate with username and password, returns a JWT access token and sets a refresh token cookie", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "User login", + "parameters": [ + { + "description": "Login credentials", + "name": "credentials", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.LoginResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/logout": { + "post": { + "description": "Revokes the current refresh token and blocklists the access token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Logout", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/logout-all": { + "post": { + "description": "Revokes all refresh tokens for the current user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Logout from all sessions", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/me": { + "get": { + "description": "Returns the authenticated user's information", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Get current user", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/oidc/authorize": { + "get": { + "description": "Generates PKCE parameters and state, returns the IdP authorization URL for the frontend to redirect to", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Start OIDC authorization flow", + "parameters": [ + { + "type": "string", + "description": "Frontend URL to return to after authentication", + "name": "redirect", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/oidc/callback": { + "get": { + "description": "Handles the IdP callback after user authentication. Exchanges the authorization code for tokens, provisions or updates the local user, and redirects to the frontend with a JWT.", + "produces": [ + "text/html" + ], + "tags": [ + "auth" + ], + "summary": "OIDC callback", + "parameters": [ + { + "type": "string", + "description": "Authorization code from IdP", + "name": "code", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "State parameter for CSRF validation", + "name": "state", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "CLI auth success page (HTML)", + "schema": { + "type": "string" + } + }, + "302": { + "description": "Redirect to login with error", + "schema": { + "type": "string" + } + }, + "410": { + "description": "CLI session expired (HTML)", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal error (HTML, CLI flow only)", + "schema": { + "type": "string" + } + } + } + } + }, + "/api/v1/auth/oidc/cli-auth": { + "post": { + "description": "Generates a session ID and OIDC authorization URL for CLI-based SSO login. The CLI opens the returned login_url in a browser and polls cli-token until authentication completes. Optionally accepts a loopback `redirect_uri` (http scheme, any loopback IP such as 127.0.0.1 or [::1], or hostname \"localhost\", with an explicit port) for the RFC 8252 native-app flow — the callback then 302-redirects the browser directly to that URL with tokens in the query string, no polling needed.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Start CLI SSO authentication flow", + "parameters": [ + { + "description": "Optional loopback redirect_uri", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/handlers.CLIAuthRequest" + } + } + ], + "responses": { + "200": { + "description": "session_id, login_url, expires_in", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "invalid redirect_uri", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/oidc/cli-token": { + "post": { + "description": "Returns the current status of a CLI auth session. Returns pending while waiting for the user to complete browser authentication, or completed with a JWT token once done.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Poll CLI SSO authentication status", + "parameters": [ + { + "description": "CLI token poll request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CLITokenRequest" + } + } + ], + "responses": { + "200": { + "description": "status, token (when completed), username, user_id", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "410": { + "description": "session expired or not found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/oidc/config": { + "get": { + "description": "Returns public OIDC configuration for the frontend (enabled status, provider name, local auth availability)", + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Get OIDC configuration", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + } + } + } + }, + "/api/v1/auth/refresh": { + "post": { + "description": "Issues a new access token using the refresh token cookie. Rotates the refresh token (old one invalidated, new one issued).", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Refresh access token", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.RefreshResponse" + } + }, + "401": { + "description": "Invalid, expired, or revoked refresh token", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "501": { + "description": "Refresh tokens not enabled", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/auth/register": { + "post": { + "description": "Create a new user account (admin only, or when self-registration is enabled)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "auth" + ], + "summary": "Register a new user", + "parameters": [ + { + "description": "User registration data", + "name": "user", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.RegisterRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Conflict", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/clusters": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns all registered clusters. Kubeconfig data is never included in responses. Admin/DevOps users receive full cluster details; other roles receive a summary (id, name, is_default only).", + "produces": [ + "application/json" + ], + "tags": [ + "clusters" + ], + "summary": "List all clusters", + "responses": { + "200": { + "description": "Summary (other roles)", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ClusterSummary" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Registers a new Kubernetes cluster with the provided kubeconfig data.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "clusters" + ], + "summary": "Register a new cluster", + "parameters": [ + { + "description": "Cluster registration payload", + "name": "cluster", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateClusterRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.Cluster" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Conflict", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/clusters/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns a single cluster by ID. Kubeconfig data is never included. Admin/DevOps users receive full cluster details; other roles receive a summary (id, name, is_default only).", + "produces": [ + "application/json" + ], + "tags": [ + "clusters" + ], + "summary": "Get cluster details", + "parameters": [ + { + "type": "string", + "description": "Cluster ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Summary (other roles)", + "schema": { + "$ref": "#/definitions/handlers.ClusterSummary" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Updates cluster metadata and/or kubeconfig. If kubeconfig is updated, the cached client is invalidated.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "clusters" + ], + "summary": "Update a cluster", + "parameters": [ + { + "type": "string", + "description": "Cluster ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Cluster update payload", + "name": "cluster", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.UpdateClusterRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Cluster" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Removes a cluster registration. Blocked if any stack instances reference this cluster.", + "produces": [ + "application/json" + ], + "tags": [ + "clusters" + ], + "summary": "Delete a cluster", + "parameters": [ + { + "type": "string", + "description": "Cluster ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Conflict", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/clusters/{id}/default": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Sets the specified cluster as the default cluster for deployments.", + "produces": [ + "application/json" + ], + "tags": [ + "clusters" + ], + "summary": "Set a cluster as the default", + "parameters": [ + { + "type": "string", + "description": "Cluster ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/clusters/{id}/health/nodes": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns per-node health, conditions, and capacity for a cluster.", + "produces": [ + "application/json" + ], + "tags": [ + "clusters" + ], + "summary": "Get cluster node statuses", + "parameters": [ + { + "type": "string", + "description": "Cluster ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/k8s.NodeStatus" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/clusters/{id}/health/summary": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns node count, CPU/memory totals, and namespace count for a cluster.", + "produces": [ + "application/json" + ], + "tags": [ + "clusters" + ], + "summary": "Get cluster health summary", + "parameters": [ + { + "type": "string", + "description": "Cluster ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/k8s.ClusterSummary" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/clusters/{id}/namespaces": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns all stack-* namespaces in the cluster.", + "produces": [ + "application/json" + ], + "tags": [ + "clusters" + ], + "summary": "Get cluster namespaces", + "parameters": [ + { + "type": "string", + "description": "Cluster ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/k8s.NamespaceInfo" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/clusters/{id}/quotas": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns the resource quota configuration for a cluster, or 404 if not set.", + "produces": [ + "application/json" + ], + "tags": [ + "clusters" + ], + "summary": "Get resource quota config for a cluster", + "parameters": [ + { + "type": "string", + "description": "Cluster ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ResourceQuotaConfig" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Creates or updates the resource quota configuration for a cluster. Admin only.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "clusters" + ], + "summary": "Create or update resource quota config for a cluster", + "parameters": [ + { + "type": "string", + "description": "Cluster ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Quota configuration", + "name": "quota", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.UpdateQuotaRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ResourceQuotaConfig" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Removes the resource quota configuration for a cluster. Admin only.", + "produces": [ + "application/json" + ], + "tags": [ + "clusters" + ], + "summary": "Delete resource quota config for a cluster", + "parameters": [ + { + "type": "string", + "description": "Cluster ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/clusters/{id}/shared-values": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns all shared values for the specified cluster, sorted by priority (lowest first).", + "produces": [ + "application/json" + ], + "tags": [ + "shared-values" + ], + "summary": "List shared values for a cluster", + "parameters": [ + { + "type": "string", + "description": "Cluster ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.SharedValues" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Creates a new shared values entry for the specified cluster.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "shared-values" + ], + "summary": "Create shared values for a cluster", + "parameters": [ + { + "type": "string", + "description": "Cluster ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Shared values payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.SharedValues" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.SharedValues" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/clusters/{id}/shared-values/{valueId}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Updates an existing shared values entry for the specified cluster.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "shared-values" + ], + "summary": "Update shared values", + "parameters": [ + { + "type": "string", + "description": "Cluster ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Shared values ID", + "name": "valueId", + "in": "path", + "required": true + }, + { + "description": "Shared values payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.SharedValues" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.SharedValues" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Deletes a shared values entry from the specified cluster.", + "tags": [ + "shared-values" + ], + "summary": "Delete shared values", + "parameters": [ + { + "type": "string", + "description": "Cluster ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Shared values ID", + "name": "valueId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/clusters/{id}/test": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Tests connectivity to a cluster by attempting to reach the Kubernetes API server.", + "produces": [ + "application/json" + ], + "tags": [ + "clusters" + ], + "summary": "Test cluster connectivity", + "parameters": [ + { + "type": "string", + "description": "Cluster ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/clusters/{id}/utilization": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns per-namespace resource usage for all stack namespaces in the cluster.", + "produces": [ + "application/json" + ], + "tags": [ + "clusters" + ], + "summary": "Get cluster-wide resource utilization", + "parameters": [ + { + "type": "string", + "description": "Cluster ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.ClusterUtilization" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/dashboard": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns aggregated dashboard data: cluster health, recent deployments, expiring instances, and failing instances.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "dashboard" + ], + "summary": "Get dashboard overview", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.DashboardResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/favorites": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns all favorited entities for the current user", + "produces": [ + "application/json" + ], + "tags": [ + "favorites" + ], + "summary": "List favorites for the authenticated user", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.UserFavorite" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Add an entity to the user's favorites (idempotent)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "favorites" + ], + "summary": "Add a favorite", + "parameters": [ + { + "description": "Favorite entity reference", + "name": "favorite", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.addFavoriteRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.UserFavorite" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/favorites/check": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Check whether the authenticated user has favorited a specific entity", + "produces": [ + "application/json" + ], + "tags": [ + "favorites" + ], + "summary": "Check if an entity is favorited", + "parameters": [ + { + "type": "string", + "description": "Entity type (definition, instance, template)", + "name": "entity_type", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Entity ID", + "name": "entity_id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/favorites/{entityType}/{entityId}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Remove an entity from the user's favorites", + "produces": [ + "application/json" + ], + "tags": [ + "favorites" + ], + "summary": "Remove a favorite", + "parameters": [ + { + "type": "string", + "description": "Entity type (definition, instance, template)", + "name": "entityType", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Entity ID", + "name": "entityId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "Favorite removed" + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/git/branches": { + "get": { + "description": "List branches for a given repository URL", + "produces": [ + "application/json" + ], + "tags": [ + "git" + ], + "summary": "List branches", + "parameters": [ + { + "type": "string", + "description": "Repository URL", + "name": "repo", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/gitprovider.Branch" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/git/providers": { + "get": { + "description": "Get the status of all configured Git providers", + "produces": [ + "application/json" + ], + "tags": [ + "git" + ], + "summary": "List Git providers", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/gitprovider.ProviderStatus" + } + } + } + } + } + }, + "/api/v1/git/validate-branch": { + "get": { + "description": "Check if a branch exists in the given repository", + "produces": [ + "application/json" + ], + "tags": [ + "git" + ], + "summary": "Validate a branch", + "parameters": [ + { + "type": "string", + "description": "Repository URL", + "name": "repo", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Branch name", + "name": "branch", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/items": { + "get": { + "description": "Get a list of all items", + "produces": [ + "application/json" + ], + "tags": [ + "items" + ], + "summary": "Get all items", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Item" + } + } + } + } + }, + "post": { + "description": "Create a new item with the provided information", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "items" + ], + "summary": "Create a new item", + "parameters": [ + { + "description": "Item object", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Item" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.Item" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/items/{id}": { + "get": { + "description": "Get an item by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "items" + ], + "summary": "Get an item by ID", + "parameters": [ + { + "type": "integer", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Item" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "description": "Update an item's information", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "items" + ], + "summary": "Update an item", + "parameters": [ + { + "type": "integer", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Item object", + "name": "item", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.Item" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Item" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "description": "Delete an item by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "items" + ], + "summary": "Delete an item", + "parameters": [ + { + "type": "integer", + "description": "Item ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/notifications": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List the authenticated user's notifications with optional filters and pagination", + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "List notifications", + "parameters": [ + { + "type": "boolean", + "description": "Only return unread notifications", + "name": "unread_only", + "in": "query" + }, + { + "type": "integer", + "description": "Page size (default 20, max 100)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset (default 0)", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.PaginatedNotifications" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/notifications/count": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns the unread notification count for badge display", + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "Count unread notifications", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/notifications/preferences": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Get the authenticated user's notification preferences", + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "Get notification preferences", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.NotificationPreference" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Update the authenticated user's notification preferences (array of event_type + enabled)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "Update notification preferences", + "parameters": [ + { + "description": "Preferences to update", + "name": "preferences", + "in": "body", + "required": true, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.updatePreferenceRequest" + } + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.NotificationPreference" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/notifications/read-all": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Mark all of the authenticated user's notifications as read", + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "Mark all notifications as read", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/notifications/{id}/read": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Mark a single notification as read (verifies ownership)", + "produces": [ + "application/json" + ], + "tags": [ + "notifications" + ], + "summary": "Mark notification as read", + "parameters": [ + { + "type": "string", + "description": "Notification ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/ping": { + "get": { + "description": "Ping test endpoint", + "produces": [ + "application/json" + ], + "tags": [ + "ping" + ], + "summary": "Ping test", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-definitions": { + "get": { + "description": "List stack definitions with server-side pagination", + "produces": [ + "application/json" + ], + "tags": [ + "stack-definitions" + ], + "summary": "List stack definitions", + "parameters": [ + { + "type": "string", + "description": "Filter by exact name", + "name": "name", + "in": "query" + }, + { + "minimum": 1, + "type": "integer", + "description": "Page number (default 1)", + "name": "page", + "in": "query" + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "description": "Items per page (default 25, max 100)", + "name": "pageSize", + "in": "query" + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "description": "Max items to return (default 25, max 100)", + "name": "limit", + "in": "query" + }, + { + "minimum": 0, + "type": "integer", + "description": "Number of items to skip (default 0)", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Paginated list with data, total, page, pageSize", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "description": "Create a new stack definition", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "stack-definitions" + ], + "summary": "Create a stack definition", + "parameters": [ + { + "description": "Definition object", + "name": "definition", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.StackDefinition" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.StackDefinition" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-definitions/import": { + "post": { + "description": "Import a stack definition from a portable JSON bundle, creating a new definition with fresh IDs", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "stack-definitions" + ], + "summary": "Import a stack definition", + "parameters": [ + { + "description": "Export bundle", + "name": "bundle", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.DefinitionExportBundle" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/handlers.DefinitionWithChartsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-definitions/{id}": { + "get": { + "description": "Get a stack definition by ID, including its chart configurations", + "produces": [ + "application/json" + ], + "tags": [ + "stack-definitions" + ], + "summary": "Get a stack definition", + "parameters": [ + { + "type": "string", + "description": "Definition ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.DefinitionWithChartsResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "description": "Update an existing stack definition", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "stack-definitions" + ], + "summary": "Update a stack definition", + "parameters": [ + { + "type": "string", + "description": "Definition ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Definition object", + "name": "definition", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.StackDefinition" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.StackDefinition" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "description": "Delete a stack definition if no running instances link to it", + "produces": [ + "application/json" + ], + "tags": [ + "stack-definitions" + ], + "summary": "Delete a stack definition", + "parameters": [ + { + "type": "string", + "description": "Definition ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Conflict", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-definitions/{id}/charts": { + "post": { + "description": "Add a new chart configuration to a stack definition", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "chart-configs" + ], + "summary": "Add a chart to a definition", + "parameters": [ + { + "type": "string", + "description": "Definition ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Chart config", + "name": "chart", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ChartConfig" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.ChartConfig" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-definitions/{id}/charts/{chartId}": { + "put": { + "description": "Update a chart configuration within a stack definition", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "chart-configs" + ], + "summary": "Update a chart config", + "parameters": [ + { + "type": "string", + "description": "Definition ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Chart config ID", + "name": "chartId", + "in": "path", + "required": true + }, + { + "description": "Updated chart config", + "name": "chart", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ChartConfig" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ChartConfig" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "description": "Remove a chart configuration from a stack definition", + "produces": [ + "application/json" + ], + "tags": [ + "chart-configs" + ], + "summary": "Delete a chart config", + "parameters": [ + { + "type": "string", + "description": "Definition ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Chart config ID", + "name": "chartId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-definitions/{id}/check-upgrade": { + "get": { + "description": "Check if the source template has a newer version than the definition's current version", + "produces": [ + "application/json" + ], + "tags": [ + "stack-definitions" + ], + "summary": "Check if a template upgrade is available", + "parameters": [ + { + "type": "string", + "description": "Definition ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-definitions/{id}/export": { + "get": { + "description": "Export a stack definition and its chart configs as a portable JSON bundle", + "produces": [ + "application/json" + ], + "tags": [ + "stack-definitions" + ], + "summary": "Export a stack definition", + "parameters": [ + { + "type": "string", + "description": "Definition ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.DefinitionExportBundle" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-definitions/{id}/upgrade": { + "post": { + "description": "Upgrade a definition to the latest template version, adding new charts and updating defaults", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "stack-definitions" + ], + "summary": "Apply a template upgrade to a definition", + "parameters": [ + { + "type": "string", + "description": "Definition ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.DefinitionWithChartsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Conflict", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances": { + "get": { + "description": "List stack instances with server-side pagination. Supports page/pageSize or legacy limit/offset params. Use owner=me to filter by the authenticated user. Filter precedence: owner \u003e name \u003e pagination (only the first matching filter is applied).", + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "List stack instances", + "parameters": [ + { + "type": "string", + "description": "Filter by owner (use 'me' for current user)", + "name": "owner", + "in": "query" + }, + { + "type": "string", + "description": "Filter by exact instance name", + "name": "name", + "in": "query" + }, + { + "type": "integer", + "description": "Page number (1-based, default: 1)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Results per page (default: 25, max: 100)", + "name": "pageSize", + "in": "query" + }, + { + "type": "integer", + "description": "Legacy: maximum number of results", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Legacy: number of results to skip", + "name": "offset", + "in": "query" + } + ], + "responses": { + "200": { + "description": "data: []StackInstance, total: int, page: int, pageSize: int", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/bulk/clean": { + "post": { + "description": "Clean multiple stack instances in a single request. Uninstalls Helm releases and deletes namespaces.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Bulk clean stack instances", + "parameters": [ + { + "description": "Instance IDs to clean", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.BulkOperationRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BulkOperationResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/bulk/delete": { + "post": { + "description": "Delete multiple stack instances in a single request. Processes instances sequentially.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Bulk delete stack instances", + "parameters": [ + { + "description": "Instance IDs to delete", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.BulkOperationRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BulkOperationResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/bulk/deploy": { + "post": { + "description": "Deploy multiple stack instances in a single request. Processes instances sequentially.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Bulk deploy stack instances", + "parameters": [ + { + "description": "Instance IDs to deploy", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.BulkOperationRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BulkOperationResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/bulk/stop": { + "post": { + "description": "Stop multiple stack instances in a single request. Processes instances sequentially.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Bulk stop stack instances", + "parameters": [ + { + "description": "Instance IDs to stop", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.BulkOperationRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BulkOperationResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/compare": { + "get": { + "description": "Compare the merged values of two stack instances side-by-side, per chart", + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Compare two stack instances", + "parameters": [ + { + "type": "string", + "description": "Left instance ID", + "name": "left", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Right instance ID", + "name": "right", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.CompareInstancesResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/recent": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns the 5 most recently updated stack instances owned by the current user", + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Get recent stack instances for the authenticated user", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.StackInstance" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}": { + "get": { + "description": "Get a stack instance by ID", + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Get a stack instance", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.StackInstance" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "description": "Update a stack instance (branch, name, etc.)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Update a stack instance", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Instance object", + "name": "instance", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.StackInstance" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.StackInstance" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "description": "Deletes a stack instance. If the instance has running resources (status running/stopped/error), a cleanup is initiated first — helm releases are uninstalled and the namespace is deleted before the database record is removed. Returns 204 for immediate deletion (draft instances) or 202 when async cleanup is required.", + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Delete a stack instance", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "Cleanup initiated, instance will be deleted after resources are removed", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "204": { + "description": "No Content — instance deleted immediately (no resources to clean)" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Instance is in a transient state (deploying/stopping/cleaning)", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Deploy manager not configured", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/actions/{name}": { + "post": { + "description": "Dispatches to the action subscriber webhook and wraps its response in an envelope containing action, instance_id, status_code, and result fields. The subscriber's JSON body is nested under the result key. Returns 200 even for non-2xx subscriber responses — check status_code to distinguish.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Invoke a registered action against a stack instance", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Action name", + "name": "name", + "in": "path", + "required": true + }, + { + "description": "Optional parameters passed through to the subscriber", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/handlers.invokeActionRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Instance not found or action not registered", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "502": { + "description": "Subscriber unreachable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Action registry not configured", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/branches": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "List all per-chart branch overrides for a stack instance", + "produces": [ + "application/json" + ], + "tags": [ + "branch-overrides" + ], + "summary": "List branch overrides for an instance", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ChartBranchOverride" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/branches/{chartId}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Upsert a per-chart branch override for a specific chart in a stack instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "branch-overrides" + ], + "summary": "Set or update branch override for a chart", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Chart config ID", + "name": "chartId", + "in": "path", + "required": true + }, + { + "description": "Branch override", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.setBranchOverrideRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ChartBranchOverride" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Remove the per-chart branch override for a specific chart in a stack instance", + "produces": [ + "application/json" + ], + "tags": [ + "branch-overrides" + ], + "summary": "Delete branch override for a chart", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Chart config ID", + "name": "chartId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/clean": { + "post": { + "description": "Uninstall all Helm releases and delete the K8s namespace, returning the instance to draft status", + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Clean a stack instance namespace", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "Namespace cleanup initiated", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Invalid status for clean", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Deployment service not configured", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/clone": { + "post": { + "description": "Create a new stack instance as a copy of an existing one", + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Clone a stack instance", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.StackInstance" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Namespace already exists", + "schema": { + "$ref": "#/definitions/handlers.NamespaceConflictResponse" + } + } + } + } + }, + "/api/v1/stack-instances/{id}/deploy": { + "post": { + "description": "Trigger Helm deployment for a stack instance", + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Deploy a stack instance", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "Deployment started", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Already deploying", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/deploy-log": { + "get": { + "description": "Get deployment log history for a stack instance. Supports cursor-based pagination for efficient large dataset traversal.", + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Get deployment logs", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Page size (default 50)", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Offset for traditional pagination (default 0)", + "name": "offset", + "in": "query" + }, + { + "type": "string", + "description": "Cursor from previous page for cursor-based pagination (overrides offset)", + "name": "cursor", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.DeploymentLogResult" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/deploy-log/{logId}/values": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns the merged Helm values that were used for a specific deployment", + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Get values snapshot for a deployment log entry", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Deployment Log ID", + "name": "logId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/deploy-preview": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Compare pending merged values against last-deployed values per chart", + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Preview deployment changes", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.DeployPreviewResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/extend": { + "post": { + "description": "Extend the expiry time of a stack instance. Uses provided ttl_minutes or the instance's existing TTLMinutes.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Extend instance TTL", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Optional TTL override", + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/handlers.extendTTLRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.StackInstance" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/overrides": { + "get": { + "description": "List all value overrides for a stack instance", + "produces": [ + "application/json" + ], + "tags": [ + "value-overrides" + ], + "summary": "Get overrides for an instance", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ValueOverride" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/overrides/{chartId}": { + "put": { + "description": "Upsert value overrides for a specific chart in a stack instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "value-overrides" + ], + "summary": "Set or update override for a chart", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Chart config ID", + "name": "chartId", + "in": "path", + "required": true + }, + { + "description": "Override values", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.ValueOverride" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.ValueOverride" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/pods": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns detailed pod health including container states, conditions, and recent events", + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Get instance pod status", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/k8s.NamespaceStatus" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "504": { + "description": "Gateway Timeout", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/quota-overrides": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Retrieve the per-instance resource quota override for a stack instance", + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Get quota override for an instance", + "parameters": [ + { + "type": "string", + "description": "Stack Instance ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.InstanceQuotaOverride" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Upsert the per-instance resource quota override for a stack instance", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Set or update quota override for an instance", + "parameters": [ + { + "type": "string", + "description": "Stack Instance ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Quota override values", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.setQuotaOverrideRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.InstanceQuotaOverride" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Remove the per-instance resource quota override for a stack instance", + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Delete quota override for an instance", + "parameters": [ + { + "type": "string", + "description": "Stack Instance ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/rollback": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Rollback all Helm releases in a stack instance to their previous revision", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Rollback a stack instance", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Optional: {\\", + "name": "body", + "in": "body", + "schema": { + "type": "object" + } + } + ], + "responses": { + "202": { + "description": "Accepted", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Conflict", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/status": { + "get": { + "description": "Get detailed Kubernetes resource status for a stack instance", + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Get instance K8s status", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/k8s.NamespaceStatus" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/stop": { + "post": { + "description": "Trigger Helm uninstall for a stack instance", + "produces": [ + "application/json" + ], + "tags": [ + "stack-instances" + ], + "summary": "Stop a stack instance", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "202": { + "description": "Stop initiated", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Not running", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/values": { + "get": { + "description": "Generate and export merged values for all charts as a zip archive", + "produces": [ + "application/zip" + ], + "tags": [ + "stack-instances" + ], + "summary": "Export all chart values", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "ZIP archive", + "schema": { + "type": "file" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/stack-instances/{id}/values/{chartId}": { + "get": { + "description": "Generate and export merged values.yaml for a specific chart", + "produces": [ + "application/x-yaml" + ], + "tags": [ + "stack-instances" + ], + "summary": "Export chart values", + "parameters": [ + { + "type": "string", + "description": "Instance ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Chart config ID", + "name": "chartId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "YAML content", + "schema": { + "type": "string" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/templates": { + "get": { + "description": "List published templates for regular users, all templates for devops/admin. Includes definition_count and owner_username. Supports server-side pagination.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "List stack templates", + "parameters": [ + { + "minimum": 1, + "type": "integer", + "description": "Page number (default 1)", + "name": "page", + "in": "query" + }, + { + "maximum": 100, + "minimum": 1, + "type": "integer", + "description": "Items per page (default 25, max 100)", + "name": "pageSize", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Paginated list with data, total, page, pageSize", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "description": "Create a new stack template (devops/admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "Create a stack template", + "parameters": [ + { + "description": "Template object", + "name": "template", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.StackTemplate" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.StackTemplate" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/templates/bulk/delete": { + "post": { + "description": "Delete multiple stack templates in a single request. Only unpublished templates with no linked definitions can be deleted.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "Bulk delete stack templates", + "parameters": [ + { + "description": "Template IDs to delete", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.BulkTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BulkTemplateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/templates/bulk/publish": { + "post": { + "description": "Publish multiple stack templates in a single request, making them visible to all users.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "Bulk publish stack templates", + "parameters": [ + { + "description": "Template IDs to publish", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.BulkTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BulkTemplateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/templates/bulk/unpublish": { + "post": { + "description": "Unpublish multiple stack templates in a single request, hiding them from regular users.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "Bulk unpublish stack templates", + "parameters": [ + { + "description": "Template IDs to unpublish", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.BulkTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.BulkTemplateResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/templates/{id}": { + "get": { + "description": "Get a stack template by ID, including its chart configurations", + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "Get a stack template", + "parameters": [ + { + "type": "string", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.TemplateDetailResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "put": { + "description": "Update an existing stack template (devops/admin only)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "Update a stack template", + "parameters": [ + { + "type": "string", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Template object", + "name": "template", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.StackTemplate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.StackTemplate" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "description": "Delete a stack template if no definitions link to it (devops/admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "Delete a stack template", + "parameters": [ + { + "type": "string", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "409": { + "description": "Conflict", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/templates/{id}/charts": { + "post": { + "description": "Add a new chart configuration to a stack template", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "template-charts" + ], + "summary": "Add a chart to a template", + "parameters": [ + { + "type": "string", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Chart config", + "name": "chart", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.TemplateChartConfig" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.TemplateChartConfig" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/templates/{id}/charts/{chartId}": { + "put": { + "description": "Update a chart configuration within a stack template", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "template-charts" + ], + "summary": "Update a template chart", + "parameters": [ + { + "type": "string", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Chart config ID", + "name": "chartId", + "in": "path", + "required": true + }, + { + "description": "Updated chart config", + "name": "chart", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.TemplateChartConfig" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.TemplateChartConfig" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "delete": { + "description": "Remove a chart configuration from a stack template", + "produces": [ + "application/json" + ], + "tags": [ + "template-charts" + ], + "summary": "Delete a template chart", + "parameters": [ + { + "type": "string", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Chart config ID", + "name": "chartId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/templates/{id}/clone": { + "post": { + "description": "Create a new draft template that is a copy of the source (devops/admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "Clone a stack template", + "parameters": [ + { + "type": "string", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/models.StackTemplate" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/templates/{id}/instantiate": { + "post": { + "description": "Create a StackDefinition and ChartConfigs from a template", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "Instantiate a template", + "parameters": [ + { + "type": "string", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Definition overrides (name, description)", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.StackDefinition" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/handlers.DefinitionWithChartsResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/templates/{id}/publish": { + "post": { + "description": "Make a template visible to all users (devops/admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "Publish a stack template", + "parameters": [ + { + "type": "string", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.StackTemplate" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/templates/{id}/quick-deploy": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Instantiate a template, create an instance, set branch overrides, and trigger deployment in a single call", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "Quick deploy from a template", + "parameters": [ + { + "type": "string", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Quick deploy options", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.quickDeployRequest" + } + } + ], + "responses": { + "202": { + "description": "Accepted", + "schema": { + "$ref": "#/definitions/handlers.quickDeployResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/templates/{id}/unpublish": { + "post": { + "description": "Hide a template from regular users (devops/admin only)", + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "Unpublish a stack template", + "parameters": [ + { + "type": "string", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.StackTemplate" + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/templates/{id}/versions": { + "get": { + "description": "List all version snapshots for a template, ordered newest first", + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "List template versions", + "parameters": [ + { + "type": "string", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.TemplateVersion" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/templates/{id}/versions/diff": { + "get": { + "description": "Compare two template version snapshots side by side", + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "Compare two template versions", + "parameters": [ + { + "type": "string", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Left version ID", + "name": "left", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Right version ID", + "name": "right", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": true + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/templates/{id}/versions/{versionId}": { + "get": { + "description": "Get a specific template version with its parsed snapshot", + "produces": [ + "application/json" + ], + "tags": [ + "templates" + ], + "summary": "Get a template version", + "parameters": [ + { + "type": "string", + "description": "Template ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Version ID", + "name": "versionId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handlers.versionDetailResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns all registered users. Admin only. PasswordHash is never included.", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "List all users", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.User" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/users/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Permanently deletes a user account. Admin only. Cannot delete own account.", + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Delete a user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/users/{id}/api-keys": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Returns all API keys for the given user. Admin or own user only.", + "produces": [ + "application/json" + ], + "tags": [ + "api-keys" + ], + "summary": "List API keys for a user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.APIKey" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Generates a new API key. An expiration is required: set expires_at (YYYY-MM-DD or RFC3339) or expires_in_days (positive int), but not both. If API_KEY_MAX_LIFETIME_DAYS is configured, the expiry must not exceed the limit. The raw key is returned once in raw_key and cannot be retrieved again.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "api-keys" + ], + "summary": "Create an API key for a user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "API key details", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.CreateAPIKeyRequest" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/handlers.CreateAPIKeyResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/users/{id}/api-keys/{keyId}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Deletes the specified API key. Admin or own user only.", + "produces": [ + "application/json" + ], + "tags": [ + "api-keys" + ], + "summary": "Delete an API key", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "API Key ID", + "name": "keyId", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/users/{id}/disable": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Disables a user account. All API keys for this user immediately stop working. Admin only.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Disable a user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/users/{id}/enable": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Re-enables a previously disabled user account. Admin only.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Enable a user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/api/v1/users/{id}/password": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Resets the password for a local/service account user. Admin only.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "users" + ], + "summary": "Reset user password", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "New password", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.ResetPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "404": { + "description": "Not Found", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/health": { + "get": { + "description": "Get API health status", + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Health Check", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + }, + "/health/live": { + "get": { + "description": "Get API liveness status", + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Liveness Check", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/health.HealthStatus" + } + } + } + } + }, + "/health/ready": { + "get": { + "description": "Get API readiness status", + "produces": [ + "application/json" + ], + "tags": [ + "health" + ], + "summary": "Readiness Check", + "parameters": [ + { + "type": "boolean", + "description": "Include per-check details", + "name": "verbose", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/health.HealthStatus" + } + }, + "503": { + "description": "Service Unavailable", + "schema": { + "$ref": "#/definitions/health.HealthStatus" + } + } + } + } + }, + "/ws": { + "get": { + "description": "Upgrades the HTTP connection to a WebSocket for real-time events. Requires a valid JWT token via ?token= query parameter or Authorization: Bearer header.", + "tags": [ + "websocket" + ], + "summary": "Open a WebSocket connection", + "parameters": [ + { + "type": "string", + "description": "JWT authentication token", + "name": "token", + "in": "query" + } + ], + "responses": { + "101": { + "description": "Switching Protocols" + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "403": { + "description": "Forbidden", + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + }, + "definitions": { + "gitprovider.Branch": { + "type": "object", + "properties": { + "is_default": { + "type": "boolean" + }, + "name": { + "type": "string" + } + } + }, + "gitprovider.ProviderStatus": { + "type": "object", + "properties": { + "available": { + "type": "boolean" + }, + "type": { + "type": "string" + } + } + }, + "handlers.BulkOperationRequest": { + "type": "object", + "required": [ + "instance_ids" + ], + "properties": { + "instance_ids": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "handlers.BulkOperationResponse": { + "type": "object", + "properties": { + "failed": { + "type": "integer" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BulkOperationResultItem" + } + }, + "succeeded": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "handlers.BulkOperationResultItem": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "instance_id": { + "type": "string" + }, + "instance_name": { + "type": "string" + }, + "log_id": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "handlers.BulkTemplateRequest": { + "type": "object", + "required": [ + "template_ids" + ], + "properties": { + "template_ids": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "handlers.BulkTemplateResponse": { + "type": "object", + "properties": { + "failed": { + "type": "integer" + }, + "results": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.BulkTemplateResultItem" + } + }, + "succeeded": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "handlers.BulkTemplateResultItem": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "status": { + "description": "\"success\" or \"error\"", + "type": "string" + }, + "template_id": { + "type": "string" + }, + "template_name": { + "type": "string" + } + } + }, + "handlers.CLIAuthRequest": { + "type": "object", + "properties": { + "redirect_uri": { + "type": "string" + } + } + }, + "handlers.CLITokenRequest": { + "type": "object", + "required": [ + "session_id" + ], + "properties": { + "session_id": { + "type": "string" + } + } + }, + "handlers.ChartConfigExportData": { + "type": "object", + "properties": { + "build_pipeline_id": { + "type": "string" + }, + "chart_name": { + "type": "string" + }, + "chart_path": { + "type": "string" + }, + "chart_version": { + "type": "string" + }, + "default_values": { + "type": "string" + }, + "deploy_order": { + "type": "integer" + }, + "repository_url": { + "type": "string" + }, + "source_repo_url": { + "type": "string" + } + } + }, + "handlers.ChartDeployPreview": { + "type": "object", + "properties": { + "chart_name": { + "type": "string" + }, + "has_changes": { + "type": "boolean" + }, + "pending_values": { + "type": "string" + }, + "previous_values": { + "type": "string" + } + } + }, + "handlers.ClusterSummary": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "string" + } + } + }, + "handlers.ClusterUtilization": { + "type": "object", + "properties": { + "cluster_id": { + "type": "string" + }, + "namespaces": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.NamespaceResourceUsage" + } + } + } + }, + "handlers.CompareChartDiff": { + "type": "object", + "properties": { + "chart_name": { + "type": "string" + }, + "has_differences": { + "type": "boolean" + }, + "left_values": { + "type": "string" + }, + "right_values": { + "type": "string" + } + } + }, + "handlers.CompareInstanceSummary": { + "type": "object", + "properties": { + "branch": { + "type": "string" + }, + "definition_name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner": { + "type": "string" + } + } + }, + "handlers.CompareInstancesResponse": { + "type": "object", + "properties": { + "charts": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.CompareChartDiff" + } + }, + "left": { + "$ref": "#/definitions/handlers.CompareInstanceSummary" + }, + "right": { + "$ref": "#/definitions/handlers.CompareInstanceSummary" + } + } + }, + "handlers.CreateAPIKeyRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "expires_at": { + "type": "string" + }, + "expires_in_days": { + "type": "integer" + }, + "name": { + "type": "string" + } + } + }, + "handlers.CreateAPIKeyResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "expires_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prefix": { + "type": "string" + }, + "raw_key": { + "description": "sk_\u003chex\u003e — shown once, store securely", + "type": "string" + } + } + }, + "handlers.CreateClusterRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "api_server_url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "image_pull_secret_name": { + "type": "string" + }, + "is_default": { + "type": "boolean" + }, + "kubeconfig_data": { + "type": "string" + }, + "kubeconfig_path": { + "type": "string" + }, + "max_instances_per_user": { + "type": "integer" + }, + "max_namespaces": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "region": { + "type": "string" + }, + "registry_password": { + "type": "string" + }, + "registry_url": { + "type": "string" + }, + "registry_username": { + "type": "string" + }, + "use_in_cluster": { + "type": "boolean" + } + } + }, + "handlers.DashboardCluster": { + "type": "object", + "properties": { + "allocatable_cpu": { + "type": "string" + }, + "allocatable_memory": { + "type": "string" + }, + "health_status": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace_count": { + "type": "integer" + }, + "node_count": { + "type": "integer" + }, + "ready_node_count": { + "type": "integer" + }, + "total_cpu": { + "type": "string" + }, + "total_memory": { + "type": "string" + } + } + }, + "handlers.DashboardDeployment": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "completed_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "instance_name": { + "type": "string" + }, + "owner_username": { + "type": "string" + }, + "stack_instance_id": { + "type": "string" + }, + "started_at": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "handlers.DashboardExpiring": { + "type": "object", + "properties": { + "cluster_id": { + "type": "string" + }, + "expires_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "status": { + "type": "string" + }, + "ttl_minutes": { + "type": "integer" + } + } + }, + "handlers.DashboardFailing": { + "type": "object", + "properties": { + "cluster_id": { + "type": "string" + }, + "error_message": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "status": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "handlers.DashboardResponse": { + "type": "object", + "properties": { + "clusters": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.DashboardCluster" + } + }, + "expiring_soon": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.DashboardExpiring" + } + }, + "failing_instances": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.DashboardFailing" + } + }, + "recent_deployments": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.DashboardDeployment" + } + } + } + }, + "handlers.DefinitionExportBundle": { + "type": "object", + "properties": { + "charts": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ChartConfigExportData" + } + }, + "definition": { + "$ref": "#/definitions/handlers.DefinitionExportData" + }, + "exported_at": { + "type": "string" + }, + "schema_version": { + "type": "string" + } + } + }, + "handlers.DefinitionExportData": { + "type": "object", + "properties": { + "default_branch": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "handlers.DefinitionWithChartsResponse": { + "type": "object", + "properties": { + "charts": { + "type": "array", + "items": { + "$ref": "#/definitions/models.ChartConfig" + } + }, + "created_at": { + "type": "string" + }, + "default_branch": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "source_template_id": { + "type": "string" + }, + "source_template_version": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "handlers.DeployPreviewResponse": { + "type": "object", + "properties": { + "charts": { + "type": "array", + "items": { + "$ref": "#/definitions/handlers.ChartDeployPreview" + } + }, + "instance_id": { + "type": "string" + }, + "instance_name": { + "type": "string" + } + } + }, + "handlers.LoginRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "handlers.LoginResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + }, + "user": { + "$ref": "#/definitions/models.User" + } + } + }, + "handlers.NamespaceConflictResponse": { + "type": "object", + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "suggestions": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "handlers.NamespaceResourceUsage": { + "type": "object", + "properties": { + "cpu_limit": { + "type": "string" + }, + "cpu_used": { + "type": "string" + }, + "memory_limit": { + "type": "string" + }, + "memory_used": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "pod_count": { + "type": "integer" + }, + "pod_limit": { + "type": "integer" + } + } + }, + "handlers.OrphanedNamespaceResponse": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "helm_releases": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "phase": { + "type": "string" + }, + "resource_counts": { + "$ref": "#/definitions/k8s.ResourceCounts" + } + } + }, + "handlers.OverviewStats": { + "type": "object", + "properties": { + "running_instances": { + "type": "integer" + }, + "total_definitions": { + "type": "integer" + }, + "total_deploys": { + "type": "integer" + }, + "total_instances": { + "type": "integer" + }, + "total_templates": { + "type": "integer" + }, + "total_users": { + "type": "integer" + } + } + }, + "handlers.RefreshResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "handlers.RegisterRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "display_name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "role": { + "type": "string" + }, + "service_account": { + "type": "boolean" + }, + "username": { + "type": "string" + } + } + }, + "handlers.ResetPasswordRequest": { + "type": "object", + "required": [ + "password" + ], + "properties": { + "password": { + "type": "string" + } + } + }, + "handlers.TemplateDetailResponse": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "charts": { + "type": "array", + "items": { + "$ref": "#/definitions/models.TemplateChartConfig" + } + }, + "created_at": { + "type": "string" + }, + "default_branch": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_published": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "handlers.TemplateStats": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "definition_count": { + "type": "integer" + }, + "deploy_count": { + "type": "integer" + }, + "error_count": { + "type": "integer" + }, + "instance_count": { + "type": "integer" + }, + "is_published": { + "type": "boolean" + }, + "success_count": { + "type": "integer" + }, + "success_rate": { + "type": "number" + }, + "template_id": { + "type": "string" + }, + "template_name": { + "type": "string" + } + } + }, + "handlers.UpdateClusterRequest": { + "type": "object", + "properties": { + "api_server_url": { + "type": "string" + }, + "description": { + "type": "string" + }, + "image_pull_secret_name": { + "type": "string" + }, + "is_default": { + "type": "boolean" + }, + "kubeconfig_data": { + "type": "string" + }, + "kubeconfig_path": { + "type": "string" + }, + "max_instances_per_user": { + "type": "integer" + }, + "max_namespaces": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "region": { + "type": "string" + }, + "registry_password": { + "type": "string" + }, + "registry_url": { + "type": "string" + }, + "registry_username": { + "type": "string" + }, + "use_in_cluster": { + "type": "boolean" + } + } + }, + "handlers.UpdateQuotaRequest": { + "type": "object", + "properties": { + "cpu_limit": { + "type": "string" + }, + "cpu_request": { + "type": "string" + }, + "memory_limit": { + "type": "string" + }, + "memory_request": { + "type": "string" + }, + "pod_limit": { + "type": "integer" + }, + "storage_limit": { + "type": "string" + } + } + }, + "handlers.UserStats": { + "type": "object", + "properties": { + "deploy_count": { + "type": "integer" + }, + "instance_count": { + "type": "integer" + }, + "last_active": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "handlers.addFavoriteRequest": { + "type": "object", + "properties": { + "entity_id": { + "type": "string" + }, + "entity_type": { + "type": "string" + } + } + }, + "handlers.createChannelRequest": { + "type": "object", + "required": [ + "name", + "webhook_url" + ], + "properties": { + "enabled": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "webhook_url": { + "type": "string" + } + } + }, + "handlers.extendTTLRequest": { + "type": "object", + "properties": { + "ttl_minutes": { + "type": "integer" + } + } + }, + "handlers.invokeActionRequest": { + "type": "object", + "properties": { + "parameters": { + "type": "object", + "additionalProperties": {} + } + } + }, + "handlers.notificationChannelWithCount": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "subscription_count": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "webhook_url": { + "type": "string" + } + } + }, + "handlers.quickDeployRequest": { + "type": "object", + "properties": { + "branch": { + "type": "string" + }, + "branch_overrides": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "cluster_id": { + "type": "string" + }, + "instance_description": { + "type": "string" + }, + "instance_name": { + "type": "string" + }, + "ttl_minutes": { + "type": "integer" + } + } + }, + "handlers.quickDeployResponse": { + "type": "object", + "properties": { + "definition": { + "$ref": "#/definitions/models.StackDefinition" + }, + "instance": { + "$ref": "#/definitions/models.StackInstance" + }, + "log_id": { + "type": "string" + } + } + }, + "handlers.setBranchOverrideRequest": { + "type": "object", + "properties": { + "branch": { + "type": "string", + "example": "feature/my-branch" + } + } + }, + "handlers.setQuotaOverrideRequest": { + "type": "object", + "properties": { + "cpu_limit": { + "type": "string", + "example": "2000m" + }, + "cpu_request": { + "type": "string", + "example": "500m" + }, + "memory_limit": { + "type": "string", + "example": "1Gi" + }, + "memory_request": { + "type": "string", + "example": "256Mi" + }, + "pod_limit": { + "type": "integer", + "example": 20 + }, + "storage_limit": { + "type": "string", + "example": "10Gi" + } + } + }, + "handlers.updateChannelRequest": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "webhook_url": { + "type": "string" + } + } + }, + "handlers.updatePreferenceRequest": { + "type": "object", + "properties": { + "channel": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "event_type": { + "type": "string" + } + } + }, + "handlers.updateSubscriptionsRequest": { + "type": "object", + "properties": { + "event_types": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "handlers.versionDetailResponse": { + "type": "object", + "properties": { + "change_summary": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "id": { + "type": "string" + }, + "snapshot": { + "$ref": "#/definitions/models.TemplateSnapshot" + }, + "template_id": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "health.CheckStatus": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "health.HealthStatus": { + "type": "object", + "properties": { + "checks": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/health.CheckStatus" + } + }, + "status": { + "type": "string" + }, + "uptime": { + "type": "string" + } + } + }, + "k8s.ChartStatus": { + "type": "object", + "properties": { + "chart_name": { + "type": "string" + }, + "deployments": { + "type": "array", + "items": { + "$ref": "#/definitions/k8s.DeploymentInfo" + } + }, + "pods": { + "type": "array", + "items": { + "$ref": "#/definitions/k8s.PodInfo" + } + }, + "release_name": { + "type": "string" + }, + "services": { + "type": "array", + "items": { + "$ref": "#/definitions/k8s.ServiceInfo" + } + }, + "status": { + "type": "string" + } + } + }, + "k8s.ClusterSummary": { + "type": "object", + "properties": { + "allocatable_cpu": { + "type": "string" + }, + "allocatable_memory": { + "type": "string" + }, + "namespace_count": { + "type": "integer" + }, + "node_count": { + "type": "integer" + }, + "ready_node_count": { + "type": "integer" + }, + "total_cpu": { + "type": "string" + }, + "total_memory": { + "type": "string" + } + } + }, + "k8s.ContainerStateInfo": { + "type": "object", + "properties": { + "exit_code": { + "type": "integer" + }, + "image": { + "type": "string" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "ready": { + "type": "boolean" + }, + "reason": { + "type": "string" + }, + "restart_count": { + "type": "integer" + }, + "state": { + "description": "\"running\", \"waiting\", \"terminated\", \"unknown\"", + "type": "string" + } + } + }, + "k8s.DeploymentInfo": { + "type": "object", + "properties": { + "available": { + "type": "boolean" + }, + "desired_replicas": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "ready_replicas": { + "type": "integer" + }, + "updated_replicas": { + "type": "integer" + } + } + }, + "k8s.IngressInfo": { + "type": "object", + "properties": { + "host": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "tls": { + "type": "boolean" + }, + "url": { + "type": "string" + } + } + }, + "k8s.NamespaceInfo": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "name": { + "type": "string" + }, + "phase": { + "type": "string" + } + } + }, + "k8s.NamespaceStatus": { + "type": "object", + "properties": { + "charts": { + "type": "array", + "items": { + "$ref": "#/definitions/k8s.ChartStatus" + } + }, + "events": { + "type": "array", + "items": { + "$ref": "#/definitions/k8s.PodEvent" + } + }, + "ingresses": { + "type": "array", + "items": { + "$ref": "#/definitions/k8s.IngressInfo" + } + }, + "last_checked": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "k8s.NodeCondition": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "status": { + "description": "\"True\", \"False\", \"Unknown\"", + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "k8s.NodeStatus": { + "type": "object", + "properties": { + "allocatable": { + "$ref": "#/definitions/k8s.ResourceQuantity" + }, + "capacity": { + "$ref": "#/definitions/k8s.ResourceQuantity" + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/definitions/k8s.NodeCondition" + } + }, + "name": { + "type": "string" + }, + "pod_count": { + "type": "integer" + }, + "status": { + "description": "\"Ready\" or \"NotReady\"", + "type": "string" + } + } + }, + "k8s.PodConditionInfo": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "status": { + "description": "\"True\", \"False\", \"Unknown\"", + "type": "string" + }, + "type": { + "description": "\"Ready\", \"ContainersReady\", \"Initialized\", \"PodScheduled\"", + "type": "string" + } + } + }, + "k8s.PodEvent": { + "type": "object", + "properties": { + "count": { + "type": "integer" + }, + "first_seen": { + "type": "string" + }, + "last_seen": { + "type": "string" + }, + "message": { + "type": "string" + }, + "object": { + "description": "\"Pod/my-pod-xyz\"", + "type": "string" + }, + "reason": { + "type": "string" + }, + "type": { + "description": "\"Normal\", \"Warning\"", + "type": "string" + } + } + }, + "k8s.PodInfo": { + "type": "object", + "properties": { + "conditions": { + "type": "array", + "items": { + "$ref": "#/definitions/k8s.PodConditionInfo" + } + }, + "container_states": { + "type": "array", + "items": { + "$ref": "#/definitions/k8s.ContainerStateInfo" + } + }, + "image": { + "type": "string" + }, + "name": { + "type": "string" + }, + "node_name": { + "type": "string" + }, + "phase": { + "type": "string" + }, + "ready": { + "type": "boolean" + }, + "restart_count": { + "type": "integer" + }, + "start_time": { + "type": "string" + } + } + }, + "k8s.ResourceCounts": { + "type": "object", + "properties": { + "deployments": { + "type": "integer" + }, + "pods": { + "type": "integer" + }, + "services": { + "type": "integer" + } + } + }, + "k8s.ResourceQuantity": { + "type": "object", + "properties": { + "cpu": { + "type": "string" + }, + "memory": { + "type": "string" + }, + "pods": { + "type": "string" + } + } + }, + "k8s.ServiceInfo": { + "type": "object", + "properties": { + "cluster_ip": { + "type": "string" + }, + "external_ip": { + "type": "string" + }, + "ingress_hosts": { + "type": "array", + "items": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "node_ports": { + "type": "array", + "items": { + "type": "integer" + } + }, + "ports": { + "type": "array", + "items": { + "type": "string" + } + }, + "type": { + "type": "string" + } + } + }, + "models.APIKey": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "expires_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "last_used_at": { + "type": "string" + }, + "name": { + "type": "string" + }, + "prefix": { + "description": "first 16 chars of raw key for display", + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "models.AuditLog": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "details": { + "type": "string" + }, + "entity_id": { + "type": "string" + }, + "entity_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "timestamp": { + "type": "string" + }, + "user_id": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "models.ChartBranchOverride": { + "type": "object", + "properties": { + "branch": { + "type": "string" + }, + "chart_config_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "stack_instance_id": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "models.ChartConfig": { + "type": "object", + "properties": { + "build_pipeline_id": { + "description": "CI pipeline ID to trigger for image builds", + "type": "string" + }, + "chart_name": { + "type": "string" + }, + "chart_path": { + "type": "string" + }, + "chart_version": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "default_values": { + "type": "string" + }, + "deploy_order": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "repository_url": { + "type": "string" + }, + "source_repo_url": { + "type": "string" + }, + "stack_definition_id": { + "type": "string" + } + } + }, + "models.CleanupPolicy": { + "type": "object", + "properties": { + "action": { + "description": "\"stop\", \"clean\", \"delete\"", + "type": "string" + }, + "cluster_id": { + "description": "or \"all\" for all clusters", + "type": "string" + }, + "condition": { + "description": "e.g. \"idle_days:7\", \"status:stopped,age_days:14\", \"ttl_expired\"", + "type": "string" + }, + "created_at": { + "type": "string" + }, + "dry_run": { + "description": "If true, only report matches without acting", + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "last_run_at": { + "type": "string" + }, + "name": { + "type": "string" + }, + "schedule": { + "description": "Cron expression, e.g. \"0 2 * * *\"", + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "models.Cluster": { + "type": "object", + "properties": { + "api_server_url": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "health_status": { + "type": "string" + }, + "id": { + "type": "string" + }, + "image_pull_secret_name": { + "type": "string" + }, + "is_default": { + "type": "boolean" + }, + "max_instances_per_user": { + "description": "0 = unlimited", + "type": "integer" + }, + "max_namespaces": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "region": { + "type": "string" + }, + "registry_url": { + "description": "Registry fields for automatic image pull secret provisioning.\nWhen RegistryURL is non-empty, a docker-registry secret is created/refreshed\nin each stack namespace before chart installs.", + "type": "string" + }, + "registry_username": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "use_in_cluster": { + "type": "boolean" + } + } + }, + "models.DeploymentLog": { + "type": "object", + "properties": { + "action": { + "description": "\"deploy\", \"stop\", \"clean\", \"rollback\"", + "type": "string" + }, + "completed_at": { + "type": "string" + }, + "error_message": { + "type": "string" + }, + "id": { + "type": "string" + }, + "output": { + "type": "string" + }, + "stack_instance_id": { + "type": "string" + }, + "started_at": { + "type": "string" + }, + "status": { + "description": "\"running\", \"success\", \"error\"", + "type": "string" + }, + "target_log_id": { + "type": "string" + }, + "values_snapshot": { + "type": "string" + } + } + }, + "models.DeploymentLogResult": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.DeploymentLog" + } + }, + "next_cursor": { + "type": "string" + }, + "total": { + "type": "integer" + } + } + }, + "models.InstanceQuotaOverride": { + "type": "object", + "properties": { + "cpu_limit": { + "type": "string" + }, + "cpu_request": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "memory_limit": { + "type": "string" + }, + "memory_request": { + "type": "string" + }, + "pod_limit": { + "type": "integer" + }, + "stack_instance_id": { + "type": "string" + }, + "storage_limit": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "models.Item": { + "type": "object", + "properties": { + "created_at": { + "type": "string", + "example": "2025-06-02T10:00:00Z" + }, + "deleted_at": { + "type": "string", + "format": "date-time" + }, + "id": { + "type": "integer", + "example": 1 + }, + "name": { + "type": "string" + }, + "price": { + "type": "number" + }, + "updated_at": { + "type": "string", + "example": "2025-06-02T10:00:00Z" + }, + "version": { + "description": "For optimistic locking (1 = initial; 0 = not provided)", + "type": "integer" + } + } + }, + "models.Notification": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "entity_id": { + "type": "string" + }, + "entity_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_read": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "models.NotificationChannel": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "webhook_url": { + "type": "string" + } + } + }, + "models.NotificationPreference": { + "type": "object", + "properties": { + "channel": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "event_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "models.PaginatedAuditLogs": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/models.AuditLog" + } + }, + "limit": { + "type": "integer" + }, + "next_cursor": { + "type": "string" + }, + "offset": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "models.PaginatedNotifications": { + "type": "object", + "properties": { + "notifications": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Notification" + } + }, + "total": { + "type": "integer" + }, + "unread_count": { + "type": "integer" + } + } + }, + "models.ResourceQuotaConfig": { + "type": "object", + "properties": { + "cluster_id": { + "type": "string" + }, + "cpu_limit": { + "type": "string" + }, + "cpu_request": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "memory_limit": { + "type": "string" + }, + "memory_request": { + "type": "string" + }, + "pod_limit": { + "type": "integer" + }, + "storage_limit": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "models.SharedValues": { + "type": "object", + "properties": { + "cluster_id": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "priority": { + "description": "Lower = applied first", + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "values": { + "description": "YAML content", + "type": "string" + } + } + }, + "models.StackDefinition": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "default_branch": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "source_template_id": { + "type": "string" + }, + "source_template_version": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "models.StackInstance": { + "type": "object", + "properties": { + "branch": { + "type": "string" + }, + "cluster_id": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "error_message": { + "type": "string" + }, + "expires_at": { + "type": "string" + }, + "id": { + "type": "string" + }, + "last_deployed_at": { + "type": "string" + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "stack_definition_id": { + "type": "string" + }, + "status": { + "type": "string" + }, + "ttl_minutes": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } + } + }, + "models.StackTemplate": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "default_branch": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "string" + }, + "is_published": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "models.TemplateChartConfig": { + "type": "object", + "properties": { + "build_pipeline_id": { + "description": "CI pipeline ID to trigger for image builds", + "type": "string" + }, + "chart_name": { + "type": "string" + }, + "chart_path": { + "type": "string" + }, + "chart_version": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "default_values": { + "type": "string" + }, + "deploy_order": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "locked_values": { + "type": "string" + }, + "repository_url": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "source_repo_url": { + "type": "string" + }, + "stack_template_id": { + "type": "string" + } + } + }, + "models.TemplateChartSnapshotData": { + "type": "object", + "properties": { + "chart_name": { + "type": "string" + }, + "default_values": { + "type": "string" + }, + "is_required": { + "type": "boolean" + }, + "locked_values": { + "type": "string" + }, + "repo_url": { + "type": "string" + }, + "sort_order": { + "type": "integer" + } + } + }, + "models.TemplateSnapshot": { + "type": "object", + "properties": { + "charts": { + "type": "array", + "items": { + "$ref": "#/definitions/models.TemplateChartSnapshotData" + } + }, + "template": { + "$ref": "#/definitions/models.TemplateSnapshotData" + } + } + }, + "models.TemplateSnapshotData": { + "type": "object", + "properties": { + "category": { + "type": "string" + }, + "default_branch": { + "type": "string" + }, + "description": { + "type": "string" + }, + "is_published": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "models.TemplateVersion": { + "type": "object", + "properties": { + "change_summary": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "created_by": { + "type": "string" + }, + "id": { + "type": "string" + }, + "template_id": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "models.User": { + "type": "object", + "properties": { + "auth_provider": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "disabled": { + "type": "boolean" + }, + "display_name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "external_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "role": { + "type": "string" + }, + "service_account": { + "type": "boolean" + }, + "updated_at": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "models.UserFavorite": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "entity_id": { + "type": "string" + }, + "entity_type": { + "type": "string" + }, + "id": { + "type": "string" + }, + "user_id": { + "type": "string" + } + } + }, + "models.ValueOverride": { + "type": "object", + "properties": { + "chart_config_id": { + "type": "string" + }, + "id": { + "type": "string" + }, + "stack_instance_id": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "values": { + "type": "string" + } + } + }, + "scheduler.CleanupResult": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "error": { + "type": "string" + }, + "instance_id": { + "type": "string" + }, + "instance_name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "status": { + "description": "\"success\", \"error\", \"dry_run\"", + "type": "string" + } + } + } + }, + "securityDefinitions": { + "ApiKeyAuth": { + "description": "API key (format: \"sk_\u003ckey\u003e\")", + "type": "apiKey", + "name": "X-API-Key", + "in": "header" + }, + "BearerAuth": { + "description": "JWT Bearer token (format: \"Bearer \u003ctoken\u003e\")", + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file From 60e295aae1f3b3f343c7ec892da917d1774ae4bc Mon Sep 17 00:00:00 2001 From: Olof Mattsson Date: Thu, 28 May 2026 21:25:12 +0200 Subject: [PATCH 2/2] =?UTF-8?q?test(contract):=20refresh-swagger.sh=20?= =?UTF-8?q?=E2=80=94=20list=20python3=20in=20deps,=20validate=20via=20stdi?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both address CodeRabbit on PR #101: the script invokes python3 for JSON validation but the requirements comment listed only curl and shasum, and the validation embedded the temp filename in the inline Python string (mktemp output is normally safe, but stdin redirection is cleaner regardless and removes any quoting concern). Co-Authored-By: Claude Opus 4.7 --- cli/test/contract/refresh-swagger.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/test/contract/refresh-swagger.sh b/cli/test/contract/refresh-swagger.sh index 123d767..3178a15 100755 --- a/cli/test/contract/refresh-swagger.sh +++ b/cli/test/contract/refresh-swagger.sh @@ -14,7 +14,7 @@ # ./refresh-swagger.sh # fetch from main # ./refresh-swagger.sh # fetch from a specific tag/branch/sha # -# Requires: curl, shasum. +# Requires: curl, shasum, python3. set -euo pipefail REF="${1:-main}" @@ -29,7 +29,7 @@ curl -fsSL "$URL" -o "$tmp" # Refuse to overwrite if the fetched payload isn't valid JSON — protects # against fetching a 404 HTML page that happens to be 200 from a CDN. -if ! python3 -c "import json,sys; json.load(open('$tmp'))" 2>/dev/null; then +if ! python3 -c 'import json,sys; json.load(sys.stdin)' < "$tmp" 2>/dev/null; then echo "ERROR: fetched file is not valid JSON, refusing to overwrite" >&2 exit 1 fi