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..3178a15 --- /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, python3. +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(sys.stdin)' < "$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