diff --git a/skills/applaunchpad/README.md b/skills/applaunchpad/README.md new file mode 100644 index 0000000..50c2532 --- /dev/null +++ b/skills/applaunchpad/README.md @@ -0,0 +1,76 @@ +# Sealos AppLaunchpad — Claude Code Skill + +Deploy and manage containerized applications on [Sealos](https://sealos.io) using natural language. Create, scale, update, and manage apps from pre-built Docker images — without leaving your terminal. + +## Quick Start + +### Prerequisites + +- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed +- A [Sealos](https://sealos.io) account (login is handled automatically via OAuth) + +### Install + +Copy the `applaunchpad` skill into your project's `.claude/skills/` directory. Claude Code auto-detects it. + +### Usage + +Ask Claude Code in natural language, or run `/sealos-applaunchpad`: + +``` +> Deploy an nginx app on Sealos +> Launch a Redis cache +> Show my running apps +> Scale my-api to 2 cores and 4 GB memory +> Stop my staging app +> Delete the old test app +> Add 10Gi storage to my-app +> Update my-app's image to node:22-slim +``` + +Claude walks you through authentication, configuration, and execution interactively. + +## Features + +- **One-command deploy** — deploy any Docker image with smart resource defaults based on your tech stack +- **Full lifecycle** — create, list, inspect, update, delete, start, pause, restart +- **Auto-scaling** — fixed replicas or HPA (CPU/memory/GPU-based) auto-scaling +- **Networking** — public URLs with HTTP/gRPC/WebSocket support, plus private internal addresses +- **GPU support** — attach NVIDIA GPUs (A100, V100, T4) for ML/AI workloads +- **Storage management** — attach and expand persistent volumes (expand-only, per Kubernetes) +- **Environment & config** — manage env vars, config maps, and storage volumes per app +- **Project integration** — optionally saves access URLs to `.env` for easy use in your code +- **Safe deletes** — requires you to type the exact app name before destroying anything + +## Authentication + +On first use, Claude opens your browser for Sealos OAuth login. Credentials are cached locally at `~/.sealos/kubeconfig` and reused across sessions. Three regions are available: `gzg`, `bja`, and `hzh`. + +## Supported Operations + +| Operation | Description | +|-----------|-------------| +| **Create** | Deploy a new app from a Docker image with customizable CPU, memory, ports, env, storage | +| **List** | Show all apps with status, resources, and replica count | +| **Get** | Inspect a specific app's full configuration and access URLs | +| **Update** | Change image, resources, ports, env vars, config maps, or storage | +| **Delete** | Permanently remove an app (with name confirmation) | +| **Start** | Resume a paused app | +| **Pause** | Scale to zero replicas (preserves configuration) | +| **Restart** | Rolling restart of all replicas | +| **Storage** | Add or expand persistent volumes (incremental, expand-only) | + +## Resource Defaults + +| Tech Stack | Recommended Image | Default Port | +|------------|-------------------|-------------| +| Next.js / React | `node:20-slim` | 3000 | +| Django / FastAPI | `python:3.12-slim` | 8000 | +| Flask | `python:3.12-slim` | 5000 | +| Go | `golang:1.22` | 8080 | +| Spring Boot | `eclipse-temurin:21` | 8080 | +| Static site | `nginx:alpine` | 80 | +| Rust | `rust:1-slim` | 8080 | +| gRPC service | (varies) | 50051 | + +Default resources: 0.2 CPU, 0.5 GB memory, 1 replica. Production hints auto-scale to 1 CPU, 2 GB, 3 replicas. diff --git a/skills/applaunchpad/SKILL.md b/skills/applaunchpad/SKILL.md new file mode 100644 index 0000000..4a1d9c2 --- /dev/null +++ b/skills/applaunchpad/SKILL.md @@ -0,0 +1,383 @@ +--- +name: sealos-applaunchpad +description: >- + Use when someone needs to deploy or manage containerized applications on Sealos: + create, list, update, scale, delete, start, stop, pause, restart apps, + check status, manage ports, env vars, storage, config maps. + Triggers on "deploy my app", "create an app on sealos", "scale my app", + "I need to deploy a container", "stop my app", "show my apps", + "run a container", "launch a service", "manage my deployment", + "check my running apps", "update my app's image", "add storage to my app", + "host my app", "put this on the cloud", "I need a server for my API", + "spin up a container", "run this image", "set up a service on sealos". + Also trigger when user asks about running/hosting any containerized workload on Sealos. +--- + +## Interaction Principle + +**NEVER output a question as plain text. ALWAYS use `AskUserQuestion` with `options`.** + +Claude Code's text output is non-interactive — if you write a question as plain text, the +user has no clickable options and must guess how to respond. `AskUserQuestion` gives them +clear choices they can click. + +- Every question → `AskUserQuestion` with `options`. Keep any preceding text to one short status line. +- `AskUserQuestion` adds an implicit "Other / Type something" option, so users can always type custom input. +- **Free-text matching:** When the user types instead of clicking, match to the closest option by intent. + "show all apps" → "Show all apps"; "deploy" → Create option; "stop" → Pause option. + Never re-ask because wording didn't match exactly. + +## Fixed Execution Order + +**ALWAYS follow these steps in this exact order. No skipping, no reordering.** + +``` +Step 0: Check Auth (try existing config from previous session) +Step 1: Authenticate (only if Step 0 fails) +Step 2: Route (determine which operation the user wants) +Step 3: Execute operation (follow the operation-specific steps below) +``` + +--- + +## Step 0: Check Auth + +The script auto-derives its API URL from `~/.sealos/auth.json` (saved by login) +and reads credentials from `~/.sealos/kubeconfig`. No separate config file needed. + +1. Run `node scripts/sealos-applaunchpad.mjs list` +2. If works → skip Step 1. Greet with context: + > Connected to Sealos. You have N apps running. +3. If fails (not authenticated, 401, connection error) → proceed to Step 1 + +--- + +## Step 1: Authenticate + +Run this step only if Step 0 failed. + +### 1a. OAuth2 Login + +Read `config.json` (in this skill's directory) for available regions and the default. +Ask the user which region to connect to using `AskUserQuestion` with the regions as options. + +Run `node scripts/sealos-auth.mjs login {region_url}` (omit region_url for default). + +This command: +1. Opens the user's browser to the Sealos authorization page +2. Displays a user code and verification URL in stderr +3. Polls until the user approves (max 10 minutes) +4. Exchanges the token for a kubeconfig +5. Saves to `~/.sealos/kubeconfig` and `~/.sealos/auth.json` (with region) + +Display while waiting: +> Opening browser for Sealos login... Approve the request in your browser. + +**If TLS error**: Retry with `node scripts/sealos-auth.mjs login --insecure` + +**If other error**: +`AskUserQuestion`: +- header: "Login Failed" +- question: "Browser login failed. Try again?" +- options: ["Try again", "Cancel"] + +### 1b. Verify connection + +After login, run `node scripts/sealos-applaunchpad.mjs list` to verify auth works. + +**If auth error (401):** Token may have expired. Re-run `node scripts/sealos-auth.mjs login`. + +**If success:** Display: +> Connected to Sealos. You have N apps. + +Use the apps list in Step 3 instead of making a separate `list` call. + +--- + +## Step 2: Route + +Determine the operation from user intent: + +| Intent | Operation | +|--------|-----------| +| "create/deploy/launch an app" | Create | +| "list/show my apps" | List | +| "check status/details" | Get | +| "scale/resize/update/change" | Update | +| "delete/remove app" | Delete | +| "start" | Action (start) | +| **"stop/pause"** | **Action (pause)** — explain: AppLaunchpad uses "pause" (scales to zero) | +| "restart" | Action (restart) | +| "expand storage/add volume" | Storage Update | + +If ambiguous, ask one clarifying question. + +--- + +## Step 3: Operations + +### Create + +**3a. Ask name first** + +`AskUserQuestion`: +- header: "Name" +- question: "App name?" +- options: generate 2-3 name suggestions from what the user said (e.g., "deploy redis" → "redis", "redis-cache"). + If a name already exists (from list), avoid it and note the conflict. +- Constraint: `^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`, max 63 chars + +**3b. Determine image** + +- If user provides a specific image name: use it directly +- If user describes their tech stack (e.g., "a Next.js app"): recommend from `references/defaults.md` image table +- If user has source code but no pre-built image: recommend `/sealos-deploy` instead +- This skill deploys **pre-built images** only — do not scan the filesystem for Dockerfiles, docker-compose.yml, or project files + +**3c. Show recommended config and confirm** + +Read `references/defaults.md` for resource presets and image recommendations. + +Auto-resolve config from context: +- User's request (e.g., "deploy my Next.js app" → node:20-slim, port 3000) +- User's stated tech stack +- Scale hints (e.g., "production app" → higher resources) + +Display the **recommended config summary**: + +``` +App config: + + Name: my-app + Image: nginx:alpine + CPU: 0.2 Core(s) + Memory: 0.5 GB + Scaling: 1 replica(s) + Ports: 80/http (public) + Env: 0 variable(s) + Storage: 0 volume(s) + ConfigMap: 0 file(s) +``` + +Then `AskUserQuestion`: +- header: "Config" +- question: "Create with this config?" +- options: + 1. "Create now (Recommended)" — accept all, proceed to 3d + 2. "Customize" — go to 3c-customize flow + +**3c-customize:** Read `references/create-flow.md` and follow the customize flow there. + +**3d. Create and wait** + +Build JSON body and run `node scripts/sealos-applaunchpad.mjs create-wait ''`. +This creates the app and polls until `running` (timeout 2 minutes). + +**3e. Show access URLs and offer integration** + +For public http/grpc/ws ports, display access URLs from the response: +- `publicAddress` from each port in the response +- `privateAddress` for internal access + +Then `AskUserQuestion`: +- header: "Integration" +- question: "Save access URL to your project?" +- options: + 1. "Add to .env" — append `APP_URL=https://...` (and `APP_PRIVATE_URL=...`) to `.env` + 2. "Skip" — just show the info, don't write anything +- When writing to `.env`, append, don't overwrite existing content. + +--- + +### List + +Run `node scripts/sealos-applaunchpad.mjs list`. Format as table: + +``` +Name Image Status CPU Mem Replicas Ports +my-app nginx:alpine running 0.2 0.5GB 1 80/http (public) +api-server node:20-slim running 1 2GB 3 3000/http (public) +``` + +Highlight abnormal statuses (error, waiting, pause). + +--- + +### Get + +If no name given, run List first, then `AskUserQuestion` with app names as options +(header: "App", question: "Which app?"). + +Run `node scripts/sealos-applaunchpad.mjs get {name}`. Display: +- name, image, status, quota (cpu, memory, replicas/hpa, gpu) +- ports with public/private addresses +- env variables, configMap, storage +- resourceType, kind, createdAt, upTime +- Access URLs for public ports + +--- + +### Update + +**3a.** If no name given → List, then `AskUserQuestion` to pick which app +(options = app names from list). + +**3b.** Run `node scripts/sealos-applaunchpad.mjs get {name}`, show current specs. + +**3c.** `AskUserQuestion` (header: "Update", question: "What to change?", multiSelect: true): +- "Image & Command" +- "Resources (CPU, Memory, Scaling)" +- "Networking (Ports)" +- "Advanced (Env, ConfigMap, Storage)" + +For each selected field, follow up with `AskUserQuestion` offering allowed values as options. +See `references/api-reference.md` for allowed update values: +- CPU: 0.1, 0.2, 0.5, 1, 2, 3, 4, 8 +- Memory: 0.1, 0.5, 1, 2, 4, 8, 16 + +**3d.** **WARN**: ports, env, configMap, storage are **COMPLETE REPLACEMENT** — +must include ALL items to keep. Items not listed will be deleted. + +Show before/after diff, then `AskUserQuestion` (header: "Confirm", +question: "Apply these changes?"): +- "Apply (Recommended)" +- "Edit again" +- "Cancel" + +**3e.** Run `node scripts/sealos-applaunchpad.mjs update {name} '{json}'`. + +--- + +### Delete + +**This is destructive. Maximum friction.** + +**3a.** If no name given → List, then `AskUserQuestion` to pick which app. + +**3b.** Run `node scripts/sealos-applaunchpad.mjs get {name}`, show full details. + +**3c.** Explain consequences: +- The app and all its resources (pods, services, ingress) will be permanently deleted +- Storage volumes may be lost +- Public URLs will stop working immediately + +**3d.** `AskUserQuestion`: +- header: "Confirm Delete" +- question: "Type `{name}` to permanently delete this app" +- options: ["Cancel"] — do NOT include the app name as a clickable option. + The user must type the exact name via "Type something" to confirm. + +If user types the correct name → proceed to 3e. +If user types something else → reply "Name doesn't match" and re-ask. +If user clicks Cancel → abort. + +**3e.** Run `node scripts/sealos-applaunchpad.mjs delete {name}`. + +--- + +### Action (Start/Pause/Restart) + +**3a.** If no name given → List, then `AskUserQuestion` to pick which app. + +**3b.** `AskUserQuestion` to confirm (header: "Action", question: "Confirm {action} on {name}?"): +- "{Action} now" +- "Cancel" + +**Important:** "stop" maps to "pause". Explain to the user: +> AppLaunchpad uses "pause" which scales your app to zero replicas. Your configuration is preserved. + +**3c.** Run `node scripts/sealos-applaunchpad.mjs {action} {name}`. +**3d.** For `start`: poll `node scripts/sealos-applaunchpad.mjs get {name}` until `running`. + +--- + +### Storage Update + +**3a.** If no name given → List, then `AskUserQuestion` to pick which app. + +**3b.** Run `node scripts/sealos-applaunchpad.mjs get {name}` to verify current storage state. + +**3c.** Ask what to add/expand: +- Show current storage volumes +- This is **incremental merge** — only specify new or expanded volumes +- Name is auto-generated from path (don't send name field) + +**3d.** **Expand only** — warn if user tries to shrink: +> Storage volumes can only be expanded, never shrunk (Kubernetes limitation). + +**3e.** Warn about potential downtime during storage operations. + +**3f.** Run `node scripts/sealos-applaunchpad.mjs update-storage {name} '{"storage":[...]}'`. + +--- + +## Scripts + +Two entry points in `scripts/` (relative to this skill's directory): +- `sealos-auth.mjs` — OAuth2 Device Grant login (shared across all skills) +- `sealos-applaunchpad.mjs` — App deployment and management operations + +**Auth commands:** +```bash +node $SCRIPTS/sealos-auth.mjs check # Check if authenticated +node $SCRIPTS/sealos-auth.mjs login # Start OAuth2 login +node $SCRIPTS/sealos-auth.mjs login --insecure # Skip TLS verification +node $SCRIPTS/sealos-auth.mjs info # Show auth details +``` + +Zero external dependencies (Node.js only). TLS verification is disabled for self-signed certs. + +**The scripts are bundled with this skill — do NOT check if they exist. Just run them.** + +**Path resolution:** Scripts are in this skill's `scripts/` directory. The full path is +listed in the system environment's "Additional working directories" — use it directly. + +**Config resolution:** The script reads `~/.sealos/auth.json` (region) and `~/.sealos/kubeconfig` +(credentials) — both created by `sealos-auth.mjs login`. + +```bash +# Examples use SCRIPT as placeholder — replace with /scripts/sealos-applaunchpad.mjs + +# After login, everything just works — API URL derived from auth.json region +node $SCRIPT list +node $SCRIPT get my-app +node $SCRIPT create '{"name":"my-app","image":{"imageName":"nginx:alpine"},"quota":{"cpu":0.2,"memory":0.5,"replicas":1},"ports":[{"number":80}]}' +node $SCRIPT create-wait '{"name":"my-app","image":{"imageName":"nginx:alpine"},"quota":{"cpu":0.2,"memory":0.5,"replicas":1},"ports":[{"number":80}]}' +node $SCRIPT update my-app '{"quota":{"cpu":1,"memory":2}}' +node $SCRIPT delete my-app +node $SCRIPT start|pause|restart my-app +node $SCRIPT update-storage my-app '{"storage":[{"path":"/var/data","size":"20Gi"}]}' +``` + +## Reference Files + +- `references/api-reference.md` — API endpoints, resource constraints, error formats. Read first. +- `references/defaults.md` — Resource presets, image recommendations, config summary template. Read for create operations. +- `references/create-flow.md` — Create customize flow (Image, Resources, Networking, Advanced). Read when user selects "Customize" during create. +- `references/openapi.json` — Complete OpenAPI spec (~6000 lines). **DO NOT read this file in full.** Use `api-reference.md` instead — it covers all standard operations. Only read specific sections of `openapi.json` (with line-limited reads) if you need schema details not covered by `api-reference.md`. + +## Error Handling + +**Treat each error independently.** Do NOT chain unrelated errors. + +| Scenario | Action | +|----------|--------| +| Kubeconfig not found | Run `node scripts/sealos-auth.mjs login` to authenticate | +| Auth error (401) | Kubeconfig expired. Run `node scripts/sealos-auth.mjs login` to re-authenticate. | +| Name conflict (409) | Suggest alternative name | +| Invalid image | Check image name and registry access | +| Storage shrink | Refuse, K8s limitation | +| Creation timeout (>2 min) | Offer to keep polling or check console | +| "namespace not found" (500) | Cluster admin kubeconfig; need Sealos user kubeconfig | + +## Rules + +- NEVER ask a question as plain text — ALWAYS use `AskUserQuestion` with options +- NEVER ask user to manually download kubeconfig — always use `scripts/sealos-auth.mjs login` +- NEVER run `test -f` or `ls` on the skill scripts — they are always present, just run them +- NEVER write kubeconfig to `~/.kube/config` — may overwrite user's existing config +- NEVER echo kubeconfig content to output +- NEVER construct HTTP requests inline — always use `scripts/sealos-applaunchpad.mjs` +- NEVER delete without explicit name confirmation +- When writing to `.env`, append, don't overwrite +- Storage can only expand, never shrink diff --git a/skills/applaunchpad/config.json b/skills/applaunchpad/config.json new file mode 100644 index 0000000..08876ee --- /dev/null +++ b/skills/applaunchpad/config.json @@ -0,0 +1,9 @@ +{ + "client_id": "af993c98-d19d-4bdc-b338-79b80dc4f8bf", + "default_region": "https://gzg.sealos.run", + "regions": [ + "https://gzg.sealos.run", + "https://bja.sealos.run", + "https://hzh.sealos.run" + ] +} diff --git a/skills/applaunchpad/references/api-reference.md b/skills/applaunchpad/references/api-reference.md new file mode 100644 index 0000000..b5a5a6a --- /dev/null +++ b/skills/applaunchpad/references/api-reference.md @@ -0,0 +1,234 @@ +# Sealos AppLaunchpad API Reference + +Base URL: `https://applaunchpad.{domain}/api/v2alpha` + +> **API Version:** `v2alpha` — centralized in the script constant `API_PATH` (`scripts/sealos-applaunchpad.mjs`). +> If the API version changes, update `API_PATH` in the script; the rest auto-follows. + +## TLS Note + +The script sets `rejectUnauthorized: false` for HTTPS requests because Sealos clusters +may use self-signed TLS certificates. Without this, Node.js would reject connections +to clusters that don't have publicly trusted certificates. + +## Authentication + +All requests require a URL-encoded kubeconfig YAML in the `Authorization` header. +There are no unauthenticated endpoints (unlike the DB API). + +``` +Authorization: +``` + +## Endpoints + +| # | Method | Path | OperationID | Description | +|---|--------|------|-------------|-------------| +| 1 | GET | /apps | listApps | List all applications | +| 2 | POST | /apps | createApp | Create a new application | +| 3 | GET | /apps/{name} | getApp | Get application details by name | +| 4 | PATCH | /apps/{name} | updateApp | Update application configuration | +| 5 | DELETE | /apps/{name} | deleteApp | Delete application | +| 6 | POST | /apps/{name}/start | startApp | Start paused application | +| 7 | POST | /apps/{name}/pause | pauseApp | Pause application (scales to zero) | +| 8 | POST | /apps/{name}/restart | restartApp | Restart application | +| 9 | PATCH | /apps/{name}/storage | updateAppStorage | Update storage (incremental merge, expand only) | + +## Resource Constraints + +### Create (POST /apps) + +| Field | Required | Type | Constraint | Default | +|-------|----------|------|------------|---------| +| name | no | string | `^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`, max 63 | "hello-world" | +| image.imageName | yes (via image) | string | Docker image with tag | "nginx" | +| image.imageRegistry | no | object/null | `{ username, password, serverAddress }` or null | null | +| launchCommand.command | no | string | Container run command | — | +| launchCommand.args | no | string[] | Command arguments | — | +| quota.cpu | no | number | range: 0.1–32 (continuous) | 0.2 | +| quota.memory | no | number | range: 0.1–32 GB (continuous) | 0.5 | +| quota.replicas | no | number | enum: 1–20 (mutually exclusive with hpa) | — | +| quota.gpu | no | object | `{ vendor (default "nvidia"), type (required), amount (default 1) }` | — | +| quota.hpa | no | object | `{ target (cpu\|memory\|gpu), value (%), minReplicas, maxReplicas }` — all required | — | +| ports[] | no | array | see Port object below | `[{ number: 80, protocol: "http", isPublic: true }]` | +| env[] | no | array | `{ name (required), value, valueFrom }` | `[]` | +| configMap[] | no | array | `{ path (required), value }` | `[]` | +| storage[] | no | array | `{ name (required), path (required), size (default "1Gi") }` | `[]` | + +**Only `quota` is strictly required** in the request body. Image defaults to `{ imageName: "nginx" }`. + +#### Port Object (Create) + +| Field | Required | Type | Constraint | Default | +|-------|----------|------|------------|---------| +| number | yes | number | 1–65535 | — | +| protocol | no | string | enum: http, grpc, ws, tcp, udp, sctp | http | +| isPublic | no | boolean | Only effective for http/grpc/ws protocols | true | + +#### GPU Object + +| Field | Required | Type | Constraint | Default | +|-------|----------|------|------------|---------| +| vendor | no | string | GPU vendor | nvidia | +| type | yes | string | GPU model (e.g., A100, V100, T4) | — | +| amount | no | number | Number of GPUs | 1 | + +#### HPA Object + +| Field | Required | Type | Constraint | +|-------|----------|------|------------| +| target | yes | string | enum: cpu, memory, gpu | +| value | yes | number | Target utilization percentage | +| minReplicas | yes | number | Minimum replica count | +| maxReplicas | yes | number | Maximum replica count | + +**Note:** `replicas` and `hpa` are mutually exclusive. Provide one or the other, not both. + +### Update (PATCH /apps/{name}) + +| Field | Type | Allowed Values | Notes | +|-------|------|----------------|-------| +| quota.cpu | number | 0.1, 0.2, 0.5, 1, 2, 3, 4, 8 | Discrete values only | +| quota.memory | number | 0.1, 0.5, 1, 2, 4, 8, 16 GB | Discrete values only | +| quota.replicas | number | enum: 1–20 | Set to switch from HPA to fixed | +| quota.hpa | object | same as create | Set to switch from fixed to HPA (omit replicas) | +| quota.gpu | object | `{ vendor, type, amount }` | | +| image | object | `{ imageName (required), imageRegistry }` | | +| launchCommand | object | `{ command, args[] }` | | +| ports[] | array | see below | **COMPLETE REPLACEMENT** | +| env[] | array | same as create | **COMPLETE REPLACEMENT** | +| configMap[] | array | `{ path (required), value }` | **COMPLETE REPLACEMENT** | +| storage[] | array | `{ path (required), size }` — name auto-generated | **COMPLETE REPLACEMENT** | + +**COMPLETE REPLACEMENT semantics for ports, env, configMap, storage:** +- Pass ALL items you want to keep — items not listed are deleted +- Pass empty array `[]` to remove all items +- Omit the field entirely to keep existing items unchanged + +**Ports in update:** Include `portName` to update an existing port, omit `portName` to create a new port. +Ports not included in the array will be deleted. + +### Storage Update (PATCH /apps/{name}/storage) + +| Field | Type | Notes | +|-------|------|-------| +| storage[] | array | `{ path (required), size (default "1Gi") }` | + +**INCREMENTAL merge** — only list items to add or modify. Unlisted items are preserved. +`name` is auto-generated from `path` (unlike POST where name is user-provided). +**Expand only** — storage cannot be shrunk (Kubernetes limitation). + +## Endpoint Details + +### POST /apps — Create + +```json +{ + "name": "my-app", + "image": { "imageName": "nginx:alpine" }, + "quota": { "cpu": 0.2, "memory": 0.5, "replicas": 1 }, + "ports": [{ "number": 80, "protocol": "http", "isPublic": true }], + "env": [{ "name": "NODE_ENV", "value": "production" }], + "configMap": [{ "path": "/etc/app/config.yaml", "value": "key: value" }], + "storage": [{ "name": "data", "path": "/var/data", "size": "10Gi" }] +} +``` + +Response: `201 Created` → Full application object (same as GET response) + +### GET /apps — List All + +Response: `200 OK` → Array of application objects + +### GET /apps/{name} — Get Details + +Response: `200 OK` → Full application object + +Application object fields: + +```json +{ + "name": "my-app", + "image": { "imageName": "nginx:alpine", "imageRegistry": null }, + "launchCommand": { "command": "", "args": [] }, + "quota": { "cpu": 0.2, "memory": 0.5, "replicas": 1 }, + "ports": [ + { + "number": 80, + "portName": "abcdef123456", + "protocol": "http", + "privateAddress": "http://my-app.ns-xxx:80", + "publicAddress": "https://abcdef123456.cloud.sealos.io", + "customDomain": "" + } + ], + "env": [], + "storage": [], + "configMap": [], + "resourceType": "launchpad", + "kind": "deployment", + "uid": "abc123-def456-789", + "createdAt": "2024-01-01T00:00:00Z", + "upTime": "2h15m", + "status": "running" +} +``` + +Status values: `running`, `creating`, `waiting`, `error`, `pause` + +### PATCH /apps/{name} — Update + +```json +{ + "quota": { "cpu": 1, "memory": 2 }, + "ports": [ + { "number": 80, "protocol": "http", "isPublic": true, "portName": "existing-port-name" }, + { "number": 8080, "protocol": "http", "isPublic": false } + ] +} +``` + +Response: `204 No Content` + +### DELETE /apps/{name} — Delete + +Response: `204 No Content` + +### POST /apps/{name}/start — Start + +Response: `204 No Content` + +### POST /apps/{name}/pause — Pause + +Scales the application to zero replicas. + +Response: `204 No Content` + +### POST /apps/{name}/restart — Restart + +Response: `204 No Content` + +### PATCH /apps/{name}/storage — Update Storage + +```json +{ + "storage": [{ "path": "/var/data", "size": "20Gi" }] +} +``` + +Response: `204 No Content` + +## Error Response Format + +```json +{ + "error": { + "type": "validation_error", + "code": "INVALID_PARAMETER", + "message": "...", + "details": [...] + } +} +``` + +Types: `validation_error`, `resource_error`, `internal_error` diff --git a/skills/applaunchpad/references/create-flow.md b/skills/applaunchpad/references/create-flow.md new file mode 100644 index 0000000..ea395a1 --- /dev/null +++ b/skills/applaunchpad/references/create-flow.md @@ -0,0 +1,60 @@ +# Create Customize Flow + +## 3d-customize: Pick fields to change, then configure only those + +`AskUserQuestion`: +- header: "Customize" +- question: "Which fields do you want to change?" +- multiSelect: true +- options: **(max 4 items)** — group into 4: + - "Image & Command — {current_image}" + - "Resources (CPU, Memory, Scaling) — {cpu}C / {mem}GB / {replicas}rep" + - "Networking (Ports) — {port_summary}" + - "Advanced (Env, ConfigMap, Storage)" + +When **"Image & Command"** selected: +- Ask for imageName +- If private registry needed: ask username, password, serverAddress +- If launch command needed: ask command and args + +When **"Resources"** selected → ask sequentially: + +**CPU** → `AskUserQuestion`: +- header: "CPU" +- question: "CPU cores? (0.1-32)" +- options: `0.2 (current), 0.5, 1, 2` cores. Mark current with "(current)". + +**Memory** → `AskUserQuestion`: +- header: "Memory" +- question: "Memory? (0.1-32 GB)" +- options: `0.5 (current), 1, 2, 4` GB. Mark current with "(current)". + +**Scaling** → `AskUserQuestion`: +- header: "Scaling" +- question: "Fixed replicas or auto-scaling (HPA)?" +- options: + 1. "Fixed replicas (current)" → ask replica count + 2. "Auto-scaling (HPA)" → ask target metric, value%, min, max + +If user explicitly mentions GPU or project context indicates ML/AI workload: +**GPU** → `AskUserQuestion`: +- header: "GPU" +- question: "GPU type?" +- options: `A100, V100, T4, Skip` + +When **"Networking"** selected: +- Ask for port number, protocol (show enum: http, grpc, ws, tcp, udp, sctp) +- If protocol is http/grpc/ws: ask isPublic toggle +- Note: isPublic is only effective for http/grpc/ws protocols + +When **"Advanced"** selected: +- Ask about env variables (name=value pairs) +- Ask about configMap files (path + content) +- Ask about storage volumes (name, path, size) + +After all fields, re-display the updated config summary and `AskUserQuestion`: +- header: "Config" +- question: "Create with this config?" +- options: + 1. "Create now (Recommended)" + 2. "Customize" — re-run the customize flow diff --git a/skills/applaunchpad/references/defaults.md b/skills/applaunchpad/references/defaults.md new file mode 100644 index 0000000..f5fcd5d --- /dev/null +++ b/skills/applaunchpad/references/defaults.md @@ -0,0 +1,95 @@ +# Sealos AppLaunchpad Defaults & Presets + +## Resource Presets (internal) + +Used to set initial default values based on user intent. These are NOT shown +to the user as "tiers" — the user sees individual CPU/Memory/Replicas fields. + +| Scenario | CPU | Memory | Replicas | Trigger phrases | +|----------|-----|--------|----------|-----------------| +| Default | 0.2 | 0.5 GB | 1 | no hint, "dev", "testing", "try" | +| Small | 0.5 | 1 GB | 1 | "small", "starter" | +| Production | 1 | 2 GB | 3 | "prod", "production" | +| HA | 2 | 4 GB | 3 | "HA", "high availability" | +| Custom | — | — | — | specific numbers like "0.5 cores, 1g memory" | + +## Image Recommendations by Tech Stack + +| Tech stack | Image | Default port | Protocol | +|------------|-------|-------------|----------| +| Next.js / React | node:20-slim | 3000 | http | +| Django / FastAPI | python:3.12-slim | 8000 | http | +| Flask | python:3.12-slim | 5000 | http | +| Go | golang:1.22 | 8080 | http | +| Spring Boot | eclipse-temurin:21 | 8080 | http | +| Static site | nginx:alpine | 80 | http | +| Rust | rust:1-slim | 8080 | http | +| gRPC service | (varies) | 50051 | grpc | + +When multiple stacks fit, prefer the first match in the table. + +## Config Summary Template + +Display this read-only summary before asking the user to confirm or customize. +Shows every field individually — no "tier" abstraction in the user-facing output. + +``` +App config: + + Name: [name] + Image: [imageName] + CPU: [n] Core(s) + Memory: [n] GB + Scaling: [n] replica(s) | HPA [min]-[max] target [metric] [value]% + Ports: [number]/[protocol] (public|private) + Env: [count] variable(s) + Storage: [count] volume(s) + ConfigMap: [count] file(s) +``` + +## AskUserQuestion Option Guidelines + +**Hard limit: max 4 options per `AskUserQuestion` call.** The tool auto-appends implicit +options ("Type something", "Chat about this") which consume slots. More than 4 user-provided +options will be truncated and invisible to the user. + +When building options for `AskUserQuestion`: +- **Name options**: generate 2-3 name suggestions from what the user said. + If a name already exists (from list), avoid it and note the conflict. +- **CPU options**: max 4 items: 0.2, 0.5, 1, 2 cores. +- **Memory options**: max 4 items: 0.5, 1, 2, 4 GB. +- **Replicas options**: max 4 items: 1, 2, 3, 5. +- For all resource options, mark current value with "(current)". + User can type other valid values via "Type something". +- **App picker** (for get/update/delete/action): list app names from + `sealos-applaunchpad.mjs list` as options, up to 4. If more than 4, show most recent ones. + +## Field Generation Rules + +- **Name**: derive from user's request (e.g., "deploy redis" → "redis"). Do not scan the local filesystem for project directory names. +- **Name constraint**: `^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`, max 63 chars + +## GPU Note + +Only mention GPU in the customize flow if the user explicitly requests it or if +the project context indicates GPU workloads (ML, AI, inference, training). + +GPU fields: +- `vendor`: GPU vendor, default "nvidia" +- `type`: required — GPU model (A100, V100, T4, etc.) +- `amount`: number of GPUs, default 1 + +## HPA Note + +Default to fixed replicas. Offer HPA only in the customize flow or when the +user says "auto-scale", "elastic", "scale based on CPU/memory". + +HPA fields: +- `target`: metric to scale on (cpu, memory, gpu) +- `value`: target utilization percentage +- `minReplicas`: minimum number of replicas +- `maxReplicas`: maximum number of replicas + +## Create API Field Reference + +See `api-reference.md` for the full field reference (Create and Update constraints). diff --git a/skills/applaunchpad/references/openapi.json b/skills/applaunchpad/references/openapi.json new file mode 100644 index 0000000..ab03456 --- /dev/null +++ b/skills/applaunchpad/references/openapi.json @@ -0,0 +1,5816 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Application Launchpad API", + "version": "2.0.0-alpha", + "description": "Application Launchpad is a Sealos service for deploying and managing containerized applications on Kubernetes. This API allows you to create, query, update, delete, start, pause, and restart applications programmatically.\n\n## Authentication\n\nAll requests require a valid kubeconfig passed in the `Authorization` header.\n\nEncode the kubeconfig YAML string with `encodeURIComponent()` before setting the header:\n\n```\nAuthorization: \n```\n\nObtain your kubeconfig from the Sealos console user menu. A missing or invalid kubeconfig results in a `401 Unauthorized` response.\n\n## Errors\n\nAll error responses use a unified format:\n\n```json\n{\n \"error\": {\n \"type\": \"validation_error\",\n \"code\": \"INVALID_PARAMETER\",\n \"message\": \"Request body validation failed.\",\n \"details\": [{ \"field\": \"image.imageName\", \"message\": \"Required\" }]\n }\n}\n```\n\n- `type` — high-level category (e.g. `validation_error`, `resource_error`, `internal_error`)\n- `code` — stable identifier for programmatic handling\n- `message` — human-readable explanation\n- `details` — optional extra context; shape varies by `code` (field list, string, or object)\n\n## Operations\n\n**Query** (read-only): returns `200 OK` with data in the response body.\n\n**Mutation** (write): Create → `201 Created` with the created resource. Update/Delete → `204 No Content`." + }, + "servers": [ + { + "url": "http://localhost:3000/api/v2alpha", + "description": "Local development" + }, + { + "url": "https://applaunchpad.192.168.12.53.nip.io/api/v2alpha", + "description": "Production" + }, + { + "url": "{baseUrl}/api/v2alpha", + "description": "Custom", + "variables": { + "baseUrl": { + "default": "https://applaunchpad.example.com", + "description": "Base URL of your instance (e.g. https://applaunchpad.192.168.x.x.nip.io)" + } + } + } + ], + "tags": [ + { + "name": "Query", + "description": "Read-only operations. Success: `200 OK` with data in the response body." + }, + { + "name": "Mutation", + "description": "Write operations. Create: `201 Created` with the new resource. Update/Delete: `204 No Content`." + } + ], + "security": [ + { + "kubeconfigAuth": [] + } + ], + "paths": { + "/apps": { + "get": { + "tags": [ + "Query" + ], + "operationId": "listApps", + "summary": "List all applications", + "description": "Returns all applications deployed in the current namespace.", + "responses": { + "200": { + "description": "List of applications retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Application name", + "example": "web-api" + }, + "image": { + "type": "object", + "properties": { + "imageName": { + "type": "string", + "description": "Docker image name with tag", + "example": "nginx:1.21" + }, + "imageRegistry": { + "type": [ + "object", + "null" + ], + "properties": { + "username": { + "type": "string", + "description": "Registry username", + "example": "robot$myproject" + }, + "password": { + "type": "string", + "description": "Registry password", + "example": "token123" + }, + "serverAddress": { + "type": "string", + "description": "Registry server address", + "example": "registry.example.com" + } + }, + "required": [ + "username", + "password", + "serverAddress" + ], + "description": "Image pull secret configuration. Set to null to switch from private to public image." + } + }, + "required": [ + "imageName" + ], + "description": "Container image configuration" + }, + "launchCommand": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "Container run command - was: runCMD", + "example": "node" + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Container command arguments (each element is a separate arg)", + "example": [ + "--config", + "/etc/app/config.yaml" + ] + } + }, + "description": "Container launch command configuration" + }, + "quota": { + "type": "object", + "properties": { + "replicas": { + "type": "number", + "description": "Number of pod replicas (for fixed instances). Cannot be used with hpa.", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20 + ], + "example": 2 + }, + "cpu": { + "type": "number", + "minimum": 0.1, + "maximum": 32, + "default": 0.2, + "description": "CPU allocation in cores - range [0.1, 32]", + "example": 0.5 + }, + "memory": { + "type": "number", + "minimum": 0.1, + "maximum": 32, + "default": 0.5, + "description": "Memory allocation in GB - range [0.1, 32]", + "example": 1 + }, + "gpu": { + "type": "object", + "properties": { + "vendor": { + "type": "string", + "default": "nvidia", + "description": "GPU vendor (e.g., nvidia)", + "example": "nvidia" + }, + "type": { + "type": "string", + "description": "GPU model (e.g., A100, V100, T4)", + "example": "A100" + }, + "amount": { + "type": "number", + "default": 1, + "description": "Number of GPUs to allocate", + "example": 1 + } + }, + "required": [ + "vendor", + "type", + "amount" + ], + "description": "GPU resource configuration" + }, + "hpa": { + "type": "object", + "properties": { + "target": { + "type": "string", + "enum": [ + "cpu", + "memory", + "gpu" + ], + "description": "Resource metric to scale on" + }, + "value": { + "type": "number", + "description": "Target resource utilization percentage" + }, + "minReplicas": { + "type": "number", + "description": "Minimum number of replicas" + }, + "maxReplicas": { + "type": "number", + "description": "Maximum number of replicas" + } + }, + "required": [ + "target", + "value", + "minReplicas", + "maxReplicas" + ], + "description": "Horizontal Pod Autoscaler configuration. If present, enables elastic scaling; if absent, uses fixed replicas." + } + }, + "required": [ + "cpu", + "memory" + ], + "description": "Resource quota and scaling configuration" + }, + "ports": { + "type": "array", + "items": { + "type": "object", + "properties": { + "number": { + "type": "number", + "description": "Port number", + "example": 8080 + }, + "portName": { + "type": "string", + "description": "Port name identifier", + "example": "abcdef123456" + }, + "protocol": { + "type": "string", + "description": "Protocol type (http, grpc, ws)", + "example": "http" + }, + "privateAddress": { + "type": "string", + "description": "Private access address", + "example": "http://my-app.ns-user123:8080" + }, + "publicAddress": { + "type": "string", + "description": "Public access address", + "example": "https://xyz789.cloud.sealos.io" + }, + "customDomain": { + "type": "string", + "description": "Custom domain (if configured)", + "example": "api.example.com" + } + }, + "required": [ + "number" + ], + "description": "Port configuration details" + }, + "description": "Port/network configurations with runtime addresses" + }, + "env": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Environment variable name", + "example": "DATABASE_URL" + }, + "value": { + "type": "string", + "description": "Environment variable value", + "example": "postgres://user:pass@host:5432/db" + }, + "valueFrom": { + "type": "object", + "properties": { + "secretKeyRef": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Key within the secret", + "example": "database-url" + }, + "name": { + "type": "string", + "description": "Name of the Kubernetes Secret", + "example": "my-app-secrets" + } + }, + "required": [ + "key", + "name" + ] + } + }, + "required": [ + "secretKeyRef" + ], + "description": "Reference to secret for the value" + } + }, + "required": [ + "name" + ], + "description": "Environment variable configuration" + }, + "description": "Environment variables" + }, + "storage": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Persistent volume name", + "example": "data" + }, + "path": { + "type": "string", + "description": "Mount path in the container", + "example": "/var/data" + }, + "size": { + "type": "string", + "default": "1Gi", + "description": "Storage size (e.g., \"10Gi\", \"1Ti\")", + "example": "10Gi" + } + }, + "required": [ + "name", + "path", + "size" + ], + "description": "Persistent storage configuration" + }, + "description": "Persistent storage volumes" + }, + "configMap": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "ConfigMap name" + }, + "path": { + "type": "string", + "description": "Mount path in the container" + }, + "key": { + "type": "string", + "description": "Specific key within the configmap" + }, + "value": { + "type": "string", + "description": "Configuration value or file content" + } + }, + "required": [ + "name", + "path" + ], + "description": "ConfigMap configuration" + }, + "description": "ConfigMap mounts" + }, + "resourceType": { + "type": "string", + "const": "launchpad", + "description": "Resource type identifier (fixed for v2alpha)", + "example": "launchpad" + }, + "kind": { + "type": "string", + "enum": [ + "deployment", + "statefulset" + ], + "description": "Underlying Kubernetes workload kind", + "example": "deployment" + }, + "uid": { + "type": "string", + "description": "Application UID", + "example": "abc123-def456-789" + }, + "createdAt": { + "type": "string", + "description": "Creation time", + "example": "2024-01-01T00:00:00Z" + }, + "upTime": { + "type": "string", + "description": "Uptime (running duration)", + "example": "2h15m" + }, + "status": { + "type": "string", + "enum": [ + "running", + "creating", + "waiting", + "error", + "pause" + ], + "description": "Application status", + "example": "running" + } + }, + "required": [ + "name", + "image", + "quota", + "resourceType", + "uid", + "createdAt", + "upTime", + "status" + ], + "title": "Launchpad Application", + "description": "Launchpad application schema for GET responses (CreateRequest + metadata)" + } + }, + "examples": { + "list": { + "summary": "List of applications", + "value": [ + { + "name": "web-api", + "resourceType": "launchpad", + "kind": "deployment", + "image": { + "imageName": "nginx:1.21", + "imageRegistry": null + }, + "quota": { + "cpu": 0.5, + "memory": 1, + "replicas": 1 + }, + "ports": [ + { + "number": 80, + "portName": "abcdef123456", + "protocol": "http", + "privateAddress": "http://web-api-80-xyz-service.ns-user123:80", + "publicAddress": "https://xyz789abc123.cloud.sealos.io" + } + ], + "env": [], + "storage": [], + "configMap": [], + "uid": "abc123-def456-789", + "createdAt": "2024-01-01T00:00:00Z", + "upTime": "2h15m", + "status": "running" + } + ] + }, + "empty": { + "summary": "No applications deployed", + "value": [] + } + } + } + } + }, + "401": { + "description": "Unauthorized - Missing or invalid kubeconfig", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authentication_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "AUTHENTICATION_REQUIRED", + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 401" + }, + "examples": { + "missingAuth": { + "summary": "Missing authentication", + "value": { + "error": { + "type": "authentication_error", + "code": "AUTHENTICATION_REQUIRED", + "message": "Authentication required. Please provide valid credentials in the Authorization header." + } + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authorization_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "PERMISSION_DENIED" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 403 - list apps" + }, + "examples": { + "insufficientPermissions": { + "summary": "Insufficient permissions", + "value": { + "error": { + "type": "authorization_error", + "code": "PERMISSION_DENIED", + "message": "Insufficient permissions to perform this operation. Please check your access rights." + } + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "operation_error", + "internal_error" + ], + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "KUBERNETES_ERROR", + "INTERNAL_ERROR" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Raw error string from the underlying system, for troubleshooting.", + "example": "deployments.apps \"web-api\" not found" + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 500" + }, + "examples": { + "kubernetesError": { + "summary": "Kubernetes API error", + "value": { + "error": { + "type": "operation_error", + "code": "KUBERNETES_ERROR", + "message": "Failed to list applications.", + "details": "namespaces \"ns-xxx\" not found" + } + } + } + } + } + } + }, + "503": { + "description": "Service Unavailable - Kubernetes cluster temporarily unreachable", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "internal_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "SERVICE_UNAVAILABLE", + "description": "Kubernetes cluster or required dependency is temporarily unreachable" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Connection error from the underlying system", + "example": "connect ECONNREFUSED 10.0.0.1:6443" + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 503 - list apps" + }, + "examples": { + "serviceUnavailable": { + "summary": "Cluster unreachable", + "value": { + "error": { + "type": "internal_error", + "code": "SERVICE_UNAVAILABLE", + "message": "Kubernetes cluster is temporarily unavailable. Please try again later.", + "details": "connect ECONNREFUSED 10.0.0.1:6443" + } + } + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Mutation" + ], + "operationId": "createApp", + "summary": "Create a new application", + "description": "Creates a new containerized application with specified quota, networking, storage, and environment configurations.\n\nKey points:\n- Quota: Use `quota.replicas` (1–20) for fixed replicas, or `quota.hpa` for auto-scaling (cannot use both). CPU and memory must be in range [0.1, 32]\n- Image: Set `image.imageRegistry` to `null` for public images, or provide credentials for private images\n- Ports: `http`/`grpc`/`ws` protocols support a public domain; `tcp`/`udp`/`sctp` use NodePort\n- Storage: Providing storage creates a StatefulSet instead of a Deployment\n\n**Example — minimal deployment (public image, fixed replicas):**\n```json\n{\n \"name\": \"web-api\",\n \"image\": {\n \"imageName\": \"nginx:1.21\",\n \"imageRegistry\": null\n },\n \"quota\": {\n \"cpu\": 0.5,\n \"memory\": 1,\n \"replicas\": 1\n },\n \"ports\": [\n { \"number\": 80, \"protocol\": \"http\", \"isPublic\": true }\n ]\n}\n```\n\n**Example — StatefulSet with persistent storage:**\n```json\n{\n \"name\": \"db-service\",\n \"image\": {\n \"imageName\": \"postgres:15\",\n \"imageRegistry\": null\n },\n \"quota\": {\n \"cpu\": 1,\n \"memory\": 2,\n \"replicas\": 1\n },\n \"ports\": [\n { \"number\": 5432, \"protocol\": \"tcp\" }\n ],\n \"storage\": [\n { \"path\": \"/var/lib/postgresql/data\", \"size\": \"20Gi\" }\n ],\n \"env\": [\n { \"key\": \"POSTGRES_PASSWORD\", \"value\": \"secret\" }\n ]\n}\n```\n\n**Example — HPA auto-scaling with private registry:**\n```json\n{\n \"name\": \"api-service\",\n \"image\": {\n \"imageName\": \"registry.example.com/myapp:v2\",\n \"imageRegistry\": {\n \"username\": \"robot$myproject\",\n \"password\": \"token\",\n \"apiUrl\": \"registry.example.com\"\n }\n },\n \"quota\": {\n \"cpu\": 1,\n \"memory\": 2,\n \"hpa\": {\n \"target\": \"cpu\",\n \"value\": 60,\n \"minReplicas\": 1,\n \"maxReplicas\": 5\n }\n },\n \"ports\": [\n { \"number\": 8080, \"protocol\": \"http\", \"isPublic\": true }\n ]\n}\n```", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "default": "hello-world", + "description": "Application name (must be unique) - was: appName" + }, + "image": { + "type": "object", + "properties": { + "imageName": { + "type": "string", + "description": "Docker image name with tag", + "example": "nginx:1.21" + }, + "imageRegistry": { + "type": [ + "object", + "null" + ], + "properties": { + "username": { + "type": "string", + "description": "Registry username", + "example": "robot$myproject" + }, + "password": { + "type": "string", + "description": "Registry password", + "example": "token123" + }, + "serverAddress": { + "type": "string", + "description": "Registry server address", + "example": "registry.example.com" + } + }, + "required": [ + "username", + "password", + "serverAddress" + ], + "description": "Image pull secret configuration. Set to null to switch from private to public image." + } + }, + "required": [ + "imageName" + ], + "description": "Container image configuration", + "default": { + "imageName": "nginx" + } + }, + "launchCommand": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "Container run command - was: runCMD", + "example": "node" + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Container command arguments (each element is a separate arg)", + "example": [ + "--config", + "/etc/app/config.yaml" + ] + } + }, + "description": "Container launch command configuration", + "default": {} + }, + "quota": { + "type": "object", + "properties": { + "replicas": { + "type": "number", + "description": "Number of pod replicas (for fixed instances). Cannot be used with hpa.", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20 + ], + "example": 2 + }, + "cpu": { + "type": "number", + "minimum": 0.1, + "maximum": 32, + "default": 0.2, + "description": "CPU allocation in cores - range [0.1, 32]", + "example": 0.5 + }, + "memory": { + "type": "number", + "minimum": 0.1, + "maximum": 32, + "default": 0.5, + "description": "Memory allocation in GB - range [0.1, 32]", + "example": 1 + }, + "gpu": { + "type": "object", + "properties": { + "vendor": { + "type": "string", + "default": "nvidia", + "description": "GPU vendor (e.g., nvidia)", + "example": "nvidia" + }, + "type": { + "type": "string", + "description": "GPU model (e.g., A100, V100, T4)", + "example": "A100" + }, + "amount": { + "type": "number", + "default": 1, + "description": "Number of GPUs to allocate", + "example": 1 + } + }, + "required": [ + "type" + ], + "description": "GPU resource configuration" + }, + "hpa": { + "type": "object", + "properties": { + "target": { + "type": "string", + "enum": [ + "cpu", + "memory", + "gpu" + ], + "description": "Resource metric to scale on" + }, + "value": { + "type": "number", + "description": "Target resource utilization percentage" + }, + "minReplicas": { + "type": "number", + "description": "Minimum number of replicas" + }, + "maxReplicas": { + "type": "number", + "description": "Maximum number of replicas" + } + }, + "required": [ + "target", + "value", + "minReplicas", + "maxReplicas" + ], + "description": "Horizontal Pod Autoscaler configuration. If present, enables elastic scaling; if absent, uses fixed replicas." + } + } + }, + "ports": { + "type": "array", + "items": { + "type": "object", + "properties": { + "number": { + "type": "number", + "minimum": 1, + "maximum": 65535, + "description": "Port number (1-65535)" + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "grpc", + "ws", + "tcp", + "udp", + "sctp" + ], + "default": "http", + "description": "Network protocol. http/grpc/ws enable public domain access, tcp/udp/sctp enable NodePort" + }, + "isPublic": { + "type": "boolean", + "default": true, + "description": "Whether to expose this port via public domain (only effective for http/grpc/ws protocols)" + } + }, + "required": [ + "number" + ], + "description": "Port configuration for creating applications" + }, + "default": [ + { + "number": 80, + "protocol": "http", + "isPublic": true + } + ], + "description": "Port/Network configurations for the application" + }, + "env": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Environment variable name", + "example": "DATABASE_URL" + }, + "value": { + "type": "string", + "description": "Environment variable value", + "example": "postgres://user:pass@host:5432/db" + }, + "valueFrom": { + "type": "object", + "properties": { + "secretKeyRef": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Key within the secret", + "example": "database-url" + }, + "name": { + "type": "string", + "description": "Name of the Kubernetes Secret", + "example": "my-app-secrets" + } + }, + "required": [ + "key", + "name" + ] + } + }, + "required": [ + "secretKeyRef" + ], + "description": "Reference to secret for the value" + } + }, + "required": [ + "name" + ], + "description": "Environment variable configuration" + }, + "default": [], + "description": "Environment variables - was: envs" + }, + "storage": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Persistent volume name", + "example": "data" + }, + "path": { + "type": "string", + "description": "Mount path in the container", + "example": "/var/data" + }, + "size": { + "type": "string", + "default": "1Gi", + "description": "Storage size (e.g., \"10Gi\", \"1Ti\")", + "example": "10Gi" + } + }, + "required": [ + "name", + "path" + ], + "description": "Persistent storage configuration" + }, + "default": [], + "description": "Storage configurations - was: storeList" + }, + "configMap": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Mount path in the container" + }, + "value": { + "type": "string", + "description": "Configuration value or file content" + } + }, + "required": [ + "path" + ], + "description": "ConfigMap configuration" + }, + "default": [], + "description": "ConfigMap configurations - was: configMapList" + } + }, + "required": [ + "quota" + ], + "title": "Create Launchpad Application", + "description": "Complete application creation schema based on AppEditType with standardized field names" + } + } + } + }, + "responses": { + "201": { + "description": "Application created successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Application name", + "example": "web-api" + }, + "image": { + "type": "object", + "properties": { + "imageName": { + "type": "string", + "description": "Docker image name with tag", + "example": "nginx:1.21" + }, + "imageRegistry": { + "type": [ + "object", + "null" + ], + "properties": { + "username": { + "type": "string", + "description": "Registry username", + "example": "robot$myproject" + }, + "password": { + "type": "string", + "description": "Registry password", + "example": "token123" + }, + "serverAddress": { + "type": "string", + "description": "Registry server address", + "example": "registry.example.com" + } + }, + "required": [ + "username", + "password", + "serverAddress" + ], + "description": "Image pull secret configuration. Set to null to switch from private to public image." + } + }, + "required": [ + "imageName" + ], + "description": "Container image configuration" + }, + "launchCommand": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "Container run command - was: runCMD", + "example": "node" + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Container command arguments (each element is a separate arg)", + "example": [ + "--config", + "/etc/app/config.yaml" + ] + } + }, + "description": "Container launch command configuration" + }, + "quota": { + "type": "object", + "properties": { + "replicas": { + "type": "number", + "description": "Number of pod replicas (for fixed instances). Cannot be used with hpa.", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20 + ], + "example": 2 + }, + "cpu": { + "type": "number", + "minimum": 0.1, + "maximum": 32, + "default": 0.2, + "description": "CPU allocation in cores - range [0.1, 32]", + "example": 0.5 + }, + "memory": { + "type": "number", + "minimum": 0.1, + "maximum": 32, + "default": 0.5, + "description": "Memory allocation in GB - range [0.1, 32]", + "example": 1 + }, + "gpu": { + "type": "object", + "properties": { + "vendor": { + "type": "string", + "default": "nvidia", + "description": "GPU vendor (e.g., nvidia)", + "example": "nvidia" + }, + "type": { + "type": "string", + "description": "GPU model (e.g., A100, V100, T4)", + "example": "A100" + }, + "amount": { + "type": "number", + "default": 1, + "description": "Number of GPUs to allocate", + "example": 1 + } + }, + "required": [ + "vendor", + "type", + "amount" + ], + "description": "GPU resource configuration" + }, + "hpa": { + "type": "object", + "properties": { + "target": { + "type": "string", + "enum": [ + "cpu", + "memory", + "gpu" + ], + "description": "Resource metric to scale on" + }, + "value": { + "type": "number", + "description": "Target resource utilization percentage" + }, + "minReplicas": { + "type": "number", + "description": "Minimum number of replicas" + }, + "maxReplicas": { + "type": "number", + "description": "Maximum number of replicas" + } + }, + "required": [ + "target", + "value", + "minReplicas", + "maxReplicas" + ], + "description": "Horizontal Pod Autoscaler configuration. If present, enables elastic scaling; if absent, uses fixed replicas." + } + }, + "required": [ + "cpu", + "memory" + ], + "description": "Resource quota and scaling configuration" + }, + "ports": { + "type": "array", + "items": { + "type": "object", + "properties": { + "number": { + "type": "number", + "description": "Port number", + "example": 8080 + }, + "portName": { + "type": "string", + "description": "Port name identifier", + "example": "abcdef123456" + }, + "protocol": { + "type": "string", + "description": "Protocol type (http, grpc, ws)", + "example": "http" + }, + "privateAddress": { + "type": "string", + "description": "Private access address", + "example": "http://my-app.ns-user123:8080" + }, + "publicAddress": { + "type": "string", + "description": "Public access address", + "example": "https://xyz789.cloud.sealos.io" + }, + "customDomain": { + "type": "string", + "description": "Custom domain (if configured)", + "example": "api.example.com" + } + }, + "required": [ + "number" + ], + "description": "Port configuration details" + }, + "description": "Port/network configurations with runtime addresses" + }, + "env": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Environment variable name", + "example": "DATABASE_URL" + }, + "value": { + "type": "string", + "description": "Environment variable value", + "example": "postgres://user:pass@host:5432/db" + }, + "valueFrom": { + "type": "object", + "properties": { + "secretKeyRef": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Key within the secret", + "example": "database-url" + }, + "name": { + "type": "string", + "description": "Name of the Kubernetes Secret", + "example": "my-app-secrets" + } + }, + "required": [ + "key", + "name" + ] + } + }, + "required": [ + "secretKeyRef" + ], + "description": "Reference to secret for the value" + } + }, + "required": [ + "name" + ], + "description": "Environment variable configuration" + }, + "description": "Environment variables" + }, + "storage": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Persistent volume name", + "example": "data" + }, + "path": { + "type": "string", + "description": "Mount path in the container", + "example": "/var/data" + }, + "size": { + "type": "string", + "default": "1Gi", + "description": "Storage size (e.g., \"10Gi\", \"1Ti\")", + "example": "10Gi" + } + }, + "required": [ + "name", + "path", + "size" + ], + "description": "Persistent storage configuration" + }, + "description": "Persistent storage volumes" + }, + "configMap": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "ConfigMap name" + }, + "path": { + "type": "string", + "description": "Mount path in the container" + }, + "key": { + "type": "string", + "description": "Specific key within the configmap" + }, + "value": { + "type": "string", + "description": "Configuration value or file content" + } + }, + "required": [ + "name", + "path" + ], + "description": "ConfigMap configuration" + }, + "description": "ConfigMap mounts" + }, + "resourceType": { + "type": "string", + "const": "launchpad", + "description": "Resource type identifier (fixed for v2alpha)", + "example": "launchpad" + }, + "kind": { + "type": "string", + "enum": [ + "deployment", + "statefulset" + ], + "description": "Underlying Kubernetes workload kind", + "example": "deployment" + }, + "uid": { + "type": "string", + "description": "Application UID", + "example": "abc123-def456-789" + }, + "createdAt": { + "type": "string", + "description": "Creation time", + "example": "2024-01-01T00:00:00Z" + }, + "upTime": { + "type": "string", + "description": "Uptime (running duration)", + "example": "2h15m" + }, + "status": { + "type": "string", + "enum": [ + "running", + "creating", + "waiting", + "error", + "pause" + ], + "description": "Application status", + "example": "running" + } + }, + "required": [ + "name", + "image", + "quota", + "resourceType", + "uid", + "createdAt", + "upTime", + "status" + ], + "title": "Launchpad Application", + "description": "Launchpad application schema for GET responses (CreateRequest + metadata)" + }, + "examples": { + "created": { + "summary": "Newly created application", + "value": { + "name": "web-api", + "resourceType": "launchpad", + "kind": "deployment", + "image": { + "imageName": "nginx:1.21", + "imageRegistry": null + }, + "quota": { + "cpu": 0.5, + "memory": 1, + "replicas": 1 + }, + "ports": [ + { + "number": 80, + "portName": "abcdef123456", + "protocol": "http", + "privateAddress": "http://web-api-80-xyz-service.ns-user123:80", + "publicAddress": "https://xyz789abc123.cloud.sealos.io", + "customDomain": "" + } + ], + "uid": "app-12345", + "createdAt": "2024-01-01T00:00:00Z", + "upTime": "0s", + "status": "creating" + } + } + } + } + } + }, + "400": { + "description": "Bad Request - Invalid request body or parameters", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "validation_error" + ], + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "INVALID_PARAMETER" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "example": "ports[0].number" + }, + "message": { + "type": "string", + "example": "Required" + } + }, + "required": [ + "field", + "message" + ] + } + }, + { + "type": "string" + } + ], + "description": "For INVALID_PARAMETER: Array<{ field: string, message: string }>. For INVALID_VALUE: optional string. Omitted for other codes.", + "example": [ + { + "field": "image.imageName", + "message": "Required" + } + ] + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 400 - create app" + }, + "examples": { + "invalidParameter": { + "summary": "Request body validation failed", + "value": { + "error": { + "type": "validation_error", + "code": "INVALID_PARAMETER", + "message": "Request body validation failed. Please check the application configuration format.", + "details": [ + { + "field": "image.imageName", + "message": "Required" + } + ] + } + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Missing or invalid kubeconfig", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authentication_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "AUTHENTICATION_REQUIRED", + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 401" + }, + "examples": { + "missingAuth": { + "summary": "Missing authentication", + "value": { + "error": { + "type": "authentication_error", + "code": "AUTHENTICATION_REQUIRED", + "message": "Authentication required. Please provide valid credentials in the Authorization header." + } + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authorization_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "PERMISSION_DENIED" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 403 - create app" + }, + "examples": { + "insufficientPermissions": { + "summary": "Insufficient permissions", + "value": { + "error": { + "type": "authorization_error", + "code": "PERMISSION_DENIED", + "message": "Insufficient permissions to perform this operation. Please check your access rights." + } + } + } + } + } + } + }, + "409": { + "description": "Conflict - Application already exists", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "ALREADY_EXISTS" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "anyOf": [ + { + "type": "object", + "additionalProperties": {} + }, + { + "type": "string" + } + ], + "description": "Ad-hoc context. For CONFLICT: object with conflict details (e.g. conflictingPortDetails, operation)." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 409" + }, + "examples": { + "alreadyExists": { + "summary": "Application already exists", + "value": { + "error": { + "type": "resource_error", + "code": "ALREADY_EXISTS", + "message": "An application with this name already exists in the current namespace. Use a different name." + } + } + } + } + } + } + }, + "422": { + "description": "Unprocessable Entity - Resource spec rejected by cluster (admission webhook, invalid field, quota exceeded)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "operation_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "INVALID_RESOURCE_SPEC", + "description": "Resource spec rejected by cluster (admission webhook, invalid field, quota exceeded)" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Raw K8s rejection reason from the cluster", + "example": "admission webhook \"vingress.sealos.io\" denied the request: cannot verify host" + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 422 - create app" + }, + "examples": { + "invalidResourceSpec": { + "summary": "Resource spec rejected", + "value": { + "error": { + "type": "operation_error", + "code": "INVALID_RESOURCE_SPEC", + "message": "The resource specification was rejected by the cluster. Check admission webhooks, field constraints, and quota limits.", + "details": "admission webhook \"vingress.sealos.io\" denied the request: cannot verify host" + } + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "operation_error", + "internal_error" + ], + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "KUBERNETES_ERROR", + "OPERATION_FAILED", + "INTERNAL_ERROR" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Raw error string from the underlying system, for troubleshooting.", + "example": "deployments.apps \"web-api\" not found" + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 500" + }, + "examples": { + "kubernetesError": { + "summary": "Kubernetes API error", + "value": { + "error": { + "type": "operation_error", + "code": "KUBERNETES_ERROR", + "message": "Failed to create application in Kubernetes cluster. Please check cluster status and permissions.", + "details": "namespaces \"ns-xxx\" not found" + } + } + }, + "initFailure": { + "summary": "Kubernetes context initialization failed", + "value": { + "error": { + "type": "internal_error", + "code": "INTERNAL_ERROR", + "message": "Failed to initialize Kubernetes context. Please check your configuration and try again.", + "details": "invalid kubeconfig format" + } + } + }, + "unexpectedError": { + "summary": "Unexpected server error", + "value": { + "error": { + "type": "internal_error", + "code": "INTERNAL_ERROR", + "message": "An unexpected internal error occurred while processing your request. Please try again or contact support if the issue persists.", + "details": "TypeError: Cannot read properties of undefined" + } + } + } + } + } + } + }, + "503": { + "description": "Service Unavailable - Kubernetes cluster temporarily unreachable", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "internal_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "SERVICE_UNAVAILABLE", + "description": "Kubernetes cluster or required dependency is temporarily unreachable" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Connection error from the underlying system", + "example": "connect ECONNREFUSED 10.0.0.1:6443" + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 503 - create app" + }, + "examples": { + "serviceUnavailable": { + "summary": "Cluster unreachable", + "value": { + "error": { + "type": "internal_error", + "code": "SERVICE_UNAVAILABLE", + "message": "Kubernetes cluster is temporarily unavailable. Please try again later.", + "details": "connect ECONNREFUSED 10.0.0.1:6443" + } + } + } + } + } + } + } + } + } + }, + "/apps/{name}": { + "get": { + "tags": [ + "Query" + ], + "operationId": "getApp", + "summary": "Get application details by name", + "description": "Retrieves complete application configuration and current runtime status.\n\nThe response includes all fields needed to inspect or reproduce the application: image, resource quota, ports (with private and public addresses), environment variables, ConfigMap, storage, and current status.", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Application name (Kubernetes resource name, must be a valid DNS subdomain: lowercase alphanumeric, hyphens)", + "required": true, + "example": "web-api", + "schema": { + "type": "string", + "minLength": 1, + "example": "web-api" + } + } + ], + "responses": { + "200": { + "description": "Application details retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Application name", + "example": "web-api" + }, + "image": { + "type": "object", + "properties": { + "imageName": { + "type": "string", + "description": "Docker image name with tag", + "example": "nginx:1.21" + }, + "imageRegistry": { + "type": [ + "object", + "null" + ], + "properties": { + "username": { + "type": "string", + "description": "Registry username", + "example": "robot$myproject" + }, + "password": { + "type": "string", + "description": "Registry password", + "example": "token123" + }, + "serverAddress": { + "type": "string", + "description": "Registry server address", + "example": "registry.example.com" + } + }, + "required": [ + "username", + "password", + "serverAddress" + ], + "description": "Image pull secret configuration. Set to null to switch from private to public image." + } + }, + "required": [ + "imageName" + ], + "description": "Container image configuration" + }, + "launchCommand": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "Container run command - was: runCMD", + "example": "node" + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Container command arguments (each element is a separate arg)", + "example": [ + "--config", + "/etc/app/config.yaml" + ] + } + }, + "description": "Container launch command configuration" + }, + "quota": { + "type": "object", + "properties": { + "replicas": { + "type": "number", + "description": "Number of pod replicas (for fixed instances). Cannot be used with hpa.", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20 + ], + "example": 2 + }, + "cpu": { + "type": "number", + "minimum": 0.1, + "maximum": 32, + "default": 0.2, + "description": "CPU allocation in cores - range [0.1, 32]", + "example": 0.5 + }, + "memory": { + "type": "number", + "minimum": 0.1, + "maximum": 32, + "default": 0.5, + "description": "Memory allocation in GB - range [0.1, 32]", + "example": 1 + }, + "gpu": { + "type": "object", + "properties": { + "vendor": { + "type": "string", + "default": "nvidia", + "description": "GPU vendor (e.g., nvidia)", + "example": "nvidia" + }, + "type": { + "type": "string", + "description": "GPU model (e.g., A100, V100, T4)", + "example": "A100" + }, + "amount": { + "type": "number", + "default": 1, + "description": "Number of GPUs to allocate", + "example": 1 + } + }, + "required": [ + "vendor", + "type", + "amount" + ], + "description": "GPU resource configuration" + }, + "hpa": { + "type": "object", + "properties": { + "target": { + "type": "string", + "enum": [ + "cpu", + "memory", + "gpu" + ], + "description": "Resource metric to scale on" + }, + "value": { + "type": "number", + "description": "Target resource utilization percentage" + }, + "minReplicas": { + "type": "number", + "description": "Minimum number of replicas" + }, + "maxReplicas": { + "type": "number", + "description": "Maximum number of replicas" + } + }, + "required": [ + "target", + "value", + "minReplicas", + "maxReplicas" + ], + "description": "Horizontal Pod Autoscaler configuration. If present, enables elastic scaling; if absent, uses fixed replicas." + } + }, + "required": [ + "cpu", + "memory" + ], + "description": "Resource quota and scaling configuration" + }, + "ports": { + "type": "array", + "items": { + "type": "object", + "properties": { + "number": { + "type": "number", + "description": "Port number", + "example": 8080 + }, + "portName": { + "type": "string", + "description": "Port name identifier", + "example": "abcdef123456" + }, + "protocol": { + "type": "string", + "description": "Protocol type (http, grpc, ws)", + "example": "http" + }, + "privateAddress": { + "type": "string", + "description": "Private access address", + "example": "http://my-app.ns-user123:8080" + }, + "publicAddress": { + "type": "string", + "description": "Public access address", + "example": "https://xyz789.cloud.sealos.io" + }, + "customDomain": { + "type": "string", + "description": "Custom domain (if configured)", + "example": "api.example.com" + } + }, + "required": [ + "number" + ], + "description": "Port configuration details" + }, + "description": "Port/network configurations with runtime addresses" + }, + "env": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Environment variable name", + "example": "DATABASE_URL" + }, + "value": { + "type": "string", + "description": "Environment variable value", + "example": "postgres://user:pass@host:5432/db" + }, + "valueFrom": { + "type": "object", + "properties": { + "secretKeyRef": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Key within the secret", + "example": "database-url" + }, + "name": { + "type": "string", + "description": "Name of the Kubernetes Secret", + "example": "my-app-secrets" + } + }, + "required": [ + "key", + "name" + ] + } + }, + "required": [ + "secretKeyRef" + ], + "description": "Reference to secret for the value" + } + }, + "required": [ + "name" + ], + "description": "Environment variable configuration" + }, + "description": "Environment variables" + }, + "storage": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Persistent volume name", + "example": "data" + }, + "path": { + "type": "string", + "description": "Mount path in the container", + "example": "/var/data" + }, + "size": { + "type": "string", + "default": "1Gi", + "description": "Storage size (e.g., \"10Gi\", \"1Ti\")", + "example": "10Gi" + } + }, + "required": [ + "name", + "path", + "size" + ], + "description": "Persistent storage configuration" + }, + "description": "Persistent storage volumes" + }, + "configMap": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "ConfigMap name" + }, + "path": { + "type": "string", + "description": "Mount path in the container" + }, + "key": { + "type": "string", + "description": "Specific key within the configmap" + }, + "value": { + "type": "string", + "description": "Configuration value or file content" + } + }, + "required": [ + "name", + "path" + ], + "description": "ConfigMap configuration" + }, + "description": "ConfigMap mounts" + }, + "resourceType": { + "type": "string", + "const": "launchpad", + "description": "Resource type identifier (fixed for v2alpha)", + "example": "launchpad" + }, + "kind": { + "type": "string", + "enum": [ + "deployment", + "statefulset" + ], + "description": "Underlying Kubernetes workload kind", + "example": "deployment" + }, + "uid": { + "type": "string", + "description": "Application UID", + "example": "abc123-def456-789" + }, + "createdAt": { + "type": "string", + "description": "Creation time", + "example": "2024-01-01T00:00:00Z" + }, + "upTime": { + "type": "string", + "description": "Uptime (running duration)", + "example": "2h15m" + }, + "status": { + "type": "string", + "enum": [ + "running", + "creating", + "waiting", + "error", + "pause" + ], + "description": "Application status", + "example": "running" + } + }, + "required": [ + "name", + "image", + "quota", + "resourceType", + "uid", + "createdAt", + "upTime", + "status" + ], + "title": "Launchpad Application", + "description": "Launchpad application schema for GET responses (CreateRequest + metadata)" + }, + "examples": { + "deploymentApp": { + "summary": "Deployment with fixed replicas", + "value": { + "name": "web-api", + "resourceType": "launchpad", + "kind": "deployment", + "image": { + "imageName": "nginx:1.21", + "imageRegistry": null + }, + "quota": { + "cpu": 1.5, + "memory": 3, + "replicas": 3 + }, + "ports": [ + { + "number": 80, + "portName": "abcdef123456", + "protocol": "http", + "privateAddress": "http://web-api.ns-user123:80", + "publicAddress": "https://xyz789abc123.cloud.sealos.io", + "customDomain": "" + } + ], + "uid": "app-12345", + "createdAt": "2024-01-01T00:00:00Z", + "upTime": "2h15m", + "status": "running" + } + } + } + } + } + }, + "400": { + "description": "Bad Request - Invalid path parameters", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "validation_error" + ], + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "INVALID_PARAMETER" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "example": "ports[0].number" + }, + "message": { + "type": "string", + "example": "Required" + } + }, + "required": [ + "field", + "message" + ] + } + }, + { + "type": "string" + } + ], + "description": "For INVALID_PARAMETER: Array<{ field: string, message: string }>. For INVALID_VALUE: optional string. Omitted for other codes.", + "example": [ + { + "field": "image.imageName", + "message": "Required" + } + ] + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 400 - get app" + }, + "examples": { + "invalidName": { + "summary": "Invalid application name format", + "value": { + "error": { + "type": "validation_error", + "code": "INVALID_PARAMETER", + "message": "Application name path parameter is invalid or missing.", + "details": [ + { + "field": "name", + "message": "name cannot be empty" + } + ] + } + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Missing or invalid kubeconfig", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authentication_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "AUTHENTICATION_REQUIRED", + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 401" + }, + "examples": { + "missingAuth": { + "summary": "Missing authentication", + "value": { + "error": { + "type": "authentication_error", + "code": "AUTHENTICATION_REQUIRED", + "message": "Authentication required. Please provide valid credentials in the Authorization header." + } + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authorization_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "PERMISSION_DENIED" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 403 - get app" + }, + "examples": { + "insufficientPermissions": { + "summary": "Insufficient permissions", + "value": { + "error": { + "type": "authorization_error", + "code": "PERMISSION_DENIED", + "message": "Insufficient permissions to perform this operation. Please check your access rights." + } + } + } + } + } + } + }, + "404": { + "description": "Not Found - Application not found in the current namespace", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "NOT_FOUND", + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 404" + }, + "examples": { + "notFound": { + "summary": "Application not found", + "value": { + "error": { + "type": "resource_error", + "code": "NOT_FOUND", + "message": "Application \"web-api\" not found in the current namespace. Please verify the application name." + } + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error - Kubernetes API error or unexpected failure", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "operation_error", + "internal_error" + ], + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "KUBERNETES_ERROR", + "OPERATION_FAILED", + "INTERNAL_ERROR" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Raw error string from the underlying system, for troubleshooting.", + "example": "deployments.apps \"web-api\" not found" + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 500" + }, + "examples": { + "kubernetesError": { + "summary": "Kubernetes API error", + "value": { + "error": { + "type": "operation_error", + "code": "KUBERNETES_ERROR", + "message": "Failed to retrieve application \"web-api\". The Kubernetes operation encountered an error.", + "details": "deployments.apps \"web-api\" not found" + } + } + }, + "initFailure": { + "summary": "Kubernetes context initialization failed", + "value": { + "error": { + "type": "internal_error", + "code": "INTERNAL_ERROR", + "message": "Failed to initialize Kubernetes context. Please check your configuration and try again.", + "details": "invalid kubeconfig format" + } + } + }, + "unexpectedError": { + "summary": "Unexpected server error", + "value": { + "error": { + "type": "internal_error", + "code": "INTERNAL_ERROR", + "message": "An unexpected error occurred while processing your request. Please try again or contact support.", + "details": "TypeError: Cannot read properties of undefined" + } + } + } + } + } + } + }, + "503": { + "description": "Service Unavailable - Kubernetes cluster temporarily unreachable", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "internal_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "SERVICE_UNAVAILABLE", + "description": "Kubernetes cluster or required dependency is temporarily unreachable" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Connection error from the underlying system", + "example": "connect ECONNREFUSED 10.0.0.1:6443" + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 503" + }, + "examples": { + "clusterUnavailable": { + "summary": "Cluster unreachable", + "value": { + "error": { + "type": "internal_error", + "code": "SERVICE_UNAVAILABLE", + "message": "Kubernetes cluster is temporarily unavailable. Please try again later.", + "details": "connect ECONNREFUSED 10.0.0.1:6443" + } + } + } + } + } + } + } + } + }, + "patch": { + "tags": [ + "Mutation" + ], + "operationId": "updateApp", + "summary": "Update application configuration", + "description": "Partially updates application configuration. Supports quota, image, ports, env, storage, and ConfigMap.\n\nKey points:\n- Quota: Switch between fixed replicas and HPA by providing one and omitting the other. CPU must be one of: 0.1, 0.2, 0.5, 1, 2, 3, 4, 8 (cores); Memory must be one of: 0.1, 0.5, 1, 2, 4, 8, 16 (GB)\n- Image: Change image or switch public/private (set `imageRegistry` to `null` for public)\n- Ports: Complete replacement — include `portName` to update an existing port, omit to create, unlisted ports are deleted, use `[]` to remove all\n- ConfigMap: Complete replacement — provide all entries to keep, use `[]` to remove all, omit to keep unchanged\n- Storage: Complete replacement — provide all entries to keep, use `[]` to remove all, omit to keep unchanged\n- Storage: If the application is a Deployment, adding storage automatically converts it to a StatefulSet (brief downtime)\n- All changes are applied atomically\n\n**Example — scale up quota:**\n```json\n{\n \"quota\": {\n \"cpu\": 2,\n \"memory\": 4,\n \"replicas\": 3\n }\n}\n```\n\n**Example — switch from fixed replicas to HPA:**\n```json\n{\n \"quota\": {\n \"cpu\": 1,\n \"memory\": 2,\n \"hpa\": {\n \"target\": \"cpu\",\n \"value\": 70,\n \"minReplicas\": 1,\n \"maxReplicas\": 10\n }\n }\n}\n```\n\n**Example — update image:**\n```json\n{\n \"image\": {\n \"imageName\": \"nginx:1.25\",\n \"imageRegistry\": null\n }\n}\n```\n\n**Example — full port list replacement (update existing port + add new port):**\n```json\n{\n \"ports\": [\n { \"portName\": \"abcdef123456\", \"number\": 80, \"protocol\": \"http\", \"isPublic\": true },\n { \"number\": 9090, \"protocol\": \"http\", \"isPublic\": false }\n ]\n}\n```", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Application name (Kubernetes resource name, must be a valid DNS subdomain: lowercase alphanumeric, hyphens)", + "required": true, + "example": "web-api", + "schema": { + "type": "string", + "minLength": 1, + "example": "web-api" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "quota": { + "type": "object", + "properties": { + "cpu": { + "type": "number", + "description": "CPU allocation in cores", + "enum": [ + 0.1, + 0.2, + 0.5, + 1, + 2, + 3, + 4, + 8 + ] + }, + "memory": { + "type": "number", + "description": "Memory allocation in GB", + "enum": [ + 0.1, + 0.5, + 1, + 2, + 4, + 8, + 16 + ] + }, + "replicas": { + "type": "number", + "description": "Number of pod replicas (used for fixed instances). To switch from HPA to fixed replicas, set this field and omit hpa field.", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20 + ] + }, + "gpu": { + "type": "object", + "properties": { + "vendor": { + "type": "string", + "description": "GPU vendor (e.g., nvidia)" + }, + "type": { + "type": "string", + "description": "GPU type/model (e.g., V100, A100, T4)" + }, + "amount": { + "type": "number", + "description": "Number of GPUs to allocate" + } + }, + "description": "GPU resource configuration" + }, + "hpa": { + "type": "object", + "properties": { + "target": { + "type": "string", + "enum": [ + "cpu", + "memory", + "gpu" + ], + "description": "Resource metric to scale on" + }, + "value": { + "type": "number", + "description": "Target resource utilization percentage" + }, + "minReplicas": { + "type": "number", + "description": "Minimum number of replicas" + }, + "maxReplicas": { + "type": "number", + "description": "Maximum number of replicas" + } + }, + "required": [ + "target", + "value", + "minReplicas", + "maxReplicas" + ], + "description": "Horizontal Pod Autoscaler configuration. To switch from fixed replicas to HPA, set this field and omit replicas field." + } + }, + "description": "Quota configuration including HPA. For mode switching: provide either replicas (fixed) or hpa (elastic), not both." + }, + "launchCommand": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "Container run command - was: runCMD", + "example": "node" + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Container command arguments (each element is a separate arg)", + "example": [ + "--config", + "/etc/app/config.yaml" + ] + } + }, + "description": "Container launch command configuration" + }, + "image": { + "type": "object", + "properties": { + "imageName": { + "type": "string", + "description": "Docker image name with tag" + }, + "imageRegistry": { + "anyOf": [ + { + "type": "object", + "properties": { + "username": { + "type": "string", + "description": "Registry username" + }, + "password": { + "type": "string", + "description": "Registry password" + }, + "apiUrl": { + "type": "string", + "description": "Registry API URL" + } + }, + "required": [ + "username", + "password", + "apiUrl" + ] + }, + { + "type": "null" + } + ], + "description": "Image pull secret configuration. Set to null to switch to public image." + } + }, + "required": [ + "imageName" + ], + "description": "Container image configuration. Set imageRegistry to null to switch to public image." + }, + "env": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Environment variable name", + "example": "DATABASE_URL" + }, + "value": { + "type": "string", + "description": "Environment variable value", + "example": "postgres://user:pass@host:5432/db" + }, + "valueFrom": { + "type": "object", + "properties": { + "secretKeyRef": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Key within the secret", + "example": "database-url" + }, + "name": { + "type": "string", + "description": "Name of the Kubernetes Secret", + "example": "my-app-secrets" + } + }, + "required": [ + "key", + "name" + ] + } + }, + "required": [ + "secretKeyRef" + ], + "description": "Reference to secret for the value" + } + }, + "required": [ + "name" + ], + "description": "Environment variable configuration" + }, + "description": "Environment variables" + }, + "configMap": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Mount path in the container" + }, + "value": { + "type": "string", + "description": "Configuration value or file content" + } + }, + "required": [ + "path" + ], + "description": "ConfigMap configuration" + }, + "description": "ConfigMap configurations (COMPLETE REPLACEMENT). Pass all ConfigMap entries to keep. Empty array removes all ConfigMaps. Omit field to keep existing ConfigMaps unchanged." + }, + "storage": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Mount path in the container" + }, + "size": { + "type": "string", + "default": "1Gi", + "description": "Storage size (e.g., \"10Gi\", \"1Ti\")" + } + }, + "required": [ + "path" + ], + "description": "Simplified storage configuration (name auto-generated from path)" + }, + "description": "Storage configurations (COMPLETE REPLACEMENT). Pass all storage entries to keep. Empty array removes all storage. Omit field to keep existing storage unchanged. Name is auto-generated from path." + }, + "ports": { + "type": "array", + "items": { + "type": "object", + "properties": { + "number": { + "type": "number", + "minimum": 1, + "maximum": 65535, + "description": "Port number (1-65535) - required for new ports, optional for updates" + }, + "protocol": { + "type": "string", + "enum": [ + "http", + "grpc", + "ws", + "tcp", + "udp", + "sctp" + ], + "description": "Network protocol. http/grpc/ws enable public domain access, tcp/udp/sctp enable NodePort" + }, + "isPublic": { + "type": "boolean", + "description": "Whether to expose this port via public domain (only effective for http/grpc/ws protocols)" + }, + "portName": { + "type": "string", + "description": "Port name (include this to update existing port, omit to create new port)" + } + }, + "description": "Port configuration. Include portName to update existing port. Omit portName to create new port. Ports not included in the array will be deleted." + }, + "description": "Port configurations (COMPLETE REPLACEMENT). Include portName to update existing port, omit portName to create new port. Ports not included will be deleted. Pass empty array [] to remove all ports." + } + }, + "title": "Update Application Configuration", + "description": "Schema for updating application configuration. IMPORTANT: configMap, storage, and ports are COMPLETE REPLACEMENTS when provided - pass all items you want to keep. Use empty array [] to remove all items." + } + } + } + }, + "responses": { + "204": { + "description": "Application updated successfully" + }, + "400": { + "description": "Bad Request - Invalid request body or port validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "validation_error" + ], + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "INVALID_PARAMETER" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "example": "ports[0].number" + }, + "message": { + "type": "string", + "example": "Required" + } + }, + "required": [ + "field", + "message" + ] + } + }, + { + "type": "string" + } + ], + "description": "For INVALID_PARAMETER: Array<{ field: string, message: string }>. For INVALID_VALUE: optional string. Omitted for other codes.", + "example": [ + { + "field": "image.imageName", + "message": "Required" + } + ] + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 400 - patch app" + }, + "examples": { + "validationFailed": { + "summary": "Request body validation failed", + "value": { + "error": { + "type": "validation_error", + "code": "INVALID_PARAMETER", + "message": "Request body validation failed. Please check the update configuration format.", + "details": [ + { + "field": "quota", + "message": "Must specify either replicas (for fixed instances) or hpa (for elastic scaling), but not both." + } + ] + } + } + }, + "portNumberRequired": { + "summary": "Port number required for creating new ports", + "value": { + "error": { + "type": "validation_error", + "code": "INVALID_PARAMETER", + "message": "Port number is required for creating new ports", + "details": { + "portConfig": {}, + "operation": "CREATE_PORT_VALIDATION" + } + } + } + }, + "isPublicNonAppProtocol": { + "summary": "Cannot set isPublic for non-application protocol", + "value": { + "error": { + "type": "validation_error", + "code": "INVALID_PARAMETER", + "message": "Cannot set isPublic for non-application protocol. Current protocol: TCP", + "details": { + "currentProtocol": "TCP", + "supportedProtocols": [ + "HTTP", + "GRPC", + "WS" + ], + "operation": "UPDATE_PUBLIC_DOMAIN" + } + } + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Missing or invalid kubeconfig", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authentication_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "AUTHENTICATION_REQUIRED", + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 401" + }, + "examples": { + "missingAuth": { + "summary": "Missing authentication", + "value": { + "error": { + "type": "authentication_error", + "code": "AUTHENTICATION_REQUIRED", + "message": "Authentication required. Please provide valid credentials in the Authorization header." + } + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authorization_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "PERMISSION_DENIED" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 403 - patch app" + }, + "examples": { + "insufficientPermissions": { + "summary": "Insufficient permissions", + "value": { + "error": { + "type": "authorization_error", + "code": "PERMISSION_DENIED", + "message": "Insufficient permissions to perform this operation. Please check your access rights." + } + } + } + } + } + } + }, + "404": { + "description": "Not Found - Application not found or port not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "NOT_FOUND", + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 404" + }, + "examples": { + "appNotFound": { + "summary": "Application not found", + "value": { + "error": { + "type": "resource_error", + "code": "NOT_FOUND", + "message": "Application \"web-api\" not found in the current namespace. Please verify the application name." + } + } + }, + "portNotFound": { + "summary": "Port not found by portName", + "value": { + "error": { + "type": "resource_error", + "code": "NOT_FOUND", + "message": "Port \"nonexistent-port-id\" not found in application \"web-api\". Verify the portName or omit it to create a new port.", + "details": { + "portName": "nonexistent-port-id", + "operation": "UPDATE_PORT_NOT_FOUND" + } + } + } + } + } + } + } + }, + "409": { + "description": "Conflict - Port conflict", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "CONFLICT" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "anyOf": [ + { + "type": "object", + "additionalProperties": {} + }, + { + "type": "string" + } + ], + "description": "Ad-hoc context. For CONFLICT: object with conflict details (e.g. conflictingPortDetails, operation)." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 409" + }, + "examples": { + "portUpdateConflict": { + "summary": "Port update conflicts with existing port", + "value": { + "error": { + "type": "resource_error", + "code": "CONFLICT", + "message": "Cannot update to port 8080: already in use by another port", + "details": { + "conflictingPortDetails": { + "port": 8080, + "portName": "abcdef123456", + "serviceName": "web-api-8080-xyz-service" + }, + "requestedPort": 8080, + "operation": "UPDATE_PORT_CONFLICT" + } + } + } + }, + "portCreateConflict": { + "summary": "New port conflicts with existing port", + "value": { + "error": { + "type": "resource_error", + "code": "CONFLICT", + "message": "Cannot create port 80: already exists", + "details": { + "existingPortDetails": { + "port": 80, + "portName": "abcdef123456", + "serviceName": "web-api-80-xyz-service" + }, + "operation": "CREATE_PORT_CONFLICT" + } + } + } + }, + "portCreateDuplicate": { + "summary": "Duplicate port in same request", + "value": { + "error": { + "type": "resource_error", + "code": "CONFLICT", + "message": "Cannot create duplicate port 80", + "details": { + "operation": "CREATE_PORT_DUPLICATE" + } + } + } + } + } + } + } + }, + "422": { + "description": "Unprocessable Entity - Resource spec rejected by cluster (admission webhook, invalid field, quota exceeded)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "operation_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "INVALID_RESOURCE_SPEC", + "description": "Resource spec rejected by cluster (admission webhook, invalid field, quota exceeded)" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Raw K8s rejection reason from the cluster", + "example": "admission webhook \"vingress.sealos.io\" denied the request: cannot verify host" + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 422 - patch app" + }, + "examples": { + "invalidResourceSpec": { + "summary": "Resource spec rejected", + "value": { + "error": { + "type": "operation_error", + "code": "INVALID_RESOURCE_SPEC", + "message": "The resource specification was rejected by the cluster. Check admission webhooks, field constraints, and quota limits.", + "details": "admission webhook \"vingress.sealos.io\" denied the request: cannot verify host" + } + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error - Kubernetes operation error or unexpected failure", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "operation_error", + "internal_error" + ], + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "KUBERNETES_ERROR", + "OPERATION_FAILED", + "INTERNAL_ERROR" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Raw error string from the underlying system, for troubleshooting.", + "example": "deployments.apps \"web-api\" not found" + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 500" + }, + "examples": { + "kubernetesError": { + "summary": "Kubernetes operation failed", + "value": { + "error": { + "type": "operation_error", + "code": "KUBERNETES_ERROR", + "message": "Failed to update application \"web-api\". The Kubernetes operation encountered an error.", + "details": "Operation cannot be fulfilled on deployments.apps \"web-api\": the object has been modified" + } + } + }, + "initFailure": { + "summary": "Kubernetes context initialization failed", + "value": { + "error": { + "type": "internal_error", + "code": "INTERNAL_ERROR", + "message": "Failed to initialize Kubernetes context. Please check your configuration and try again.", + "details": "invalid kubeconfig format" + } + } + }, + "unexpectedError": { + "summary": "Unexpected server error", + "value": { + "error": { + "type": "internal_error", + "code": "INTERNAL_ERROR", + "message": "An unexpected error occurred while processing your request. Please try again or contact support.", + "details": "TypeError: Cannot read properties of undefined" + } + } + } + } + } + } + }, + "503": { + "description": "Service Unavailable - Kubernetes cluster temporarily unreachable", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "internal_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "SERVICE_UNAVAILABLE", + "description": "Kubernetes cluster or required dependency is temporarily unreachable" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Connection error from the underlying system", + "example": "connect ECONNREFUSED 10.0.0.1:6443" + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 503 - patch app" + }, + "examples": { + "clusterUnavailable": { + "summary": "Cluster unreachable", + "value": { + "error": { + "type": "internal_error", + "code": "SERVICE_UNAVAILABLE", + "message": "Kubernetes cluster is temporarily unavailable. Please try again later.", + "details": "connect ECONNREFUSED 10.0.0.1:6443" + } + } + } + } + } + } + } + } + }, + "delete": { + "tags": [ + "Mutation" + ], + "operationId": "deleteApp", + "summary": "Delete application", + "description": "Permanently deletes the application and all associated Kubernetes resources (Deployments/StatefulSets, Services, Ingresses, ConfigMaps, Secrets).\n\nKey points:\n- Pods are terminated gracefully (30s grace period)\n- For StatefulSet applications, PVCs are preserved by default\n- Idempotent: returns `204` even if the application does not exist\n- **Irreversible**", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Application name (Kubernetes resource name, must be a valid DNS subdomain: lowercase alphanumeric, hyphens)", + "required": true, + "example": "web-api", + "schema": { + "type": "string", + "minLength": 1, + "example": "web-api" + } + } + ], + "responses": { + "204": { + "description": "Application deleted successfully" + }, + "400": { + "description": "Bad Request - Invalid parameters", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "validation_error" + ], + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "INVALID_PARAMETER" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "example": "ports[0].number" + }, + "message": { + "type": "string", + "example": "Required" + } + }, + "required": [ + "field", + "message" + ] + } + }, + { + "type": "string" + } + ], + "description": "For INVALID_PARAMETER: Array<{ field: string, message: string }>. For INVALID_VALUE: optional string. Omitted for other codes.", + "example": [ + { + "field": "image.imageName", + "message": "Required" + } + ] + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 400 - delete" + }, + "examples": { + "invalidName": { + "summary": "Invalid application name", + "value": { + "error": { + "type": "validation_error", + "code": "INVALID_PARAMETER", + "message": "Application name path parameter is invalid or missing.", + "details": [ + { + "field": "name", + "message": "name cannot be empty" + } + ] + } + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Missing or invalid kubeconfig", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authentication_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "AUTHENTICATION_REQUIRED", + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 401" + }, + "examples": { + "missingAuth": { + "summary": "Missing authentication", + "value": { + "error": { + "type": "authentication_error", + "code": "AUTHENTICATION_REQUIRED", + "message": "Authentication required. Please provide valid credentials in the Authorization header." + } + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authorization_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "PERMISSION_DENIED" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 403 - delete" + }, + "examples": { + "insufficientPermissions": { + "summary": "Insufficient permissions", + "value": { + "error": { + "type": "authorization_error", + "code": "PERMISSION_DENIED", + "message": "Insufficient permissions to perform this operation. Please check your access rights." + } + } + } + } + } + } + }, + "422": { + "description": "Unprocessable Entity - Resource spec rejected by cluster", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "operation_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "INVALID_RESOURCE_SPEC", + "description": "Resource spec rejected by cluster (admission webhook, invalid field, quota exceeded)" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Raw K8s rejection reason from the cluster", + "example": "admission webhook \"vingress.sealos.io\" denied the request: cannot verify host" + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 422 - delete" + }, + "examples": { + "invalidResourceSpec": { + "summary": "Resource spec rejected", + "value": { + "error": { + "type": "operation_error", + "code": "INVALID_RESOURCE_SPEC", + "message": "The resource specification was rejected by the cluster. Check admission webhooks, field constraints, and quota limits.", + "details": "admission webhook denied the request" + } + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error - Kubernetes operation error or unexpected failure", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "operation_error", + "internal_error" + ], + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "KUBERNETES_ERROR", + "OPERATION_FAILED", + "INTERNAL_ERROR" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Raw error string from the underlying system, for troubleshooting.", + "example": "deployments.apps \"web-api\" not found" + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 500" + }, + "examples": { + "kubernetesError": { + "summary": "Kubernetes delete failed", + "value": { + "error": { + "type": "operation_error", + "code": "KUBERNETES_ERROR", + "message": "Failed to delete application \"web-api\". The Kubernetes operation encountered an error.", + "details": "Failed to delete some resources for application \"web-api\". Errors in: [Deployment, StatefulSet]. First error: deployments.apps \"web-api\" not found" + } + } + }, + "initFailure": { + "summary": "Kubernetes context initialization failed", + "value": { + "error": { + "type": "internal_error", + "code": "INTERNAL_ERROR", + "message": "Failed to initialize Kubernetes context. Please check your configuration and try again.", + "details": "invalid kubeconfig format" + } + } + }, + "unexpectedError": { + "summary": "Unexpected server error", + "value": { + "error": { + "type": "internal_error", + "code": "INTERNAL_ERROR", + "message": "An unexpected error occurred while processing your request. Please try again or contact support.", + "details": "TypeError: Cannot read properties of undefined" + } + } + } + } + } + } + }, + "503": { + "description": "Service Unavailable - Kubernetes cluster temporarily unreachable", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "internal_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "SERVICE_UNAVAILABLE", + "description": "Kubernetes cluster or required dependency is temporarily unreachable" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Connection error from the underlying system", + "example": "connect ECONNREFUSED 10.0.0.1:6443" + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 503 - delete" + }, + "examples": { + "clusterUnavailable": { + "summary": "Cluster unreachable", + "value": { + "error": { + "type": "internal_error", + "code": "SERVICE_UNAVAILABLE", + "message": "Kubernetes cluster is temporarily unavailable. Please try again later.", + "details": "connect ECONNREFUSED 10.0.0.1:6443" + } + } + } + } + } + } + } + } + } + }, + "/apps/{name}/start": { + "post": { + "tags": [ + "Mutation" + ], + "operationId": "startApp", + "summary": "Start paused application", + "description": "Resumes a paused application by restoring replica count or HPA configuration.\n\nPods are restored to handle traffic. Services and ingresses remain active throughout. Typically takes 1–3 minutes to reach full availability. If the application is already running, returns `204` immediately (idempotent).", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Application name (Kubernetes resource name, must be a valid DNS subdomain: lowercase alphanumeric, hyphens)", + "required": true, + "example": "web-api", + "schema": { + "type": "string", + "minLength": 1, + "example": "web-api" + } + } + ], + "responses": { + "204": { + "description": "Application started successfully (or was already running)" + }, + "400": { + "description": "Bad Request - Invalid parameters", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "validation_error" + ], + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "INVALID_PARAMETER" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "example": "ports[0].number" + }, + "message": { + "type": "string", + "example": "Required" + } + }, + "required": [ + "field", + "message" + ] + } + }, + { + "type": "string" + } + ], + "description": "For INVALID_PARAMETER: Array<{ field: string, message: string }>. For INVALID_VALUE: optional string. Omitted for other codes.", + "example": [ + { + "field": "image.imageName", + "message": "Required" + } + ] + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 400 - start" + }, + "examples": { + "invalidName": { + "summary": "Invalid application name", + "value": { + "error": { + "type": "validation_error", + "code": "INVALID_PARAMETER", + "message": "Application name path parameter is invalid or missing.", + "details": [ + { + "field": "name", + "message": "name cannot be empty" + } + ] + } + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Missing or invalid kubeconfig", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authentication_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "AUTHENTICATION_REQUIRED", + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 401" + }, + "examples": { + "missingAuth": { + "summary": "Missing authentication", + "value": { + "error": { + "type": "authentication_error", + "code": "AUTHENTICATION_REQUIRED", + "message": "Authentication required. Please provide valid credentials in the Authorization header." + } + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authorization_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "PERMISSION_DENIED" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 403 - start" + }, + "examples": { + "insufficientPermissions": { + "summary": "Insufficient permissions", + "value": { + "error": { + "type": "authorization_error", + "code": "PERMISSION_DENIED", + "message": "Insufficient permissions to perform this operation. Please check your access rights." + } + } + } + } + } + } + }, + "404": { + "description": "Not Found - Application not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "NOT_FOUND", + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 404" + }, + "examples": { + "notFound": { + "summary": "Application not found", + "value": { + "error": { + "type": "resource_error", + "code": "NOT_FOUND", + "message": "Application \"web-api\" not found in the current namespace. Please verify the application name." + } + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "operation_error", + "internal_error" + ], + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "KUBERNETES_ERROR", + "OPERATION_FAILED", + "INTERNAL_ERROR" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Raw error string from the underlying system, for troubleshooting.", + "example": "deployments.apps \"web-api\" not found" + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 500" + }, + "examples": { + "startFailed": { + "summary": "Start operation failed", + "value": { + "error": { + "type": "operation_error", + "code": "KUBERNETES_ERROR", + "message": "Failed to start application \"web-api\". The Kubernetes operation encountered an error.", + "details": "deployments.apps \"web-api\" not found" + } + } + } + } + } + } + } + } + } + }, + "/apps/{name}/pause": { + "post": { + "tags": [ + "Mutation" + ], + "operationId": "pauseApp", + "summary": "Pause application", + "description": "Temporarily stops an application by scaling replicas to zero while preserving its configuration.\n\nKey points:\n- Replica/HPA state is stored and restored on next start\n- Pods are terminated gracefully (30s grace period)\n- Services and ingresses remain but route no traffic\n- Storage is preserved\n- Typical compute cost reduction: 60–80%\n- This operation is idempotent: pausing an already-paused application returns `204` with no changes", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Application name (Kubernetes resource name, must be a valid DNS subdomain: lowercase alphanumeric, hyphens)", + "required": true, + "example": "web-api", + "schema": { + "type": "string", + "minLength": 1, + "example": "web-api" + } + } + ], + "responses": { + "204": { + "description": "Application paused successfully" + }, + "400": { + "description": "Bad Request - Invalid parameters", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "validation_error" + ], + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "INVALID_PARAMETER" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "example": "ports[0].number" + }, + "message": { + "type": "string", + "example": "Required" + } + }, + "required": [ + "field", + "message" + ] + } + }, + { + "type": "string" + } + ], + "description": "For INVALID_PARAMETER: Array<{ field: string, message: string }>. For INVALID_VALUE: optional string. Omitted for other codes.", + "example": [ + { + "field": "image.imageName", + "message": "Required" + } + ] + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 400 - pause" + }, + "examples": { + "invalidName": { + "summary": "Invalid application name", + "value": { + "error": { + "type": "validation_error", + "code": "INVALID_PARAMETER", + "message": "Application name path parameter is invalid or missing.", + "details": [ + { + "field": "name", + "message": "name cannot be empty" + } + ] + } + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Missing or invalid kubeconfig", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authentication_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "AUTHENTICATION_REQUIRED", + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 401" + }, + "examples": { + "missingAuth": { + "summary": "Missing authentication", + "value": { + "error": { + "type": "authentication_error", + "code": "AUTHENTICATION_REQUIRED", + "message": "Authentication required. Please provide valid credentials in the Authorization header." + } + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authorization_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "PERMISSION_DENIED" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 403 - pause" + }, + "examples": { + "insufficientPermissions": { + "summary": "Insufficient permissions", + "value": { + "error": { + "type": "authorization_error", + "code": "PERMISSION_DENIED", + "message": "Insufficient permissions to perform this operation. Please check your access rights." + } + } + } + } + } + } + }, + "404": { + "description": "Not Found - Application not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "NOT_FOUND", + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 404" + }, + "examples": { + "notFound": { + "summary": "Application not found", + "value": { + "error": { + "type": "resource_error", + "code": "NOT_FOUND", + "message": "Application \"web-api\" not found in the current namespace. Please verify the application name." + } + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "operation_error", + "internal_error" + ], + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "KUBERNETES_ERROR", + "OPERATION_FAILED", + "INTERNAL_ERROR" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Raw error string from the underlying system, for troubleshooting.", + "example": "deployments.apps \"web-api\" not found" + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 500" + }, + "examples": { + "pauseFailed": { + "summary": "Pause operation failed", + "value": { + "error": { + "type": "operation_error", + "code": "KUBERNETES_ERROR", + "message": "Failed to pause application \"web-api\". The Kubernetes operation encountered an error.", + "details": "deployments.apps \"web-api\" not found" + } + } + } + } + } + } + } + } + } + }, + "/apps/{name}/storage": { + "patch": { + "tags": [ + "Mutation" + ], + "operationId": "updateAppStorage", + "summary": "Update application storage (incremental)", + "description": "Incrementally updates storage volumes for a StatefulSet application.\n\nKey points:\n- Only StatefulSet applications are supported (Deployment returns `400`)\n- Incremental merge: pass only the volumes you want to add or resize — unlisted volumes are preserved\n- To remove a volume, use `PATCH /apps/{name}` with a complete storage replacement\n- Storage can only be expanded, not shrunk (PVC limitation)\n- Changes are applied by deleting and recreating the StatefulSet — brief downtime is expected", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Application name (Kubernetes resource name, must be a valid DNS subdomain: lowercase alphanumeric, hyphens)", + "required": true, + "example": "web-api", + "schema": { + "type": "string", + "minLength": 1, + "example": "web-api" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "storage": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Mount path in the container" + }, + "size": { + "type": "string", + "default": "1Gi", + "description": "Storage size (e.g., \"10Gi\", \"1Ti\")" + } + }, + "required": [ + "path" + ], + "description": "Simplified storage configuration (name auto-generated from path)" + }, + "default": [], + "description": "Storage configurations to update (incremental). Only includes storage to add or modify, existing storage not listed will be preserved. Name is auto-generated from path." + } + } + }, + "examples": { + "expandVolume": { + "summary": "Expand an existing volume", + "value": { + "storage": [ + { + "path": "/data", + "size": "20Gi" + } + ] + } + }, + "addVolume": { + "summary": "Add a new volume", + "value": { + "storage": [ + { + "path": "/logs", + "size": "5Gi" + } + ] + } + } + } + } + } + }, + "responses": { + "204": { + "description": "Storage updated successfully" + }, + "400": { + "description": "Bad Request - Invalid parameters or unsupported application type", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "validation_error", + "client_error" + ], + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "INVALID_PARAMETER", + "STORAGE_REQUIRES_STATEFULSET" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "example": "ports[0].number" + }, + "message": { + "type": "string", + "example": "Required" + } + }, + "required": [ + "field", + "message" + ] + } + }, + { + "type": "string" + } + ], + "description": "For INVALID_PARAMETER: Array<{ field: string, message: string }>. For INVALID_VALUE: optional string. Omitted for other codes.", + "example": [ + { + "field": "image.imageName", + "message": "Required" + } + ] + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 400 - storage" + }, + "examples": { + "invalidName": { + "summary": "Invalid application name", + "value": { + "error": { + "type": "validation_error", + "code": "INVALID_PARAMETER", + "message": "Application name path parameter is invalid or missing." + } + } + }, + "notStatefulSet": { + "summary": "Application is not a StatefulSet", + "value": { + "error": { + "type": "client_error", + "code": "STORAGE_REQUIRES_STATEFULSET", + "message": "Storage updates are only supported for StatefulSet applications. Application \"web-api\" is currently a deployment.", + "details": "Convert your application to StatefulSet to enable storage management." + } + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Missing or invalid kubeconfig", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authentication_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "AUTHENTICATION_REQUIRED", + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 401" + }, + "examples": { + "missingAuth": { + "summary": "Missing authentication", + "value": { + "error": { + "type": "authentication_error", + "code": "AUTHENTICATION_REQUIRED", + "message": "Authentication required. Please provide valid credentials in the Authorization header." + } + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authorization_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "PERMISSION_DENIED" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 403 - storage" + }, + "examples": { + "insufficientPermissions": { + "summary": "Insufficient permissions", + "value": { + "error": { + "type": "authorization_error", + "code": "PERMISSION_DENIED", + "message": "Insufficient permissions to perform this operation. Please check your access rights." + } + } + } + } + } + } + }, + "404": { + "description": "Not Found - Application not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "NOT_FOUND", + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 404" + }, + "examples": { + "notFound": { + "summary": "Application not found", + "value": { + "error": { + "type": "resource_error", + "code": "NOT_FOUND", + "message": "Application \"web-api\" not found in the current namespace. Please verify the application name." + } + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error - StatefulSet recreation or PVC update failed", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "operation_error", + "internal_error" + ], + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "KUBERNETES_ERROR", + "STORAGE_UPDATE_FAILED", + "OPERATION_FAILED", + "INTERNAL_ERROR" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Raw error string from the underlying system, for troubleshooting.", + "example": "deployments.apps \"web-api\" not found" + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 500 - storage" + }, + "examples": { + "storageFailed": { + "summary": "Storage update failed", + "value": { + "error": { + "type": "operation_error", + "code": "STORAGE_UPDATE_FAILED", + "message": "Failed to update storage for application \"web-api\". The StatefulSet recreation or PVC update failed.", + "details": "cannot shrink PVC from 10Gi to 5Gi" + } + } + } + } + } + } + } + } + } + }, + "/apps/{name}/restart": { + "post": { + "tags": [ + "Mutation" + ], + "operationId": "restartApp", + "summary": "Restart application", + "description": "Performs a rolling restart of all application pods while maintaining service availability.\n\nKey points:\n- Zero-downtime for applications with multiple replicas; 30–90s downtime for single-replica applications\n- Typical duration: 30–90s (single replica) to 2–5 min (multiple replicas)\n- Does **not** update configuration — use `PATCH /apps/{name}` for config changes", + "parameters": [ + { + "name": "name", + "in": "path", + "description": "Application name (Kubernetes resource name, must be a valid DNS subdomain: lowercase alphanumeric, hyphens)", + "required": true, + "example": "web-api", + "schema": { + "type": "string", + "minLength": 1, + "example": "web-api" + } + } + ], + "responses": { + "204": { + "description": "Application restart initiated successfully" + }, + "400": { + "description": "Bad Request - Invalid parameters", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "validation_error" + ], + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "INVALID_PARAMETER" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "object", + "properties": { + "field": { + "type": "string", + "example": "ports[0].number" + }, + "message": { + "type": "string", + "example": "Required" + } + }, + "required": [ + "field", + "message" + ] + } + }, + { + "type": "string" + } + ], + "description": "For INVALID_PARAMETER: Array<{ field: string, message: string }>. For INVALID_VALUE: optional string. Omitted for other codes.", + "example": [ + { + "field": "image.imageName", + "message": "Required" + } + ] + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 400 - restart" + }, + "examples": { + "invalidName": { + "summary": "Invalid application name", + "value": { + "error": { + "type": "validation_error", + "code": "INVALID_PARAMETER", + "message": "Application name path parameter is invalid or missing.", + "details": [ + { + "field": "name", + "message": "name cannot be empty" + } + ] + } + } + } + } + } + } + }, + "401": { + "description": "Unauthorized - Missing or invalid kubeconfig", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authentication_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "AUTHENTICATION_REQUIRED", + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 401" + }, + "examples": { + "missingAuth": { + "summary": "Missing authentication", + "value": { + "error": { + "type": "authentication_error", + "code": "AUTHENTICATION_REQUIRED", + "message": "Authentication required. Please provide valid credentials in the Authorization header." + } + } + } + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "authorization_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "PERMISSION_DENIED" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 403 - restart" + }, + "examples": { + "insufficientPermissions": { + "summary": "Insufficient permissions", + "value": { + "error": { + "type": "authorization_error", + "code": "PERMISSION_DENIED", + "message": "Insufficient permissions to perform this operation. Please check your access rights." + } + } + } + } + } + } + }, + "404": { + "description": "Not Found - Application not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "resource_error", + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "const": "NOT_FOUND", + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Typically omitted. May contain additional context in edge cases." + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 404" + }, + "examples": { + "notFound": { + "summary": "Application not found", + "value": { + "error": { + "type": "resource_error", + "code": "NOT_FOUND", + "message": "Application \"web-api\" not found in the current namespace. Please verify the application name." + } + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "operation_error", + "internal_error" + ], + "description": "High-level error type for categorization" + }, + "code": { + "type": "string", + "enum": [ + "KUBERNETES_ERROR", + "OPERATION_FAILED", + "INTERNAL_ERROR" + ], + "description": "Specific error code for programmatic handling and i18n" + }, + "message": { + "type": "string", + "description": "Human-readable error message" + }, + "details": { + "type": "string", + "description": "Raw error string from the underlying system, for troubleshooting.", + "example": "deployments.apps \"web-api\" not found" + } + }, + "required": [ + "type", + "code", + "message" + ] + } + }, + "required": [ + "error" + ], + "title": "Error 500" + }, + "examples": { + "restartFailed": { + "summary": "Restart operation failed", + "value": { + "error": { + "type": "operation_error", + "code": "KUBERNETES_ERROR", + "message": "Failed to restart application \"web-api\". The Kubernetes operation encountered an error.", + "details": "deployments.apps \"web-api\" not found" + } + } + } + } + } + } + } + } + } + } + }, + "components": { + "securitySchemes": { + "kubeconfigAuth": { + "type": "apiKey", + "in": "header", + "name": "Authorization", + "description": "Kubeconfig encoded with `encodeURIComponent()`. Example: `Authorization: `. Obtain your kubeconfig from the Sealos console user menu." + } + } + } +} diff --git a/skills/applaunchpad/scripts/sealos-applaunchpad.mjs b/skills/applaunchpad/scripts/sealos-applaunchpad.mjs new file mode 100644 index 0000000..83f24cb --- /dev/null +++ b/skills/applaunchpad/scripts/sealos-applaunchpad.mjs @@ -0,0 +1,395 @@ +#!/usr/bin/env node +// Sealos AppLaunchpad CLI - single entry point for all app operations. +// Zero external dependencies. Requires Node.js (guaranteed by Claude Code). +// +// Usage: +// node sealos-applaunchpad.mjs [args...] +// +// Config resolution: +// ~/.sealos/auth.json region field → derives API URL automatically +// Kubeconfig is always read from ~/.sealos/kubeconfig +// +// Commands: +// list List all apps +// get Get app details +// create Create a new app +// create-wait Create + poll until running (timeout 2min) +// update Update app configuration +// delete Delete an app +// start Start a paused app +// pause Pause a running app (scales to zero) +// restart Restart an app +// update-storage Incremental storage update (expand only) + +import { readFileSync, existsSync } from 'node:fs'; +import { request as httpsRequest } from 'node:https'; +import { request as httpRequest } from 'node:http'; +import { resolve } from 'node:path'; +import { homedir } from 'node:os'; + +const KC_PATH = resolve(homedir(), '.sealos/kubeconfig'); +const AUTH_PATH = resolve(homedir(), '.sealos/auth.json'); +const API_PATH = '/api/v2alpha'; // API version — update here if the version changes + +// --- config --- + +function loadConfig() { + // Derive from auth.json region + if (!existsSync(AUTH_PATH)) { + throw new Error('Not authenticated. Run: node sealos-auth.mjs login'); + } + + let auth; + try { + auth = JSON.parse(readFileSync(AUTH_PATH, 'utf-8')); + } catch { + throw new Error('Invalid auth.json. Run: node sealos-auth.mjs login'); + } + + if (!auth.region) { + throw new Error('No region in auth.json. Run: node sealos-auth.mjs login'); + } + + // Derive API URL: region "https://gzg.sealos.run" → "https://applaunchpad.gzg.sealos.run/api/v2alpha" + const regionUrl = new URL(auth.region); + const apiUrl = `https://applaunchpad.${regionUrl.hostname}${API_PATH}`; + + if (!existsSync(KC_PATH)) { + throw new Error(`Kubeconfig not found at ${KC_PATH}. Run: node sealos-auth.mjs login`); + } + + return { apiUrl, kubeconfigPath: KC_PATH }; +} + +// --- auth --- + +function getEncodedKubeconfig(path) { + if (!existsSync(path)) { + throw new Error(`Kubeconfig not found at ${path}`); + } + return encodeURIComponent(readFileSync(path, 'utf-8')); +} + +// --- HTTP --- + +function apiCall(method, endpoint, { apiUrl, auth, body, timeout = 30000 } = {}) { + return new Promise((resolve, reject) => { + const url = new URL(apiUrl + endpoint); + const isHttps = url.protocol === 'https:'; + const reqFn = isHttps ? httpsRequest : httpRequest; + + const headers = {}; + if (auth) headers['Authorization'] = auth; + if (body) headers['Content-Type'] = 'application/json'; + + const bodyStr = body ? JSON.stringify(body) : null; + if (bodyStr) headers['Content-Length'] = Buffer.byteLength(bodyStr); + + const opts = { + hostname: url.hostname, + port: url.port || (isHttps ? 443 : 80), + path: url.pathname + url.search, + method, + headers, + timeout, + rejectUnauthorized: false, // Sealos clusters may use self-signed certificates + }; + + const req = reqFn(opts, (res) => { + const chunks = []; + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => { + const rawBody = Buffer.concat(chunks).toString(); + let parsed = null; + try { parsed = JSON.parse(rawBody); } catch { parsed = rawBody || null; } + resolve({ status: res.statusCode, body: parsed }); + }); + }); + + req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); }); + req.on('error', reject); + if (bodyStr) req.write(bodyStr); + req.end(); + }); +} + +// --- helpers --- + +function requireName(args) { + if (!args[0]) throw new Error('App name required'); + return args[0]; +} + +function output(data) { + console.log(JSON.stringify(data, null, 2)); +} + +function sleep(ms) { + return new Promise((r) => setTimeout(r, ms)); +} + +// --- individual commands --- + +async function list(cfg) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('GET', '/apps', { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 200) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +async function get(cfg, name) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('GET', `/apps/${name}`, { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 200) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +function validateCreateBody(body) { + const errors = []; + if (body.name) { + if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(body.name)) errors.push('name must match ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'); + if (body.name.length > 63) errors.push('name must be at most 63 characters'); + } + if (!body.image || !body.image.imageName) { + errors.push('image.imageName is required'); + } + if (body.image && body.image.imageRegistry && body.image.imageRegistry !== null) { + const reg = body.image.imageRegistry; + if (!reg.username || !reg.password || !reg.serverAddress) { + errors.push('imageRegistry requires username, password, and serverAddress'); + } + } + if (body.quota) { + const q = body.quota; + if (q.cpu !== undefined && (typeof q.cpu !== 'number' || q.cpu < 0.1 || q.cpu > 32)) errors.push('cpu must be 0.1-32'); + if (q.memory !== undefined && (typeof q.memory !== 'number' || q.memory < 0.1 || q.memory > 32)) errors.push('memory must be 0.1-32 GB'); + if (q.replicas !== undefined && (!Number.isInteger(q.replicas) || q.replicas < 1 || q.replicas > 20)) errors.push('replicas must be an integer 1-20'); + if (q.replicas !== undefined && q.hpa !== undefined) errors.push('replicas and hpa are mutually exclusive'); + if (q.hpa) { + if (!['cpu', 'memory', 'gpu'].includes(q.hpa.target)) errors.push('hpa.target must be cpu, memory, or gpu'); + if (typeof q.hpa.value !== 'number') errors.push('hpa.value is required'); + if (typeof q.hpa.minReplicas !== 'number') errors.push('hpa.minReplicas is required'); + if (typeof q.hpa.maxReplicas !== 'number') errors.push('hpa.maxReplicas is required'); + } + if (q.gpu) { + if (!q.gpu.type) errors.push('gpu.type is required (e.g., A100, V100, T4)'); + } + } + if (body.ports) { + for (const p of body.ports) { + if (!p.number || p.number < 1 || p.number > 65535) errors.push(`port number must be 1-65535, got ${p.number}`); + if (p.protocol && !['http', 'grpc', 'ws', 'tcp', 'udp', 'sctp'].includes(p.protocol)) { + errors.push(`port protocol must be http|grpc|ws|tcp|udp|sctp, got ${p.protocol}`); + } + } + } + if (errors.length) throw new Error('Validation failed: ' + errors.join('; ')); +} + +function validateUpdateBody(body) { + const errors = []; + if (body.quota) { + const q = body.quota; + if (q.cpu !== undefined && ![0.1, 0.2, 0.5, 1, 2, 3, 4, 8].includes(q.cpu)) errors.push('cpu must be one of: 0.1, 0.2, 0.5, 1, 2, 3, 4, 8'); + if (q.memory !== undefined && ![0.1, 0.5, 1, 2, 4, 8, 16].includes(q.memory)) errors.push('memory must be one of: 0.1, 0.5, 1, 2, 4, 8, 16 GB'); + if (q.replicas !== undefined && (!Number.isInteger(q.replicas) || q.replicas < 1 || q.replicas > 20)) errors.push('replicas must be an integer 1-20'); + if (q.replicas !== undefined && q.hpa !== undefined) errors.push('replicas and hpa are mutually exclusive'); + if (q.hpa) { + if (!['cpu', 'memory', 'gpu'].includes(q.hpa.target)) errors.push('hpa.target must be cpu, memory, or gpu'); + if (typeof q.hpa.value !== 'number') errors.push('hpa.value is required'); + if (typeof q.hpa.minReplicas !== 'number') errors.push('hpa.minReplicas is required'); + if (typeof q.hpa.maxReplicas !== 'number') errors.push('hpa.maxReplicas is required'); + } + } + if (body.ports) { + for (const p of body.ports) { + if (p.number !== undefined && (p.number < 1 || p.number > 65535)) errors.push(`port number must be 1-65535, got ${p.number}`); + if (p.protocol && !['http', 'grpc', 'ws', 'tcp', 'udp', 'sctp'].includes(p.protocol)) { + errors.push(`port protocol must be http|grpc|ws|tcp|udp|sctp, got ${p.protocol}`); + } + } + } + if (body.image && !body.image.imageName) { + errors.push('image.imageName is required when updating image'); + } + if (errors.length) throw new Error('Validation failed: ' + errors.join('; ')); +} + +async function create(cfg, jsonBody) { + const body = typeof jsonBody === 'string' ? JSON.parse(jsonBody) : jsonBody; + validateCreateBody(body); + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('POST', '/apps', { apiUrl: cfg.apiUrl, auth, body }); + if (res.status !== 201) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +async function update(cfg, name, jsonBody) { + const body = typeof jsonBody === 'string' ? JSON.parse(jsonBody) : jsonBody; + validateUpdateBody(body); + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('PATCH', `/apps/${name}`, { apiUrl: cfg.apiUrl, auth, body }); + if (res.status !== 204) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + // Re-fetch to return updated state + try { + const updated = await get(cfg, name); + return { success: true, message: 'App update initiated', app: updated }; + } catch { + return { success: true, message: 'App update initiated' }; + } +} + +async function del(cfg, name) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('DELETE', `/apps/${name}`, { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 204) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return { success: true, message: `App '${name}' deleted` }; +} + +async function action(cfg, name, actionName) { + const validActions = ['start', 'pause', 'restart']; + if (!validActions.includes(actionName)) { + throw new Error(`Invalid action '${actionName}'. Valid actions: ${validActions.join(', ')}`); + } + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('POST', `/apps/${name}/${actionName}`, { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 204) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return { success: true, message: `Action '${actionName}' on '${name}' completed` }; +} + +async function updateStorage(cfg, name, jsonBody) { + const body = typeof jsonBody === 'string' ? JSON.parse(jsonBody) : jsonBody; + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('PATCH', `/apps/${name}/storage`, { apiUrl: cfg.apiUrl, auth, body }); + if (res.status !== 204) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + // Re-fetch to return updated state + try { + const updated = await get(cfg, name); + return { success: true, message: 'Storage update initiated', app: updated }; + } catch { + return { success: true, message: 'Storage update initiated' }; + } +} + +// --- batch commands --- + +async function createWait(cfg, jsonBody) { + // 1. Create + const body = typeof jsonBody === 'string' ? JSON.parse(jsonBody) : jsonBody; + const createResult = await create(cfg, body); + const name = body.name || createResult.name; + + // 2. Poll every 5s until running (max 2 min) + const timeout = 120000; + const interval = 5000; + const start = Date.now(); + + let lastStatus = 'creating'; + let consecutiveErrors = 0; + while (Date.now() - start < timeout) { + await sleep(interval); + try { + const info = await get(cfg, name); + consecutiveErrors = 0; + lastStatus = info.status; + process.stderr.write(`Status: ${lastStatus}\n`); + if (lastStatus.toLowerCase() === 'running') { + return info; + } + if (lastStatus.toLowerCase() === 'error') { + throw new Error(`App creation failed. Status: ${lastStatus}`); + } + } catch (e) { + consecutiveErrors++; + if (consecutiveErrors >= 5 || Date.now() - start > timeout) throw e; + } + } + + // Timeout - return last known state + try { + const info = await get(cfg, name); + return { ...info, warning: `Timed out after 2 minutes. Last status: ${info.status}` }; + } catch { + return { name, status: lastStatus, warning: 'Timed out after 2 minutes' }; + } +} + +// --- main --- + +async function main() { + const [cmd, ...args] = process.argv.slice(2); + + if (!cmd) { + console.error('ERROR: Command required.'); + console.error('Commands: list|get|create|create-wait|update|delete|start|pause|restart|update-storage'); + process.exit(1); + } + + try { + const cfg = loadConfig(); + let result; + + switch (cmd) { + case 'list': { + result = await list(cfg); + break; + } + + case 'get': { + const name = requireName(args); + result = await get(cfg, name); + break; + } + + case 'create': { + if (!args[0]) throw new Error('JSON body required'); + result = await create(cfg, args[0]); + break; + } + + case 'create-wait': { + if (!args[0]) throw new Error('JSON body required'); + result = await createWait(cfg, args[0]); + break; + } + + case 'update': { + const name = requireName(args); + if (!args[1]) throw new Error('JSON body required'); + result = await update(cfg, name, args[1]); + break; + } + + case 'delete': { + const name = requireName(args); + result = await del(cfg, name); + break; + } + + case 'start': + case 'pause': + case 'restart': { + const name = requireName(args); + result = await action(cfg, name, cmd); + break; + } + + case 'update-storage': { + const name = requireName(args); + if (!args[1]) throw new Error('JSON body required'); + result = await updateStorage(cfg, name, args[1]); + break; + } + + default: + throw new Error(`Unknown command '${cmd}'. Commands: list|get|create|create-wait|update|delete|start|pause|restart|update-storage`); + } + + if (result !== undefined) output(result); + } catch (err) { + console.error(`ERROR: ${err.message}`); + process.exit(1); + } +} + +main(); diff --git a/skills/applaunchpad/scripts/sealos-auth.mjs b/skills/applaunchpad/scripts/sealos-auth.mjs new file mode 100644 index 0000000..aa25cf5 --- /dev/null +++ b/skills/applaunchpad/scripts/sealos-auth.mjs @@ -0,0 +1,329 @@ +#!/usr/bin/env node + +/** + * Sealos Cloud Authentication — OAuth2 Device Grant Flow (RFC 8628) + * + * Usage: + * node sealos-auth.mjs check # Check if already authenticated + * node sealos-auth.mjs login [region] # Start device grant login flow + * node sealos-auth.mjs info # Show current auth info + * + * Environment variables: + * SEALOS_REGION — Sealos Cloud region URL (default from config.json) + * + * Flow: + * 1. POST /api/auth/oauth2/device → { device_code, user_code, verification_uri_complete } + * 2. User opens verification_uri_complete in browser to authorize + * 3. Script polls /api/auth/oauth2/token until approved + * 4. Receives access_token → exchanges for kubeconfig → saves to ~/.sealos/kubeconfig + */ + +import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs' +import { execSync } from 'child_process' +import { homedir, platform } from 'os' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' + +// ── Paths ──────────────────────────────────────────────── +const __dirname = dirname(fileURLToPath(import.meta.url)) +const SEALOS_DIR = join(homedir(), '.sealos') +const KC_PATH = join(SEALOS_DIR, 'kubeconfig') +const AUTH_PATH = join(SEALOS_DIR, 'auth.json') + +// ── Skill constants (from config.json) ─────────────────── +const CONFIG_PATH = join(__dirname, '..', 'config.json') +const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')) +const CLIENT_ID = config.client_id +const DEFAULT_REGION = config.default_region + +// ── Check ────────────────────────────────────────────── + +function check () { + if (!existsSync(KC_PATH)) { + return { authenticated: false } + } + + try { + const kc = readFileSync(KC_PATH, 'utf-8') + if (kc.includes('server:') && (kc.includes('token:') || kc.includes('client-certificate'))) { + const auth = existsSync(AUTH_PATH) ? JSON.parse(readFileSync(AUTH_PATH, 'utf-8')) : {} + return { + authenticated: true, + kubeconfig_path: KC_PATH, + region: auth.region || 'unknown' + } + } + } catch { } + + return { authenticated: false } +} + +// ── Device Grant Flow ────────────────────────────────── + +/** + * Step 1: Request device authorization + * POST /api/auth/oauth2/device + * Body: { client_id } + * Response: { device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval } + */ +async function requestDeviceAuthorization (region) { + const res = await fetch(`${region}/api/auth/oauth2/device`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + client_id: CLIENT_ID, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code' + }) + }) + + if (!res.ok) { + const body = await res.text().catch(() => '') + throw new Error(`Device authorization request failed (${res.status}): ${body || res.statusText}`) + } + + return res.json() +} + +/** + * Step 2: Poll for token + * POST /api/auth/oauth2/token + * Body: { client_id, grant_type, device_code } + * + * Possible responses: + * - 200: { access_token, token_type, ... } → success + * - 400: { error: "authorization_pending" } → keep polling + * - 400: { error: "slow_down" } → increase interval by 5s + * - 400: { error: "access_denied" } → user denied + * - 400: { error: "expired_token" } → device code expired + */ +async function pollForToken (region, deviceCode, interval, expiresIn) { + // Hard cap at 10 minutes regardless of server's expires_in + const maxWait = Math.min(expiresIn, 600) * 1000 + const deadline = Date.now() + maxWait + let pollInterval = interval * 1000 + let lastLoggedMinute = -1 + + while (Date.now() < deadline) { + await sleep(pollInterval) + + // Log remaining time every minute + const remaining = Math.ceil((deadline - Date.now()) / 60000) + if (remaining !== lastLoggedMinute && remaining > 0) { + lastLoggedMinute = remaining + process.stderr.write(` Waiting for authorization... (${remaining} min remaining)\n`) + } + + const res = await fetch(`${region}/api/auth/oauth2/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + client_id: CLIENT_ID, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + device_code: deviceCode + }) + }) + + if (res.ok) { + // Success — got the token + return res.json() + } + + const body = await res.json().catch(() => ({})) + + switch (body.error) { + case 'authorization_pending': + // User hasn't authorized yet, keep polling + break + + case 'slow_down': + // Increase polling interval by 5 seconds (RFC 8628 §3.5) + pollInterval += 5000 + break + + case 'access_denied': + throw new Error('Authorization denied by user') + + case 'expired_token': + throw new Error('Device code expired. Please run login again.') + + default: + throw new Error(`Token request failed: ${body.error || res.statusText}`) + } + } + + throw new Error('Authorization timed out (10 minutes). Please run login again.') +} + +/** + * Step 3: Exchange access token for kubeconfig + */ +async function exchangeForKubeconfig (region, accessToken) { + const res = await fetch(`${region}/api/auth/getDefaultKubeconfig`, { + method: 'POST', + headers: { + Authorization: accessToken, + 'Content-Type': 'application/json' + } + }) + + if (!res.ok) { + const body = await res.text().catch(() => '') + throw new Error(`Kubeconfig exchange failed (${res.status}): ${body || res.statusText}`) + } + + return res.json() +} + +function sleep (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +// ── Login (Device Grant Flow) ────────────────────────── + +async function login (region = DEFAULT_REGION) { + region = region.replace(/\/+$/, '') + + // Step 1: Request device authorization + const deviceAuth = await requestDeviceAuthorization(region) + + const { + device_code: deviceCode, + user_code: userCode, + verification_uri: verificationUri, + verification_uri_complete: verificationUriComplete, + expires_in: expiresIn, + interval = 5 + } = deviceAuth + + // Output device authorization info for the AI tool / user to display + const authPrompt = { + action: 'user_authorization_required', + user_code: userCode, + verification_uri: verificationUri, + verification_uri_complete: verificationUriComplete, + expires_in: expiresIn, + message: `Please open the following URL in your browser to authorize:\n\n ${verificationUriComplete || verificationUri}\n\nAuthorization code: ${userCode}\nExpires in: ${Math.floor(expiresIn / 60)} minutes` + } + + // Print the authorization prompt to stderr so it's visible to the user + // while stdout is reserved for JSON output + process.stderr.write('\n' + authPrompt.message + '\n\nWaiting for authorization...\n') + + // Auto-open browser + const url = verificationUriComplete || verificationUri + try { + const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open' + execSync(`${cmd} "${url}"`, { stdio: 'ignore' }) + process.stderr.write('Browser opened automatically.\n') + } catch { + process.stderr.write('Could not open browser automatically. Please open the URL manually.\n') + } + + // Step 2: Poll for token + const tokenResponse = await pollForToken(region, deviceCode, interval, expiresIn) + const accessToken = tokenResponse.access_token + + if (!accessToken) { + throw new Error('Token response missing access_token') + } + + process.stderr.write('Authorization received. Exchanging for kubeconfig...\n') + + // Step 3: Exchange access token for kubeconfig + const kcData = await exchangeForKubeconfig(region, accessToken) + const kubeconfig = kcData.data?.kubeconfig + + if (!kubeconfig) { + throw new Error('API response missing data.kubeconfig field') + } + + // Save kubeconfig to ~/.sealos/kubeconfig (Sealos-specific, avoids conflict with ~/.kube/config) + mkdirSync(SEALOS_DIR, { recursive: true }) + writeFileSync(KC_PATH, kubeconfig, { mode: 0o600 }) + writeFileSync(AUTH_PATH, JSON.stringify({ + region, + authenticated_at: new Date().toISOString(), + auth_method: 'oauth2_device_grant' + }, null, 2), { mode: 0o600 }) + + process.stderr.write('Authentication successful!\n') + + return { kubeconfig_path: KC_PATH, region } +} + +// ── Info ─────────────────────────────────────────────── + +function info () { + const status = check() + if (!status.authenticated) { + return { authenticated: false, message: 'Not authenticated. Run: node sealos-auth.mjs login' } + } + + const auth = existsSync(AUTH_PATH) ? JSON.parse(readFileSync(AUTH_PATH, 'utf-8')) : {} + return { + authenticated: true, + kubeconfig_path: KC_PATH, + region: auth.region || 'unknown', + auth_method: auth.auth_method || 'unknown', + authenticated_at: auth.authenticated_at || 'unknown' + } +} + +// ── CLI ──────────────────────────────────────────────── + +const [, , cmd, ...rawArgs] = process.argv + +// --insecure flag: skip TLS certificate verification (for self-signed certs) +const insecure = rawArgs.includes('--insecure') +const args = rawArgs.filter(a => a !== '--insecure') + +if (insecure) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' +} + +try { + switch (cmd) { + case 'check': { + console.log(JSON.stringify(check())) + break + } + + case 'login': { + const region = args[0] || process.env.SEALOS_REGION || DEFAULT_REGION + const result = await login(region) + console.log(JSON.stringify(result)) + break + } + + case 'info': { + console.log(JSON.stringify(info(), null, 2)) + break + } + + default: { + console.log(`Sealos Cloud Auth — OAuth2 Device Grant Flow + +Usage: + node sealos-auth.mjs check Check authentication status + node sealos-auth.mjs login [region] Start OAuth2 device login flow + node sealos-auth.mjs login --insecure Skip TLS verification (self-signed cert) + node sealos-auth.mjs info Show current auth details + +Environment: + SEALOS_REGION Region URL (default: ${DEFAULT_REGION}) + +Flow: + 1. Run "login" → opens browser for authorization + 2. Approve in browser → script receives token automatically + 3. Token exchanged for kubeconfig → saved to ~/.sealos/kubeconfig`) + } + } +} catch (err) { + // If TLS error and not using --insecure, hint the user + if (!insecure && (err.message.includes('fetch failed') || err.message.includes('self-signed') || err.message.includes('CERT'))) { + console.error(JSON.stringify({ error: err.message, hint: 'Try adding --insecure for self-signed certificates' })) + } else { + console.error(JSON.stringify({ error: err.message })) + } + process.exit(1) +} diff --git a/skills/database/README.md b/skills/database/README.md new file mode 100644 index 0000000..f4b619c --- /dev/null +++ b/skills/database/README.md @@ -0,0 +1,90 @@ +# Sealos DB — Claude Code Skill + +Manage databases on [Sealos](https://sealos.io) using natural language. Create, scale, backup, and manage PostgreSQL, MongoDB, Redis, and 8 more database types — without leaving your terminal. + +## Quick Start + +### Prerequisites + +- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed +- A [Sealos](https://sealos.io) account (login is handled automatically via OAuth) + +### Install + +Copy the `database` skill into your project's `.claude/skills/` directory. Claude Code auto-detects it. + +### Usage + +Ask Claude Code in natural language, or run `/sealos-db`: + +``` +> I need a PostgreSQL database for my app +> Create a Redis cache +> Show my databases +> Scale my-app-pg to 4 cores and 8 GB memory +> Delete the test database +> Enable public access on my-app-pg +> Backup my production database +> Restore from last night's backup +> Show slow query logs for my-app-pg +> Restart the staging database +``` + +Claude walks you through authentication, configuration, and execution interactively. + +## Features + +- **Smart defaults** — detects your tech stack and recommends a database type, version, and resources +- **Full lifecycle** — create, list, inspect, update, delete, start, pause, restart +- **11 database types** — PostgreSQL, MongoDB, MySQL, Redis, Kafka, ClickHouse, Qdrant, Milvus, Weaviate, Nebula, Pulsar +- **Public access control** — expose or hide databases from the internet +- **Backup & restore** — create snapshots, list backups, restore to a new instance +- **Log inspection** — view runtime logs, slow queries, and error logs +- **Project integration** — writes connection info to `.env`, `docker-compose.yml`, or framework config +- **Safe deletes** — requires explicit name confirmation before destroying anything + +## Authentication + +On first use, Claude opens your browser for OAuth2 login. Credentials are saved to `~/.sealos/kubeconfig` and the API URL is auto-derived from `~/.sealos/auth.json`. Three regions are available: `gzg`, `bja`, and `hzh`. + +## Supported Operations + +| Operation | Description | +|-----------|-------------| +| **Create** | Deploy a new database with customizable type, version, CPU, memory, storage, and replicas | +| **List** | Show all databases with status, resources, and replica count | +| **Get** | Inspect a specific database's full configuration and connection info | +| **Update** | Scale CPU, memory, storage, or replicas (storage expand-only) | +| **Delete** | Permanently remove a database (with name confirmation) | +| **Start** | Resume a paused database | +| **Pause** | Scale database to zero (preserves data) | +| **Restart** | Rolling restart of all replicas | +| **Public Access** | Enable or disable internet-facing connections | +| **Backup** | Create, list, delete, or restore from snapshots | +| **Logs** | View runtime logs, slow queries, and error logs | + +## Supported Database Types + +| Type | Identifier | Default Port | Typical Use | +|------|-----------|------|-------------| +| PostgreSQL | `postgresql` | 5432 | General-purpose RDBMS | +| MongoDB | `mongodb` | 27017 | Document database | +| MySQL | `apecloud-mysql` | 3306 | General-purpose RDBMS | +| Redis | `redis` | 6379 | Cache, sessions, pub/sub | +| Kafka | `kafka` | 9092 | Event streaming | +| ClickHouse | `clickhouse` | 8123 | Analytics / OLAP | +| Qdrant | `qdrant` | 6333 | Vector search | +| Milvus | `milvus` | 19530 | Vector search | +| Weaviate | `weaviate` | 8080 | Vector search | +| Nebula | `nebula` | 9669 | Graph database | +| Pulsar | `pulsar` | 6650 | Message queue | + +## Resource Defaults + +| Scenario | CPU | Memory | Storage | Replicas | Trigger | +|----------|-----|--------|---------|----------|---------| +| Default | 1 | 1 GB | 3 GB | 3 | No size hint, "dev", "testing" | +| Medium | 2 | 2 GB | 10 GB | 1 | "small", "starter" | +| Production | 2 | 4 GB | 20 GB | 3 | "prod", "production", "HA" | + +CPU: 1–8 cores. Memory: 0.1–32 GB. Storage: 1–300 GB (expand-only). Replicas: 1–20. diff --git a/skills/database/SKILL.md b/skills/database/SKILL.md new file mode 100644 index 0000000..2efb9b3 --- /dev/null +++ b/skills/database/SKILL.md @@ -0,0 +1,411 @@ +--- +name: sealos-db +description: >- + Use when someone needs to manage databases on Sealos: create, list, update, scale, + delete, start, stop, restart, check status, get connection info, enable/disable + public access, backup/restore databases, or view database logs. Triggers on + "I need a database", "create a PostgreSQL on sealos", "scale my database", + "delete the database", "show my databases", "backup my database", + "restore from backup", "show database logs", or "my app needs a database connection". +--- + +## Interaction Principle + +**NEVER output a question as plain text. ALWAYS use `AskUserQuestion` with `options`.** + +Claude Code's text output is non-interactive — if you write a question as plain text, the +user has no clickable options and must guess how to respond. `AskUserQuestion` gives them +clear choices they can click. + +- Every question → `AskUserQuestion` with `options`. Keep any preceding text to one short status line. +- `AskUserQuestion` adds an implicit "Other / Type something" option, so users can always type custom input. +- **Free-text matching:** When the user types instead of clicking, match to the closest option by intent. + "pg" → PostgreSQL; "mongo" → MongoDB. Never re-ask because wording didn't match exactly. + +## Fixed Execution Order + +**ALWAYS follow these steps in this exact order. No skipping, no reordering.** + +``` +Step 0: Check Auth (try existing config from previous session) +Step 1: Authenticate (only if Step 0 fails) +Step 2: Route (determine which operation the user wants) +Step 3: Execute operation (follow the operation-specific steps below) +``` + +--- + +## Step 0: Check Auth + +The script auto-derives its API URL from `~/.sealos/auth.json` (saved by login) +and reads credentials from `~/.sealos/kubeconfig`. No separate config file needed. + +1. Run `node scripts/sealos-db.mjs list` +2. If works → skip Step 1. Greet with context: + > Connected to Sealos. You have N databases. +3. If fails (not authenticated, 401, connection error) → proceed to Step 1 + +--- + +## Step 1: Authenticate + +Run this step only if Step 0 failed. + +### 1a. OAuth2 Login + +Read `config.json` (in this skill's directory) for available regions and the default. +Ask the user which region to connect to using `AskUserQuestion` with the regions as options. + +Run `node scripts/sealos-auth.mjs login {region_url}` (omit region_url for default). + +This command: +1. Opens the user's browser to the Sealos authorization page +2. Displays a user code and verification URL in stderr +3. Polls until the user approves (max 10 minutes) +4. Exchanges the token for a kubeconfig +5. Saves to `~/.sealos/kubeconfig` and `~/.sealos/auth.json` (with region) + +Display while waiting: +> Opening browser for Sealos login... Approve the request in your browser. + +**If TLS error**: Retry with `node scripts/sealos-auth.mjs login --insecure` + +**If other error**: +`AskUserQuestion`: +- header: "Login Failed" +- question: "Browser login failed. Try again?" +- options: ["Try again", "Cancel"] + +### 1b. Verify connection + +After login, run `node scripts/sealos-db.mjs list` to verify auth works. + +**If auth error (401):** Token may have expired. Re-run `node scripts/sealos-auth.mjs login`. + +**If success:** Display: +> Connected to Sealos. You have N databases. + +Use the databases list in Step 3 instead of making a separate `list` call. + +--- + +## Step 2: Route + +Determine the operation from user intent: + +| Intent | Operation | +|--------|-----------| +| "create/deploy/set up a database" | Create | +| "list/show my databases" | List | +| "check status/connection info" | Get | +| "scale/resize/update resources" | Update | +| "delete/remove database" | Delete | +| "start/stop/restart/public access" | Action | +| "backup/restore/list backups" | Backup | +| "show logs/check errors/slow queries" | Logs | + +If ambiguous, ask one clarifying question. + +--- + +## Step 3: Operations + +### Create + +**3a. Versions & existing databases** + +Use databases from the `list` response (Step 0 or 1b). Run `node scripts/sealos-db.mjs list-versions` +to get available versions. + +**3b. Ask name first** + +`AskUserQuestion`: +- header: "Name" +- question: "Database name?" +- options: generate 2-3 name suggestions from project dir + detected type + (see `references/defaults.md` for name suffix rules). + If a name already exists (from list), avoid it and note the conflict. +- Constraint: `^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`, max 63 chars + +**3c. Show recommended config and confirm** + +Read `references/defaults.md` for type recommendation rules, resource presets, +and termination policy defaults. + +Auto-resolve config from context: +- User's request (e.g., "create a pg" → type is postgresql) +- Project tech stack (e.g., Next.js → recommend postgresql) +- Scale hints (e.g., "production database" → higher resources) + +Display the **recommended config summary** (all fields from the create API): + +``` +Database config: + + Name: my-app-pg + Type: PostgreSQL (recommended for web apps) + Version: postgresql-16.4.0 (latest) + CPU: 1 Core + Memory: 1 GB + Storage: 3 GB + Replicas: 3 + Termination: delete (data volumes kept) +``` + +Then `AskUserQuestion`: +- header: "Config" +- question: "Create with this config?" +- options: + 1. "Create now (Recommended)" — accept all, proceed to 3d + 2. "Customize" — go to 3c-customize flow + +**3c-customize:** Read `references/create-flow.md` and follow the customize flow there. +It walks through field selection (Type & Version, Resources, Replicas, Termination) +with `AskUserQuestion` for each selected group. After all changes, re-display the +updated config summary and confirm. + +**3d. Create and wait** + +Build JSON body: +```json +{"name":"my-db","type":"postgresql","version":"postgresql-16.4.0","quota":{"cpu":1,"memory":1,"storage":3,"replicas":3},"terminationPolicy":"delete"} +``` + +Run `node scripts/sealos-db.mjs create-wait ''`. This single command creates the +database and polls until `running` (timeout 2 minutes). The response includes connection info. + +**3e. Show connection info and offer integration** + +Display connection details (host, port, username, password, connection string). + +Then `AskUserQuestion`: +- header: "Integration" +- question: "Write connection info to your project?" +- options: + 1. "Add to .env (Recommended)" — append to .env file + 2. "Add to docker-compose.yml" — add service/env vars + 3. "Auto-detect framework config" — detect and write to framework-specific config + 4. "Skip" — just show the info, don't write anything +- When writing to `.env`, append, don't overwrite. + +--- + +### List + +Run `node scripts/sealos-db.mjs list`. Format as table: + +``` +Name Type Version Status CPU Mem Storage Replicas +my-app-db postgresql postgresql-14.8.0 Running 1 2GB 5GB 1 +cache redis redis-7.0.6 Running 1 1GB 3GB 1 +``` + +Highlight abnormal statuses (Failed, Stopped). + +--- + +### Get + +If no name given, run List first, then `AskUserQuestion` with database names as options +(header: "Database", question: "Which database?"). + +Run `node scripts/sealos-db.mjs get {name}`. Display: name, type, version, status, quota, connection info. + +--- + +### Update + +**3a.** If no name given → List, then `AskUserQuestion` to pick which database +(options = database names from list). + +**3b.** Run `node scripts/sealos-db.mjs get {name}`, show current specs. + +**3c.** `AskUserQuestion` (header: "Update", question: "What to change?", multiSelect: true): +- "CPU" / "Memory" / "Storage" / "Replicas" +- For each selected field, follow up with `AskUserQuestion` offering allowed values as options. + See `references/api-reference.md` for allowed values per field. + +**3d.** Show before/after diff, then `AskUserQuestion` (header: "Confirm", +question: "Apply these changes?"): +- "Apply (Recommended)" +- "Edit again" +- "Cancel" + +**3e.** Run `node scripts/sealos-db.mjs update {name} '{json}'`. + +--- + +### Delete + +**This is destructive. Maximum friction.** + +**3a.** If no name given → List, then `AskUserQuestion` to pick which database. + +**3b.** Run `node scripts/sealos-db.mjs get {name}`, show full details + termination policy. + +**3c.** Explain consequences: +- `delete` policy: cluster removed, data volumes kept +- `wipeout` policy: everything removed, irreversible + +**3d.** `AskUserQuestion`: +- header: "Confirm Delete" +- question: "Type `{name}` to permanently delete this database" +- options: ["Cancel"] — do NOT include the database name as a clickable option. + The user must type the exact name via "Type something" to confirm. + +If user types the correct name → proceed to 3e. +If user types something else → reply "Name doesn't match" and re-ask. +If user clicks Cancel → abort. + +**3e.** Run `node scripts/sealos-db.mjs delete {name}`. + +--- + +### Action (Start/Pause/Restart/Public Access) + +**3a.** If no name given → List, then `AskUserQuestion` to pick which database. + +**3b.** `AskUserQuestion` to confirm (header: "Action", question: "Confirm {action} on {name}?"): +- "{Action} now" +- "Cancel" +- For `enable-public`, add description warning about internet exposure. + +**3c.** Run `node scripts/sealos-db.mjs {action} {name}`. +**3d.** For `start`: poll `node scripts/sealos-db.mjs get {name}` until `running`. +For `enable-public`: re-fetch and display `publicConnection`. + +--- + +### Backup + +**3a.** If no database name given → List, then `AskUserQuestion` to pick which database +(options = database names from list). + +**3b.** `AskUserQuestion` (header: "Backup", question: "What would you like to do?"): +- "List backups" +- "Create backup" +- "Restore from backup" +- "Delete backup" + +**3c.** Read `references/backup-flow.md` and follow the sub-operation steps there. + +--- + +### Logs + +**3a.** If no database name given → List, then `AskUserQuestion` to pick which database +(options = database names from list). + +**3b.** Get database details: run `node scripts/sealos-db.mjs get {name}` to determine +the database type. Map the type to log dbType parameter: +- `postgresql` → `postgresql` +- `apecloud-mysql` → `mysql` +- `mongodb` → `mongodb` +- `redis` → `redis` +- Other types → inform user that logs are only supported for mysql, mongodb, redis, postgresql + +**3c.** `AskUserQuestion` (header: "Log Type", question: "Which logs?"): +- "Runtime logs" — logType: `runtimeLog` +- "Slow queries" — logType: `slowQuery` +- "Error logs" — logType: `errorLog` + +**3d.** List log files: run `node scripts/sealos-db.mjs log-files {podName} {dbType} {logType}`. + +The `podName` is constructed as `{name}-{type}-0` where `{name}` is the database name +and `{type}` is the database type from the `get` response (e.g., `my-db-postgresql-0`). +For MySQL, use the API type: `my-db-apecloud-mysql-0`. + +Display available log files. If multiple files, `AskUserQuestion` with file paths as options +(header: "Log File", question: "Which log file?"). If only one, use it directly. + +**3e.** Fetch logs: run `node scripts/sealos-db.mjs logs {podName} {dbType} {logType} {logPath}`. + +Display log entries in a readable format: +``` +[2024-01-15 02:00:00] [LOG] checkpoint starting: xlog +[2024-01-15 02:00:01] [ERROR] connection reset by peer +``` + +If `hasMore` is true in the response metadata, offer to fetch more: +`AskUserQuestion` (header: "More Logs", question: "Load more log entries?"): +- "Next page" +- "Done" + +--- + +## Scripts + +Two entry points in `scripts/` (relative to this skill's directory): +- `sealos-auth.mjs` — OAuth2 Device Grant login (shared across all skills) +- `sealos-db.mjs` — Database operations + +**Auth commands:** +```bash +node $SCRIPTS/sealos-auth.mjs check # Check if authenticated +node $SCRIPTS/sealos-auth.mjs login # Start OAuth2 login +node $SCRIPTS/sealos-auth.mjs login --insecure # Skip TLS verification +node $SCRIPTS/sealos-auth.mjs info # Show auth details +``` + +Zero external dependencies (Node.js only). TLS verification is disabled for self-signed certs. + +**The scripts are bundled with this skill — do NOT check if they exist. Just run them.** + +**Path resolution:** Scripts are in this skill's `scripts/` directory. The full path is +listed in the system environment's "Additional working directories" — use it directly. + +**Config resolution:** The script reads `~/.sealos/auth.json` (region) and `~/.sealos/kubeconfig` +(credentials) — both created by `sealos-auth.mjs login`. + +```bash +# Examples use SCRIPT as placeholder — replace with /scripts/sealos-db.mjs + +# After login, everything just works — API URL derived from auth.json region +node $SCRIPT list-versions +node $SCRIPT list +node $SCRIPT get my-db +node $SCRIPT create-wait '{"name":"my-db","type":"postgresql","version":"postgresql-16.4.0","quota":{"cpu":1,"memory":1,"storage":3,"replicas":3},"terminationPolicy":"delete"}' +node $SCRIPT update my-db '{"quota":{"cpu":2,"memory":4}}' +node $SCRIPT delete my-db +node $SCRIPT start|pause|restart my-db +node $SCRIPT enable-public|disable-public my-db +node $SCRIPT list-backups|create-backup|delete-backup|restore-backup my-db [args] +node $SCRIPT log-files|logs my-db-postgresql-0 postgresql runtimeLog [logPath] [page] [pageSize] +``` + +## Reference Files + +- `references/api-reference.md` — API endpoints, resource constraints, error formats. Read first. +- `references/defaults.md` — Tier presets, type recommendations, config card templates, termination policy. Read for create operations. +- `references/create-flow.md` — Create customize flow (Type, Version, Resources, Replicas, Termination). Read when user selects "Customize" during create. +- `references/backup-flow.md` — Backup sub-operations (list, create, delete, restore). Read for backup operations. +- `references/openapi.json` — Complete OpenAPI spec. Read only for edge cases. + +## Error Handling + +**Treat each error independently.** Do NOT chain unrelated errors. + +| Scenario | Action | +|----------|--------| +| Kubeconfig not found | Run `node scripts/sealos-auth.mjs login` to authenticate | +| Auth error (401) | Kubeconfig expired. Run `node scripts/sealos-auth.mjs login` to re-authenticate. | +| Name conflict (409) | Suggest alternative name | +| Invalid specs | Explain constraint, suggest valid value | +| Storage shrink | Refuse, K8s limitation | +| Creation timeout (>2 min) | Offer to keep polling or check console | +| "Unsupported version" (500) | Retry WITHOUT version field | +| "namespace not found" (500) | Cluster admin kubeconfig; need Sealos user kubeconfig | + +## Rules + +- NEVER ask a question as plain text — ALWAYS use `AskUserQuestion` with options +- NEVER ask user to manually download kubeconfig — always use `scripts/sealos-auth.mjs login` +- NEVER run `test -f` or `ls` on the skill scripts — they are always present, just run them +- NEVER write kubeconfig to `~/.kube/config` — may overwrite user's existing config +- NEVER echo kubeconfig content to output +- NEVER delete without explicit name confirmation +- NEVER construct HTTP requests inline — always use `scripts/sealos-db.mjs` +- When writing to `.env`, append, don't overwrite +- Version must come from `node scripts/sealos-db.mjs list-versions`. If rejected, retry without version field +- MySQL type is `apecloud-mysql`, not `mysql` +- Storage can only expand, never shrink diff --git a/skills/database/config.json b/skills/database/config.json new file mode 100644 index 0000000..08876ee --- /dev/null +++ b/skills/database/config.json @@ -0,0 +1,9 @@ +{ + "client_id": "af993c98-d19d-4bdc-b338-79b80dc4f8bf", + "default_region": "https://gzg.sealos.run", + "regions": [ + "https://gzg.sealos.run", + "https://bja.sealos.run", + "https://hzh.sealos.run" + ] +} diff --git a/skills/database/references/api-reference.md b/skills/database/references/api-reference.md new file mode 100644 index 0000000..0c93a47 --- /dev/null +++ b/skills/database/references/api-reference.md @@ -0,0 +1,246 @@ +# Sealos Database API Reference + +Base URL: `https://dbprovider.{domain}/api/v2alpha` + +> **API Version:** `v2alpha` — centralized in the script constant `API_PATH` (`scripts/sealos-db.mjs`). +> If the API version changes, update `API_PATH` in the script; the rest auto-follows. + +## TLS Note + +The script sets `rejectUnauthorized: false` for HTTPS requests because Sealos clusters +may use self-signed TLS certificates. Without this, Node.js would reject connections +to clusters that don't have publicly trusted certificates. + +## Authentication + +All requests require a URL-encoded kubeconfig YAML in the `Authorization` header, +**except** `GET /databases/versions` which requires no authentication. + +``` +Authorization: +``` + +## Supported Database Types + +| Type | Identifier | Default Port | Typical Use | +|------|-----------|------|-------------| +| PostgreSQL | `postgresql` | 5432 | General purpose RDBMS | +| MongoDB | `mongodb` | 27017 | Document database | +| MySQL | `apecloud-mysql` | 3306 | General purpose RDBMS | +| Redis | `redis` | 6379 | Cache, sessions, pub/sub | +| Kafka | `kafka` | 9092 | Event streaming | +| Qdrant | `qdrant` | 6333 | Vector search | +| Nebula | `nebula` | 9669 | Graph database | +| Weaviate | `weaviate` | 8080 | Vector search | +| Milvus | `milvus` | 19530 | Vector search | +| Pulsar | `pulsar` | 6650 | Message queue | +| ClickHouse | `clickhouse` | 8123 | Analytics/OLAP | + +**Note:** MySQL type is `apecloud-mysql`, NOT `mysql`. + +## Resource Constraints + +### Create (POST /databases) + +| Field | Type | Range | Default | +|-------|------|-------|---------| +| cpu | number | enum: 1, 2, 3, 4, 5, 6, 7, 8 | 1 | +| memory | number | 0.1 - 32 GB (continuous range) | 1 | +| storage | number | 1 - 300 GB | 3 | +| replicas | integer | 1 - 20 | 3 | + +### Update (PATCH /databases/{name}) + +| Field | Type | Allowed Values | Notes | +|-------|------|----------------|-------| +| cpu | number | 1, 2, 3, 4, 5, 6, 7, 8 | | +| memory | number | 1, 2, 4, 6, 8, 12, 16, 32 GB | Discrete values only | +| storage | number | 1 - 300 GB | **Expand only, cannot shrink** | +| replicas | integer | 1 - 20 | | + +All update fields are optional -- only provide fields to change. + +## Endpoints + +### POST /databases -- Create + +```json +{ + "name": "my-db", + "type": "postgresql", + "version": "postgresql-14.8.0", // optional, auto-selects latest + "quota": { "cpu": 1, "memory": 1, "storage": 3, "replicas": 1 }, + "terminationPolicy": "delete", // optional, "delete" or "wipeout" + "autoBackup": { ... }, // optional + "parameterConfig": { ... } // optional +} +``` + +Response: `201 Created` -> `{ "name": "my-db", "status": "creating" }` + +### GET /databases -- List All + +Response: `200 OK` -> Array of `{ name, uid, type, version, status, quota }` + +Status values: `Running`, `Stopped`, `Creating`, `Updating`, `Failed`, `Deleting` +> **Note:** List returns capitalized statuses (`Running`), Get returns lowercase (`running`). +> Always compare case-insensitively (e.g., `.toLowerCase() === 'running'`). + +### GET /databases/{name} -- Get Details + +Response: `200 OK` -> Full object with connection info. + +Status values: `creating`, `starting`, `stopping`, `stopped`, `running`, `updating`, +`specUpdating`, `rebooting`, `upgrade`, `verticalScaling`, `volumeExpanding`, `failed`, `unknown`, `deleting` + +Connection info: + +```json +{ + "connection": { + "privateConnection": { + "endpoint": "host:port", + "host": "my-db-postgresql.ns-xxx.svc.cluster.local", + "port": "5432", + "username": "postgres", + "password": "s3cr3tpassword", + "connectionString": "postgresql://postgres:pass@host:5432/postgres" + }, + "publicConnection": null + } +} +``` + +### GET /databases/versions -- List Available Versions + +**No authentication required.** This endpoint uses the server's own service account. + +Response: `200 OK` -> `{ "postgresql": ["postgresql-14.8.0", ...], ... }` + +### PATCH /databases/{name} -- Update Resources + +```json +{ "quota": { "cpu": 2, "memory": 4 } } +``` + +Response: `204 No Content` + +### DELETE /databases/{name} -- Delete + +Response: `204 No Content` (idempotent: returns 204 even if not found) + +### POST /databases/{name}/{action} -- Actions + +Actions: `start`, `pause`, `restart`, `enable-public`, `disable-public` + +Response: `204 No Content` (all idempotent) + +### GET /databases/{name}/backups -- List Backups + +Response: `200 OK` -> Array of `{ name, description, createdAt, status }` + +Status values: `completed`, `inprogress`, `failed`, `unknown`, `running`, `deleting` + +### POST /databases/{name}/backups -- Create Backup + +```json +{ + "description": "pre-migration snapshot", + "name": "my-custom-backup-name" +} +``` + +Both fields are optional. Description max 31 characters (Kubernetes label limit). +Name auto-generated if omitted. + +Response: `204 No Content` + +### DELETE /databases/{name}/backups/{backupName} -- Delete Backup + +Response: `204 No Content` + +### POST /databases/{name}/backups/{backupName}/restore -- Restore from Backup + +Creates a **new** database instance from the backup. The original database is not affected. + +```json +{ + "name": "my-db-restored", + "replicas": 3 +} +``` + +Both fields are optional. Name auto-generated if omitted. Replicas inherited from source if omitted. + +Response: `204 No Content` + +### GET /logs -- Get Database Log Entries + +Query parameters (all required unless noted): +- `podName` (required) — Pod name to retrieve logs from +- `dbType` (required) — `mysql`, `mongodb`, `redis`, or `postgresql` +- `logType` (required) — `runtimeLog`, `slowQuery`, or `errorLog` +- `logPath` (required) — Absolute path to the log file within the pod +- `page` (optional, default: 1) — Page number for pagination +- `pageSize` (optional, default: 100, max: 1000) — Entries per page + +Response: `200 OK` + +```json +{ + "code": 200, + "message": "ok", + "data": { + "logs": [ + { "timestamp": "2024-01-15T02:00:00Z", "level": "LOG", "content": "checkpoint starting" } + ], + "metadata": { + "total": 1500, + "page": 1, + "pageSize": 100, + "processingTime": "50ms", + "hasMore": true + } + } +} +``` + +### GET /logs/files -- List Log Files + +Query parameters (all required): +- `podName` (required) — Pod name to list log files from +- `dbType` (required) — `mysql`, `mongodb`, `redis`, or `postgresql` +- `logType` (required) — `runtimeLog`, `slowQuery`, or `errorLog` + +Response: `200 OK` + +```json +{ + "code": 200, + "message": "ok", + "data": [ + { + "name": "postgresql.log", + "path": "/var/log/postgresql/postgresql.log", + "dir": "/var/log/postgresql", + "size": 1048576, + "updateTime": "2024-01-15T02:00:00Z" + } + ] +} +``` + +## Error Response Format + +```json +{ + "error": { + "type": "validation_error", + "code": "INVALID_PARAMETER", + "message": "...", + "details": [...] + } +} +``` + +Types: `validation_error`, `resource_error`, `internal_error` diff --git a/skills/database/references/backup-flow.md b/skills/database/references/backup-flow.md new file mode 100644 index 0000000..44205e4 --- /dev/null +++ b/skills/database/references/backup-flow.md @@ -0,0 +1,55 @@ +# Backup Operations + +## List backups + +Run `node scripts/sealos-db.mjs list-backups {name}`. Format as table: + +``` +Name Status Created +my-db-backup-20240115 completed 2024-01-15T02:00:00Z +my-db-backup-20240120 completed 2024-01-20T02:00:00Z +``` + +Highlight non-completed statuses (inprogress, failed). + +## Create backup + +`AskUserQuestion` (header: "Backup Description", question: "Optional description? (max 31 chars)"): +- "Skip (no description)" +- "Pre-migration backup" +- "Manual snapshot" + +Run `node scripts/sealos-db.mjs create-backup {name} '{"description":"..."}'` (omit body if skipped). +Confirm success. + +## Delete backup + +Run `node scripts/sealos-db.mjs list-backups {name}` first to show available backups. +`AskUserQuestion` with backup names as options (header: "Delete Backup", question: "Which backup to delete?"). +Then confirm: +`AskUserQuestion` (header: "Confirm", question: "Delete backup '{backupName}'?"): +- "Delete" +- "Cancel" + +Run `node scripts/sealos-db.mjs delete-backup {name} {backupName}`. + +## Restore from backup + +Run `node scripts/sealos-db.mjs list-backups {name}` first to show available backups. +`AskUserQuestion` with backup names (status=completed only) as options +(header: "Restore", question: "Which backup to restore from?"). + +Then `AskUserQuestion` (header: "Restore Config", question: "Restore with defaults?"): +- "Restore now (auto-name, same replicas)" — proceed with empty body +- "Customize name & replicas" + +If customize: ask for new database name and replica count via `AskUserQuestion`. + +Warn that restore creates a **new** database instance: +> This will create a new database from the backup. The original database is not affected. + +`AskUserQuestion` (header: "Confirm Restore", question: "Restore from '{backupName}'?"): +- "Restore now" +- "Cancel" + +Run `node scripts/sealos-db.mjs restore-backup {name} {backupName} '{"name":"...","replicas":N}'`. diff --git a/skills/database/references/create-flow.md b/skills/database/references/create-flow.md new file mode 100644 index 0000000..04cbe78 --- /dev/null +++ b/skills/database/references/create-flow.md @@ -0,0 +1,93 @@ +# Create Customize Flow + +When user selects "Customize" in step 3d, follow this flow. + +## Pick fields to change, then configure only those + +`AskUserQuestion`: +- header: "Customize" +- question: "Which fields do you want to change?" +- multiSelect: true +- options: **(max 4 items)** — group the 7 fields into 4: + - "Type & Version — {current_type} {current_version}" + - "Resources (CPU, Memory, Storage) — {cpu}C / {mem}GB / {storage}GB" + - "Replicas — {current_replicas}" + - "Termination — {current_policy}" + +When "Type & Version" selected → ask Type (step 1), then Version (step 2). +When "Resources" selected → ask CPU (step 3), Memory (step 4), Storage (step 5) sequentially. +Fields not selected keep their current values. + +**1) Type** — First output ALL available types from `list-versions` as a numbered text list. +Then `AskUserQuestion`: +- header: "Type" +- question: "Database type?" +- options: **(max 4 items)** — top 4 types for the project context + (see `references/defaults.md`), mark current with "(current)". + User can type any other type name/number via "Type something". +- Example options array (for a Next.js project where current is postgresql): + ``` + ["PostgreSQL (current)", + "MongoDB", + "Redis", + "MySQL"] + ``` +- After type change: auto-update version to latest for new type + +**2) Version** — First output ALL available versions for the chosen type as a numbered text list. +Then `AskUserQuestion`: +- header: "Version" +- question: "Which version?" +- options: **(max 4 items)** — latest 4 versions for the chosen type from `list-versions`, + mark latest with "(latest)". + User can type any other version via "Type something". +- Example options array: + ``` + ["postgresql-16.4.0 (latest)", + "postgresql-15.7.0", + "postgresql-14.12.0", + "postgresql-13.15.0"] + ``` + +**3) CPU** → `AskUserQuestion`: +- header: "CPU" +- question: "CPU cores? (1-8)" +- options: **(max 4 items)** — `1 (current), 2, 4, 8` cores. + Mark current with "(current)". + +**4) Memory** → `AskUserQuestion`: +- header: "Memory" +- question: "Memory? (0.1-32 GB)" +- options: **(max 4 items)** — `1 (current), 2, 4, 8` GB. + Mark current with "(current)". + +**5) Storage** → `AskUserQuestion`: +- header: "Storage" +- question: "Storage? (1-300 GB)" +- options: **(max 4 items)** — `3 (current), 10, 20, 50` GB. + Mark current with "(current)". + +**6) Replicas** → `AskUserQuestion`: +- header: "Replicas" +- question: "Replicas? (1-20)" +- options: **(max 4 items)** — `1 (current), 2, 3, 5`. + Mark current with "(current)". + +**7) Termination policy** → `AskUserQuestion`: +- header: "Termination" +- question: "Termination policy? (cannot be changed after creation)" +- options: + 1. "delete (Recommended)" — description: "Cluster removed, data volumes (PVC) kept" + 2. "wipeout" — description: "Everything removed including data, irreversible" + +After all fields, re-display the updated config summary and `AskUserQuestion`: +- header: "Config" +- question: "Create with this config?" +- options: + 1. "Create now (Recommended)" + 2. "Customize" — re-run the customize flow + +Constraints: +- MySQL type is `apecloud-mysql`, not `mysql` +- Termination policy is set at creation and **cannot be changed later** +- `multiSelect: true` works with up to 4 options — the implicit "Type something" option is always appended by the tool diff --git a/skills/database/references/defaults.md b/skills/database/references/defaults.md new file mode 100644 index 0000000..3b784b8 --- /dev/null +++ b/skills/database/references/defaults.md @@ -0,0 +1,113 @@ +# Sealos DB Defaults & Presets + +## Resource Presets (internal) + +Used to set initial default values based on user intent. These are NOT shown +to the user as "tiers" — the user sees individual CPU/Memory/Storage/Replicas fields. + +| Scenario | CPU | Memory | Storage | Replicas | Trigger phrases | +|----------|-----|--------|---------|----------|-----------------| +| Default | 1 | 1 GB | 3 GB | 3 | no size hint, "dev", "testing", "try" | +| Medium | 2 | 2 GB | 10 GB | 1 | "small", "starter" | +| Production | 2 | 4 GB | 20 GB | 3 | "prod", "production", "HA", "high availability" | +| Custom | — | — | — | — | specific numbers like "4 cores, 8g memory" | + +> **Why Default has 3 replicas but Medium has 1:** Default is the safe fallback when +> intent is unclear — 3 replicas ensure the user doesn't accidentally create a +> single-point-of-failure. Medium is for users who explicitly said "small/starter", +> signaling they accept fewer replicas for lower cost. + +## Type Recommendation Rules + +Match project tech stack to a recommended database type. + +| Tech stack signal | Recommended type | Why | +|-------------------|------------------|-----| +| Web frameworks (Next.js, Rails, Django, Laravel, Spring, Express, FastAPI, etc.) | postgresql | General-purpose RDBMS, best ecosystem support | +| Caching / sessions / rate limiting | redis | In-memory, sub-ms latency | +| Document-heavy / schemaless / flexible schema | mongodb | Native JSON documents | +| Event streaming / log aggregation | kafka | Distributed event log | +| Vector search / AI / embeddings / RAG | qdrant, milvus, or weaviate | Purpose-built vector indexes | +| Analytics / OLAP / time-series aggregation | clickhouse | Columnar storage, fast aggregation | +| Graph relationships / knowledge graphs | nebula | Native graph engine | +| Message queues / pub-sub (non-Kafka) | pulsar | Multi-tenant messaging | + +When multiple types fit, prefer the first match in the table. + +## Config Summary Template + +Display this read-only summary before asking the user to confirm or customize. +Shows every field individually — no "tier" abstraction in the user-facing output. + +``` +Database config: + + Name: [project]-[type-suffix] + Type: [Type] ([reason]) + Version: [version] (latest) + CPU: [n] Core(s) + Memory: [n] GB + Storage: [n] GB + Replicas: [n] +``` + +## Field Generation Rules + +- **Type list**: always derive from `sealos-db.mjs list-versions` output, not hardcoded +- **Type suffix for name**: pg, mongo, mysql, redis, kafka, qdrant, milvus, weaviate, ch, nebula, pulsar +- **Version**: use the latest (first) version from `list-versions` for the chosen type +- **Name**: `[project-directory-name]-[type-suffix]`, lowercased, truncated to 63 chars + +## AskUserQuestion Option Guidelines + +**Hard limit: max 4 options per `AskUserQuestion` call.** The tool auto-appends implicit +options ("Type something", "Chat about this") which consume slots. More than 4 user-provided +options will be truncated and invisible to the user. This limit applies regardless of +whether `multiSelect` is enabled — `multiSelect: true` allows choosing multiple from the +same max-4 option list. + +When building options for `AskUserQuestion`: +- **Name options**: generate 2-3 name suggestions from project dir + type. If a name + already exists (from list), avoid it and note the conflict. +- **Type options**: Always output ALL types as a numbered text list first, then + AskUserQuestion with max 4 clickable options (top 4 types for the context). + Mark recommended with "(Recommended)". User can type any other type name/number. +- **Version options**: Always output ALL versions as a numbered text list first, then + AskUserQuestion with max 4 clickable options (latest 4 versions). + Mark latest with "(latest)". User can type any other version. +- **CPU options**: max 4 items: 1, 2, 4, 8 cores. +- **Memory options**: max 4 items: 1, 2, 4, 8 GB. +- **Storage options**: max 4 items: 3, 10, 20, 50 GB. +- **Replicas options**: max 4 items: 1, 2, 3, 5. +- For all resource options, mark current value with "(current)". + User can type other valid values via "Type something". +- **Database picker** (for get/update/delete/action): list database names from + `sealos-db.mjs list` as options, up to 4. If more than 4, show most recent ones. + +## Termination Policy + +| Policy | Behavior | Default | +|--------|----------|---------| +| `delete` | Cluster removed, PVC data volumes kept | Yes | +| `wipeout` | Everything removed, irreversible | No | + +**Cannot be changed after creation.** Always shown in config summary and available +in the customize flow. Default to `delete` in the recommended config. + +## Create API Field Reference (from openapi.json) + +| Field | Required | Type | Constraint | Default | +|-------|----------|------|------------|---------| +| name | yes | string | `^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`, max 63 | — | +| type | yes | string | enum from `list-versions` | — | +| version | no | string | from `list-versions` | latest | +| quota.cpu | yes | number | enum: 1,2,3,4,5,6,7,8 | 1 | +| quota.memory | yes | number | range: 0.1–32 GB | 1 | +| quota.storage | yes | number | range: 1–300 GB | 3 | +| quota.replicas | yes | integer | range: 1–20 | 3 | +| terminationPolicy | no | string | enum: delete, wipeout | delete | +| autoBackup | no | object | see openapi.json | none | +| parameterConfig | no | object | DB-specific | none | + +**Note:** Memory for create is a continuous range (0.1–32), +while update is discrete enum (1,2,4,6,8,12,16,32). diff --git a/skills/database/references/openapi.json b/skills/database/references/openapi.json new file mode 100644 index 0000000..1cdbfba --- /dev/null +++ b/skills/database/references/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"Database API","version":"2.0.0-alpha","description":"Manage database instances (PostgreSQL, MySQL, Redis, MongoDB, and more) on Kubernetes via KubeBlocks.\n\n## Authentication\n\nAll endpoints require a URL-encoded kubeconfig in the `Authorization` header. Encode with `encodeURIComponent(kubeconfigYaml)` before setting the header value. Obtain your kubeconfig from the Sealos console.\n\n## Errors\n\nAll error responses use a unified format:\n\n```json\n{\n \"error\": {\n \"type\": \"validation_error\",\n \"code\": \"INVALID_PARAMETER\",\n \"message\": \"...\",\n \"details\": [...]\n }\n}\n```\n\n- `type` — high-level category (e.g. `validation_error`, `resource_error`, `internal_error`)\n- `code` — stable identifier for programmatic handling\n- `message` — human-readable explanation\n- `details` — optional extra context; shape varies by `code` (field list, string, or object)\n\n## Operations\n\n**Query** (read-only): returns `200 OK` with data in the response body.\n\n**Mutation** (write):\n\n- **Create** → `201 Created` with `{ \"name\": \"...\", \"status\": \"creating\" }`. The Kubernetes resource is created synchronously; the cluster is provisioned in the background. Poll `GET /databases/{name}` until `status` is `\"Running\"`.\n- **Update / Delete / Action** → `204 No Content` with no response body."},"servers":[{"url":"http://localhost:3000/api/v2alpha","description":"Local development"},{"url":"https://dbprovider.example.com/api/v2alpha","description":"Production"},{"url":"{baseUrl}/api/v2alpha","description":"Custom","variables":{"baseUrl":{"default":"https://dbprovider.example.com","description":"Base URL of your instance (e.g. https://dbprovider.192.168.x.x.nip.io)"}}}],"security":[{"kubeconfigAuth":[]}],"tags":[{"name":"Query","description":"Read-only operations. Success: `200 OK` with data in the response body."},{"name":"Mutation","description":"Write operations. Create: `201 Created` with `{ name, status: \"creating\" }` (K8s resource created synchronously; poll GET to confirm running). Update/Delete/Action: `204 No Content`."}],"paths":{"/databases":{"post":{"summary":"Create database","description":"Provisions a new database cluster. The database is created asynchronously — the request returns immediately and the cluster becomes available shortly after.\n\n**Example — PostgreSQL with daily backup:**\n```json\n{\n \"name\": \"my-postgres-db\",\n \"type\": \"postgresql\",\n \"version\": \"postgresql-14.8.0\",\n \"quota\": { \"cpu\": 1, \"memory\": 2, \"storage\": 5, \"replicas\": 1 },\n \"autoBackup\": {\n \"start\": true,\n \"type\": \"day\",\n \"hour\": \"02\",\n \"minute\": \"00\",\n \"saveTime\": 7,\n \"saveType\": \"days\"\n }\n}\n```\n\n**Example — Redis (minimal):**\n```json\n{\n \"name\": \"my-redis\",\n \"type\": \"redis\",\n \"quota\": { \"cpu\": 1, \"memory\": 1, \"storage\": 3, \"replicas\": 1 }\n}\n```","operationId":"createDatabase","tags":["Mutation"],"requestBody":{"required":true,"description":"Database configuration including type, version, resources, and optional backup/parameter settings","content":{"application/json":{"schema":{"type":"object","required":["name","type","quota"],"properties":{"terminationPolicy":{"type":"string","enum":["delete","wipeout"],"default":"delete","description":"Cluster termination policy. \"delete\" removes the cluster but keeps PVCs, \"wipeout\" removes everything including data. Defaults to \"delete\" if not provided.","example":"delete"},"name":{"type":"string","minLength":1,"maxLength":63,"pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","description":"Database name. Must be a valid Kubernetes resource name (lowercase alphanumeric and hyphens)","example":"my-postgres-db"},"type":{"type":"string","enum":["postgresql","mongodb","apecloud-mysql","redis","kafka","qdrant","nebula","weaviate","milvus","pulsar","clickhouse"],"description":"Database type/engine to deploy","example":"postgresql"},"version":{"type":"string","description":"Database version (e.g., \"14.8.0\" for PostgreSQL). Must match available versions from /databases/versions endpoint. If not provided, the latest version for the specified database type will be automatically selected.","example":"postgresql-14.8.0"},"quota":{"type":"object","required":["cpu","memory","storage","replicas"],"description":"Resource allocation for the database cluster. All four fields are required.","properties":{"cpu":{"type":"number","enum":[1,2,3,4,5,6,7,8],"description":"CPU cores allocated to each database instance. Allowed values: 1, 2, 3, 4, 5, 6, 7, or 8 (converted to millicores in Kubernetes)","default":1,"example":1},"memory":{"type":"number","minimum":0.1,"maximum":32,"description":"Memory in GB allocated to each database instance - range [0.1, 32] (automatically converted to Gi in Kubernetes)","default":1,"example":2},"storage":{"type":"number","minimum":1,"maximum":300,"description":"Persistent storage in GB for each database instance (automatically converted to Gi in Kubernetes)","default":3,"example":5},"replicas":{"type":"integer","minimum":1,"maximum":20,"description":"Number of database replicas for high availability","default":3,"example":1}}},"autoBackup":{"type":"object","description":"Automatic backup configuration (optional). If not provided, no automatic backups will be configured","properties":{"start":{"type":"boolean","description":"Enable automatic backups","example":true},"type":{"type":"string","enum":["day","hour","week"],"description":"Backup frequency type","example":"day"},"week":{"type":"array","items":{"type":"string","enum":["monday","tuesday","wednesday","thursday","friday","saturday","sunday"]},"description":"Days of the week to run backups (for weekly backups)","example":["monday"]},"hour":{"type":"string","pattern":"^([01]?[0-9]|2[0-3])$","description":"Hour to run backup (24-hour format, 00-23)","example":"02"},"minute":{"type":"string","pattern":"^[0-5]?[0-9]$","description":"Minute to run backup (00-59)","example":"00"},"saveTime":{"type":"number","minimum":1,"maximum":365,"description":"Backup retention duration","example":7},"saveType":{"type":"string","enum":["days","hours","weeks","months"],"description":"Backup retention unit","example":"days"}}},"parameterConfig":{"type":"object","description":"Database-specific parameter configuration (optional). Available parameters vary by database type","properties":{"maxConnections":{"type":"string","description":"Maximum number of database connections","example":"100"},"timeZone":{"type":"string","description":"Database timezone (e.g., \"Asia/Shanghai\", \"UTC\")","example":"Asia/Shanghai"},"lowerCaseTableNames":{"type":"string","enum":["0","1"],"description":"MySQL-specific: whether table names are case-sensitive. 0=case-sensitive, 1=case-insensitive","example":"0"},"maxmemory":{"type":"string","description":"Redis-specific: maximum memory usage in bytes","example":"512mb"}}}}}}}},"responses":{"201":{"description":"Database created. The Kubernetes Cluster resource has been created and provisioning is underway. Poll `GET /databases/{databaseName}` until `status` is `\"Running\"`.","content":{"application/json":{"example":{"name":"my-postgres-db","status":"creating"},"schema":{"type":"object","required":["name","status"],"properties":{"name":{"type":"string","description":"Name of the created database","example":"my-postgres-db"},"status":{"type":"string","enum":["creating"],"description":"Initial provisioning status — always \"creating\" at creation time","example":"creating"}}}}}},"400":{"description":"Bad request — invalid body, resource values, or parameter configuration","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["validation_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["INVALID_PARAMETER","INVALID_VALUE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"description":"Field path using dot/bracket notation, e.g. \"resource.cpu\"","type":"string"},"message":{"description":"Validation error message for this field","type":"string"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"invalidParameter":{"summary":"Missing required field","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Request body validation failed.","details":[{"field":"quota","message":"Required"}]}}},"invalidValue":{"summary":"Invalid CPU value","value":{"error":{"type":"validation_error","code":"INVALID_VALUE","message":"Invalid CPU value. Must be one of: 1, 2, 3, 4, 5, 6, 7, 8 cores."}}}}}}},"401":{"description":"Unauthorized — invalid or missing kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authentication_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Unauthorized, please login again."}}}}}}},"403":{"description":"Forbidden — insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authorization_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["PERMISSION_DENIED","INSUFFICIENT_BALANCE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"permissionDenied":{"summary":"Insufficient permissions","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Insufficient permissions to perform this operation.","details":"clusters.apps.kubeblocks.io is forbidden: User \"system:serviceaccount:ns-abc\" cannot create resource \"clusters\" in API group \"apps.kubeblocks.io\""}}},"insufficientBalance":{"summary":"Insufficient account balance","value":{"error":{"type":"authorization_error","code":"INSUFFICIENT_BALANCE","message":"Insufficient balance to perform this operation.","details":"admission webhook \"account.sealos.io\" denied the request: account balance less than 0"}}}}}}},"409":{"description":"Conflict — database with this name already exists","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"resource_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["ALREADY_EXISTS","CONFLICT"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"alreadyExists":{"summary":"Database already exists","value":{"error":{"type":"resource_error","code":"ALREADY_EXISTS","message":"Resource already exists.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\" already exists"}}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["operation_error","internal_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["KUBERNETES_ERROR","OPERATION_FAILED","INTERNAL_ERROR"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"A Kubernetes API call failed.","details":"admission webhook \"resource.sealos.io\" denied the request: resource quota exceeded"}}}}}}},"503":{"description":"Service unavailable — Kubernetes cluster unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"internal_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"SERVICE_UNAVAILABLE"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"The Kubernetes cluster is temporarily unreachable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}},"get":{"summary":"List all databases","description":"Returns all database clusters in the current namespace with their status and resource allocation.","operationId":"listDatabases","tags":["Query"],"responses":{"200":{"description":"List of databases retrieved successfully","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Database name","example":"my-postgres-db"},"uid":{"type":"string","description":"Unique identifier","example":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"},"type":{"type":"string","description":"Database engine","example":"postgresql"},"version":{"type":"string","description":"Database version","example":"postgresql-14.8.0"},"resourceType":{"type":"string","description":"Resource type identifier — always \"cluster\"","example":"cluster"},"status":{"type":"string","enum":["Running","Stopped","Creating","Updating","Failed","Deleting"],"description":"Current cluster status","example":"Running"},"quota":{"type":"object","description":"Resource allocation for each database replica","properties":{"cpu":{"type":"number","description":"CPU cores per replica","example":1},"memory":{"type":"number","description":"Memory in GB per replica","example":2},"storage":{"type":"number","description":"Storage in GB per replica","example":5},"replicas":{"type":"integer","description":"Number of database replicas","example":1}}}}}}}}},"401":{"description":"Unauthorized — invalid or missing kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authentication_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Unauthorized, please login again."}}}}}}},"403":{"description":"Forbidden — insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authorization_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["PERMISSION_DENIED","INSUFFICIENT_BALANCE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"permissionDenied":{"summary":"Insufficient permissions","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Insufficient permissions to perform this operation.","details":"clusters.apps.kubeblocks.io is forbidden: User \"system:serviceaccount:ns-abc\" cannot get resource \"clusters\" in API group \"apps.kubeblocks.io\""}}},"insufficientBalance":{"summary":"Insufficient account balance","value":{"error":{"type":"authorization_error","code":"INSUFFICIENT_BALANCE","message":"Insufficient balance to perform this operation.","details":"admission webhook \"account.sealos.io\" denied the request: account balance less than 0"}}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["operation_error","internal_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["KUBERNETES_ERROR","OPERATION_FAILED","INTERNAL_ERROR"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"A Kubernetes API call failed.","details":"etcdserver: request timed out"}}}}}}},"503":{"description":"Service unavailable — Kubernetes cluster unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"internal_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"SERVICE_UNAVAILABLE"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"The Kubernetes cluster is temporarily unreachable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}}},"/databases/{databaseName}":{"patch":{"summary":"Update database resources","description":"Updates a database's resource allocation (CPU, memory, storage, replicas). Only provide fields you want to change — all fields are optional.\n\nKey points:\n- CPU: one of `1, 2, 3, 4, 5, 6, 7, 8` cores\n- Memory: one of `1, 2, 4, 6, 8, 12, 16, 32` GB\n- Storage: `1–300` GB (can only be expanded, not shrunk)\n- Replicas: `1–20`","operationId":"updateDatabase","tags":["Mutation"],"parameters":[{"name":"databaseName","in":"path","required":true,"description":"Name of the database to operate on (format: lowercase alphanumeric and hyphens)","schema":{"type":"string","minLength":1,"example":"my-postgres-db"}}],"requestBody":{"required":true,"description":"Resource updates to apply. All fields inside `quota` are optional — only provide fields you want to change.","content":{"application/json":{"schema":{"type":"object","required":["quota"],"properties":{"quota":{"$ref":"#/components/schemas/UpdateResourceSchema"}}}}}},"responses":{"204":{"description":"Database update initiated successfully."},"400":{"description":"Bad request — invalid resource values","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["validation_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["INVALID_PARAMETER","INVALID_VALUE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"description":"Field path using dot/bracket notation, e.g. \"resource.cpu\"","type":"string"},"message":{"description":"Validation error message for this field","type":"string"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"invalidParameter":{"summary":"Missing quota field","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Request body validation failed.","details":[{"field":"quota","message":"Required"}]}}},"invalidValue":{"summary":"Invalid CPU value","value":{"error":{"type":"validation_error","code":"INVALID_VALUE","message":"Invalid CPU value. Must be one of: 1, 2, 3, 4, 5, 6, 7, 8 cores."}}}}}}},"401":{"description":"Unauthorized — invalid or missing kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authentication_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Unauthorized, please login again."}}}}}}},"403":{"description":"Forbidden — insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authorization_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["PERMISSION_DENIED","INSUFFICIENT_BALANCE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"permissionDenied":{"summary":"Insufficient permissions","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Insufficient permissions to perform this operation.","details":"opsrequests.apps.kubeblocks.io is forbidden: User \"system:serviceaccount:ns-abc\" cannot create resource \"opsrequests\" in API group \"apps.kubeblocks.io\""}}},"insufficientBalance":{"summary":"Insufficient account balance","value":{"error":{"type":"authorization_error","code":"INSUFFICIENT_BALANCE","message":"Insufficient balance to perform this operation.","details":"admission webhook \"account.sealos.io\" denied the request: account balance less than 0"}}}}}}},"404":{"description":"Not found — database does not exist","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"resource_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"NOT_FOUND"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"notFound":{"summary":"Database not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Database 'my-postgres-db' not found.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\" not found"}}}}}}},"409":{"description":"Conflict — database is currently being updated","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"resource_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["ALREADY_EXISTS","CONFLICT"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"conflict":{"summary":"Update in progress","value":{"error":{"type":"resource_error","code":"CONFLICT","message":"A conflicting operation is already in progress.","details":"opsrequests.apps.kubeblocks.io \"my-postgres-db-verticalscaling\" already exists"}}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["operation_error","internal_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["KUBERNETES_ERROR","OPERATION_FAILED","INTERNAL_ERROR"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"A Kubernetes API call failed.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\": Internal error occurred: etcdserver: request timed out"}}}}}}},"503":{"description":"Service unavailable — Kubernetes cluster unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"internal_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"SERVICE_UNAVAILABLE"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"The Kubernetes cluster is temporarily unreachable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}},"get":{"summary":"Get database details","description":"Returns detailed information about a specific database including its current status, resource allocation, and configuration.","operationId":"getDatabase","tags":["Query"],"parameters":[{"name":"databaseName","in":"path","required":true,"description":"Name of the database to operate on (format: lowercase alphanumeric and hyphens)","schema":{"type":"string","minLength":1,"example":"my-postgres-db"}}],"responses":{"200":{"description":"Database details retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"terminationPolicy":{"type":"string","enum":["delete","wipeout"],"default":"delete","description":"Cluster termination policy. \"delete\" removes the cluster but keeps PVCs, \"wipeout\" removes everything including data.","example":"delete"},"name":{"type":"string","minLength":1,"description":"Database name (Kubernetes resource name — lowercase alphanumeric and hyphens)","example":"my-postgres-db"},"type":{"type":"string","enum":["postgresql","mongodb","apecloud-mysql","redis","kafka","qdrant","nebula","weaviate","milvus","pulsar","clickhouse"],"description":"Database engine type","example":"postgresql"},"version":{"type":"string","description":"Database version string (e.g. \"postgresql-14.8.0\")","example":"postgresql-14.8.0"},"quota":{"type":"object","description":"Resource allocation for each database replica","properties":{"cpu":{"type":"number","description":"CPU cores per replica","example":1},"memory":{"type":"number","description":"Memory in GB per replica","example":2},"storage":{"type":"number","description":"Storage in GB per replica","example":5},"replicas":{"type":"number","description":"Number of database replicas","example":1}}},"autoBackup":{"type":"object","description":"Automatic backup configuration","properties":{"start":{"type":"boolean","description":"Whether automatic backups are enabled","example":true},"type":{"type":"string","enum":["day","hour","week"],"description":"Backup frequency type","example":"day"},"week":{"type":"array","items":{"type":"string","enum":["monday","tuesday","wednesday","thursday","friday","saturday","sunday"]},"description":"Days of the week for weekly backups","example":["monday"]},"hour":{"type":"string","description":"Hour to run backup (24-hour format)","example":"02"},"minute":{"type":"string","description":"Minute to run backup","example":"00"},"saveTime":{"type":"number","description":"Backup retention duration","example":7},"saveType":{"type":"string","enum":["days","hours","weeks","months"],"description":"Backup retention unit","example":"days"}}},"parameterConfig":{"type":"object","description":"Database-specific parameter configuration","properties":{"maxConnections":{"type":"string","description":"Maximum number of database connections","example":"100"},"timeZone":{"type":"string","description":"Database timezone","example":"Asia/Shanghai"},"lowerCaseTableNames":{"type":"string","enum":["0","1"],"description":"MySQL-specific: case sensitivity for table names. 0=case-sensitive, 1=case-insensitive","example":"0"},"maxmemory":{"type":"string","description":"Redis-specific: maximum memory usage","example":"512mb"}}},"uid":{"type":"string","description":"Unique identifier of the database resource","example":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"},"status":{"type":"string","enum":["creating","starting","stopping","stopped","running","updating","specUpdating","rebooting","upgrade","verticalScaling","volumeExpanding","failed","unknown","deleting"],"description":"Current status of the database cluster","example":"running"},"createdAt":{"type":"string","description":"Creation timestamp of the database cluster (ISO 8601)","example":"2024-01-15T10:30:00Z"},"resourceType":{"type":"string","description":"Resource type identifier — always \"cluster\"","default":"cluster","example":"cluster"},"operationalStatus":{"type":"object","description":"Operational status flags from KubeBlocks (structure varies by version)","additionalProperties":true,"example":{}},"connection":{"type":"object","description":"Connection details for the database cluster","properties":{"privateConnection":{"description":"Internal (in-cluster) connection details. null if not yet available.","oneOf":[{"type":"object","properties":{"endpoint":{"type":"string","description":"host:port string for internal cluster access","example":"my-postgres-db-postgresql.ns-abc.svc.cluster.local:5432"},"host":{"type":"string","description":"ClusterIP service hostname (internal only)","example":"my-postgres-db-postgresql.ns-abc.svc.cluster.local"},"port":{"type":"string","description":"Database port","example":"5432"},"username":{"type":"string","description":"Database username","example":"postgres"},"password":{"type":"string","description":"Database password","example":"s3cr3tpassword"},"connectionString":{"type":"string","description":"Ready-to-use connection string","example":"postgresql://postgres:s3cr3tpassword@my-postgres-db-postgresql.ns-abc.svc.cluster.local:5432/postgres"}}},{"type":"null"}]},"publicConnection":{"description":"External connection string via NodePort/LoadBalancer. null if public access is not enabled.","oneOf":[{"type":"string","example":"postgresql://postgres:s3cr3tpassword@203.0.113.1:30001/postgres"},{"type":"null"}]}}},"pods":{"type":"array","description":"List of database pods and their current phase","items":{"type":"object","properties":{"name":{"type":"string","description":"Pod name","example":"my-postgres-db-postgresql-0"},"status":{"type":"string","description":"Pod phase (e.g. \"running\", \"pending\", \"failed\")","example":"running"}}}}}}}}},"400":{"description":"Bad request — invalid path parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["validation_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["INVALID_PARAMETER"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"description":"Field path using dot/bracket notation, e.g. \"resource.cpu\"","type":"string"},"message":{"description":"Validation error message for this field","type":"string"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"invalidParameter":{"summary":"Invalid database name","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid request parameters.","details":[{"field":"databaseName","message":"Required"}]}}}}}}},"401":{"description":"Unauthorized — invalid or missing kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authentication_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Unauthorized, please login again."}}}}}}},"403":{"description":"Forbidden — insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authorization_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["PERMISSION_DENIED","INSUFFICIENT_BALANCE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"permissionDenied":{"summary":"Insufficient permissions","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Insufficient permissions to perform this operation.","details":"clusters.apps.kubeblocks.io is forbidden: User \"system:serviceaccount:ns-abc\" cannot get resource \"clusters\" in API group \"apps.kubeblocks.io\""}}},"insufficientBalance":{"summary":"Insufficient account balance","value":{"error":{"type":"authorization_error","code":"INSUFFICIENT_BALANCE","message":"Insufficient balance to perform this operation.","details":"admission webhook \"account.sealos.io\" denied the request: account balance less than 0"}}}}}}},"404":{"description":"Not found — database does not exist","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"resource_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"NOT_FOUND"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"notFound":{"summary":"Database not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Database 'my-postgres-db' not found.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\" not found"}}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["operation_error","internal_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["KUBERNETES_ERROR","OPERATION_FAILED","INTERNAL_ERROR"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"A Kubernetes API call failed.","details":"etcdserver: request timed out"}}}}}}},"503":{"description":"Service unavailable — Kubernetes cluster unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"internal_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"SERVICE_UNAVAILABLE"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"The Kubernetes cluster is temporarily unreachable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}},"delete":{"summary":"Delete database","description":"Deletes a database cluster. **Irreversible** — depending on `terminationPolicy`, persistent volumes may also be removed. This operation is idempotent: if the database does not exist, `204` is returned.","operationId":"deleteDatabase","tags":["Mutation"],"parameters":[{"name":"databaseName","in":"path","required":true,"description":"Name of the database to operate on (format: lowercase alphanumeric and hyphens)","schema":{"type":"string","minLength":1,"example":"my-postgres-db"}}],"responses":{"204":{"description":"Database deleted (or did not exist)."},"400":{"description":"Bad request — invalid path parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["validation_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["INVALID_PARAMETER"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"description":"Field path using dot/bracket notation, e.g. \"resource.cpu\"","type":"string"},"message":{"description":"Validation error message for this field","type":"string"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"invalidParameter":{"summary":"Invalid database name","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid request parameters.","details":[{"field":"databaseName","message":"Required"}]}}}}}}},"401":{"description":"Unauthorized — invalid or missing kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authentication_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Unauthorized, please login again."}}}}}}},"403":{"description":"Forbidden — insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authorization_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["PERMISSION_DENIED","INSUFFICIENT_BALANCE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"permissionDenied":{"summary":"Insufficient permissions","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Insufficient permissions to perform this operation.","details":"clusters.apps.kubeblocks.io is forbidden: User \"system:serviceaccount:ns-abc\" cannot delete resource \"clusters\" in API group \"apps.kubeblocks.io\""}}},"insufficientBalance":{"summary":"Insufficient account balance","value":{"error":{"type":"authorization_error","code":"INSUFFICIENT_BALANCE","message":"Insufficient balance to perform this operation.","details":"admission webhook \"account.sealos.io\" denied the request: account balance less than 0"}}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["operation_error","internal_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["KUBERNETES_ERROR","OPERATION_FAILED","INTERNAL_ERROR"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"A Kubernetes API call failed.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\": Internal error occurred: etcdserver: request timed out"}}}}}}},"503":{"description":"Service unavailable — Kubernetes cluster unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"internal_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"SERVICE_UNAVAILABLE"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"The Kubernetes cluster is temporarily unreachable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}}},"/databases/{databaseName}/start":{"post":{"summary":"Start database","description":"Resumes a paused or stopped database. This operation is idempotent: if the database is already running, `204` is returned.","operationId":"startDatabase","tags":["Mutation"],"parameters":[{"name":"databaseName","in":"path","required":true,"description":"Name of the database to operate on (format: lowercase alphanumeric and hyphens)","schema":{"type":"string","minLength":1,"example":"my-postgres-db"}}],"responses":{"204":{"description":"Database start initiated (or was already running)."},"400":{"description":"Bad request — invalid path parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["validation_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["INVALID_PARAMETER"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"description":"Field path using dot/bracket notation, e.g. \"resource.cpu\"","type":"string"},"message":{"description":"Validation error message for this field","type":"string"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"invalidParameter":{"summary":"Missing database name","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid request parameters.","details":[{"field":"databaseName","message":"Required"}]}}}}}}},"401":{"description":"Unauthorized — invalid or missing kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authentication_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Unauthorized, please login again."}}}}}}},"403":{"description":"Forbidden — insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authorization_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["PERMISSION_DENIED","INSUFFICIENT_BALANCE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"permissionDenied":{"summary":"Insufficient permissions","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Insufficient permissions to perform this operation.","details":"opsrequests.apps.kubeblocks.io is forbidden: User \"system:serviceaccount:ns-abc\" cannot create resource \"opsrequests\" in API group \"apps.kubeblocks.io\""}}},"insufficientBalance":{"summary":"Insufficient account balance","value":{"error":{"type":"authorization_error","code":"INSUFFICIENT_BALANCE","message":"Insufficient balance to perform this operation.","details":"admission webhook \"account.sealos.io\" denied the request: account balance less than 0"}}}}}}},"404":{"description":"Not found — database does not exist","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"resource_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"NOT_FOUND"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"notFound":{"summary":"Database not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Database 'my-postgres-db' not found.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\" not found"}}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["operation_error","internal_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["KUBERNETES_ERROR","OPERATION_FAILED","INTERNAL_ERROR"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"A Kubernetes API call failed.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\": Internal error occurred: etcdserver: request timed out"}}}}}}},"503":{"description":"Service unavailable — Kubernetes cluster unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"internal_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"SERVICE_UNAVAILABLE"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"The Kubernetes cluster is temporarily unreachable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}}},"/databases/{databaseName}/pause":{"post":{"summary":"Pause database","description":"Gracefully stops a running database while preserving all data and configuration. The database can be resumed with the `start` operation.","operationId":"pauseDatabase","tags":["Mutation"],"parameters":[{"name":"databaseName","in":"path","required":true,"description":"Name of the database to operate on (format: lowercase alphanumeric and hyphens)","schema":{"type":"string","minLength":1,"example":"my-postgres-db"}}],"responses":{"204":{"description":"Database pause initiated successfully."},"400":{"description":"Bad request — invalid path parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["validation_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["INVALID_PARAMETER"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"description":"Field path using dot/bracket notation, e.g. \"resource.cpu\"","type":"string"},"message":{"description":"Validation error message for this field","type":"string"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"invalidParameter":{"summary":"Missing database name","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid request parameters.","details":[{"field":"databaseName","message":"Required"}]}}}}}}},"401":{"description":"Unauthorized — invalid or missing kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authentication_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Unauthorized, please login again."}}}}}}},"403":{"description":"Forbidden — insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authorization_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["PERMISSION_DENIED","INSUFFICIENT_BALANCE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"permissionDenied":{"summary":"Insufficient permissions","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Insufficient permissions to perform this operation.","details":"opsrequests.apps.kubeblocks.io is forbidden: User \"system:serviceaccount:ns-abc\" cannot create resource \"opsrequests\" in API group \"apps.kubeblocks.io\""}}},"insufficientBalance":{"summary":"Insufficient account balance","value":{"error":{"type":"authorization_error","code":"INSUFFICIENT_BALANCE","message":"Insufficient balance to perform this operation.","details":"admission webhook \"account.sealos.io\" denied the request: account balance less than 0"}}}}}}},"404":{"description":"Not found — database does not exist","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"resource_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"NOT_FOUND"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"notFound":{"summary":"Database not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Database 'my-postgres-db' not found.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\" not found"}}}}}}},"409":{"description":"Conflict — database is already paused or a pause operation is in progress","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"resource_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["ALREADY_EXISTS","CONFLICT"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"conflict":{"summary":"Already paused","value":{"error":{"type":"resource_error","code":"CONFLICT","message":"A conflicting operation is already in progress.","details":"opsrequests.apps.kubeblocks.io \"my-postgres-db-stop\" already exists"}}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["operation_error","internal_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["KUBERNETES_ERROR","OPERATION_FAILED","INTERNAL_ERROR"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"A Kubernetes API call failed.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\": Internal error occurred: etcdserver: request timed out"}}}}}}},"503":{"description":"Service unavailable — Kubernetes cluster unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"internal_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"SERVICE_UNAVAILABLE"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"The Kubernetes cluster is temporarily unreachable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}}},"/databases/{databaseName}/restart":{"post":{"summary":"Restart database","description":"Performs a rolling restart of all database replicas. This operation is idempotent: if a restart is already in progress, `204` is returned.","operationId":"restartDatabase","tags":["Mutation"],"parameters":[{"name":"databaseName","in":"path","required":true,"description":"Name of the database to operate on (format: lowercase alphanumeric and hyphens)","schema":{"type":"string","minLength":1,"example":"my-postgres-db"}}],"responses":{"204":{"description":"Database restart initiated (or was already restarting)."},"400":{"description":"Bad request — invalid path parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["validation_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["INVALID_PARAMETER"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"description":"Field path using dot/bracket notation, e.g. \"resource.cpu\"","type":"string"},"message":{"description":"Validation error message for this field","type":"string"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"invalidParameter":{"summary":"Missing database name","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid request parameters.","details":[{"field":"databaseName","message":"Required"}]}}}}}}},"401":{"description":"Unauthorized — invalid or missing kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authentication_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Unauthorized, please login again."}}}}}}},"403":{"description":"Forbidden — insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authorization_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["PERMISSION_DENIED","INSUFFICIENT_BALANCE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"permissionDenied":{"summary":"Insufficient permissions","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Insufficient permissions to perform this operation.","details":"opsrequests.apps.kubeblocks.io is forbidden: User \"system:serviceaccount:ns-abc\" cannot create resource \"opsrequests\" in API group \"apps.kubeblocks.io\""}}},"insufficientBalance":{"summary":"Insufficient account balance","value":{"error":{"type":"authorization_error","code":"INSUFFICIENT_BALANCE","message":"Insufficient balance to perform this operation.","details":"admission webhook \"account.sealos.io\" denied the request: account balance less than 0"}}}}}}},"404":{"description":"Not found — database does not exist","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"resource_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"NOT_FOUND"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"notFound":{"summary":"Database not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Database 'my-postgres-db' not found.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\" not found"}}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["operation_error","internal_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["KUBERNETES_ERROR","OPERATION_FAILED","INTERNAL_ERROR"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"A Kubernetes API call failed.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\": Internal error occurred: etcdserver: request timed out"}}}}}}},"503":{"description":"Service unavailable — Kubernetes cluster unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"internal_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"SERVICE_UNAVAILABLE"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"The Kubernetes cluster is temporarily unreachable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}}},"/databases/{databaseName}/backups":{"get":{"summary":"List database backups","description":"Returns all manual and automatic backups associated with the specified database, including status and creation timestamp.","operationId":"listDatabaseBackups","tags":["Query"],"parameters":[{"name":"databaseName","in":"path","required":true,"description":"Name of the database to operate on (format: lowercase alphanumeric and hyphens)","schema":{"type":"string","minLength":1,"example":"my-postgres-db"}}],"responses":{"200":{"description":"Backups retrieved successfully","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Backup resource name","example":"my-postgres-db-backup-20240115"},"description":{"type":"string","description":"Optional description decoded from backup annotations. Empty string if none was provided.","example":"weekly backup before schema migration"},"createdAt":{"type":"string","format":"date-time","description":"Creation timestamp of the backup resource","example":"2024-01-15T02:00:00Z"},"status":{"type":"string","enum":["completed","inprogress","failed","unknown","running","deleting"],"description":"Current backup status as reported by Kubeblocks (lowercase)","example":"completed"}},"required":["name","description","createdAt","status"]}}}}},"400":{"description":"Bad request — invalid path parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["validation_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["INVALID_PARAMETER"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"description":"Field path using dot/bracket notation, e.g. \"resource.cpu\"","type":"string"},"message":{"description":"Validation error message for this field","type":"string"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"invalidParameter":{"summary":"Invalid database name","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid request parameters.","details":[{"field":"databaseName","message":"Required"}]}}}}}}},"401":{"description":"Unauthorized — invalid or missing kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authentication_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Unauthorized, please login again."}}}}}}},"403":{"description":"Forbidden — insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authorization_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["PERMISSION_DENIED","INSUFFICIENT_BALANCE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"permissionDenied":{"summary":"Insufficient permissions","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Insufficient permissions to perform this operation.","details":"backups.dataprotection.kubeblocks.io is forbidden: User \"system:serviceaccount:ns-abc\" cannot list resource \"backups\" in API group \"dataprotection.kubeblocks.io\""}}},"insufficientBalance":{"summary":"Insufficient account balance","value":{"error":{"type":"authorization_error","code":"INSUFFICIENT_BALANCE","message":"Insufficient balance to perform this operation.","details":"admission webhook \"account.sealos.io\" denied the request: account balance less than 0"}}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["operation_error","internal_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["KUBERNETES_ERROR","OPERATION_FAILED","INTERNAL_ERROR"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"A Kubernetes API call failed.","details":"etcdserver: request timed out"}}}}}}},"503":{"description":"Service unavailable — Kubernetes cluster unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"internal_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"SERVICE_UNAVAILABLE"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"The Kubernetes cluster is temporarily unreachable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}},"post":{"summary":"Create database backup","description":"Initiates a manual backup of the database. The backup is created asynchronously.","operationId":"createDatabaseBackup","tags":["Mutation"],"parameters":[{"name":"databaseName","in":"path","required":true,"description":"Name of the database to operate on (format: lowercase alphanumeric and hyphens)","schema":{"type":"string","minLength":1,"example":"my-postgres-db"}}],"requestBody":{"required":false,"description":"Optional backup configuration including description annotation and explicit backup name","content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string","maxLength":31,"description":"Optional description for the backup. Stored as label on the Backup resource (max 31 characters due to Kubernetes label value limit of 63 characters when hex-encoded)"},"name":{"type":"string","minLength":1,"maxLength":63,"pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","description":"Optional custom backup name. If omitted, a name will be auto-generated using the database name prefix"}},"example":{"description":"description backup","name":"my-db-backup-20231113"}}}}},"responses":{"204":{"description":"Backup creation initiated successfully."},"400":{"description":"Bad request — invalid body, description too long, or unsupported database type","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["validation_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["INVALID_PARAMETER","INVALID_VALUE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"description":"Field path using dot/bracket notation, e.g. \"resource.cpu\"","type":"string"},"message":{"description":"Validation error message for this field","type":"string"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"invalidParameter":{"summary":"Body validation failure","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid request body.","details":[{"field":"name","message":"String must contain at least 1 character(s)"}]}}},"descriptionTooLong":{"summary":"Description exceeds limit","value":{"error":{"type":"validation_error","code":"INVALID_VALUE","message":"Backup description is too long. Maximum 31 characters allowed."}}}}}}},"401":{"description":"Unauthorized — invalid or missing kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authentication_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Unauthorized, please login again."}}}}}}},"403":{"description":"Forbidden — insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authorization_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["PERMISSION_DENIED","INSUFFICIENT_BALANCE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"permissionDenied":{"summary":"Insufficient permissions","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Insufficient permissions to perform this operation.","details":"backups.dataprotection.kubeblocks.io is forbidden: User \"system:serviceaccount:ns-abc\" cannot create resource \"backups\" in API group \"dataprotection.kubeblocks.io\""}}},"insufficientBalance":{"summary":"Insufficient account balance","value":{"error":{"type":"authorization_error","code":"INSUFFICIENT_BALANCE","message":"Insufficient balance to perform this operation.","details":"admission webhook \"account.sealos.io\" denied the request: account balance less than 0"}}}}}}},"404":{"description":"Not found — database does not exist","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"resource_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"NOT_FOUND"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"notFound":{"summary":"Database not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Database 'my-postgres-db' not found.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\" not found"}}}}}}},"409":{"description":"Conflict — a backup is currently in progress","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"resource_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["ALREADY_EXISTS","CONFLICT"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"alreadyExists":{"summary":"Backup already in progress","value":{"error":{"type":"resource_error","code":"ALREADY_EXISTS","message":"Resource already exists.","details":"backups.dataprotection.kubeblocks.io \"my-postgres-db-backup\" already exists"}}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["operation_error","internal_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["KUBERNETES_ERROR","OPERATION_FAILED","INTERNAL_ERROR"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"A Kubernetes API call failed.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\": Internal error occurred: etcdserver: request timed out"}}}}}}},"503":{"description":"Service unavailable — Kubernetes cluster unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"internal_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"SERVICE_UNAVAILABLE"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"The Kubernetes cluster is temporarily unreachable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}}},"/databases/{databaseName}/backups/{backupName}":{"delete":{"summary":"Delete database backup","description":"Deletes a backup. **Irreversible.** This operation is idempotent: if the backup does not exist, `204` is returned.","operationId":"deleteDatabaseBackup","tags":["Mutation"],"parameters":[{"name":"databaseName","in":"path","required":true,"description":"Name of the database that owns the backup (format: lowercase alphanumeric and hyphens)","schema":{"type":"string","minLength":1,"example":"my-postgres-db"}},{"name":"backupName","in":"path","required":true,"description":"Name of the backup to delete (format: lowercase alphanumeric and hyphens)","schema":{"type":"string","minLength":1,"example":"my-postgres-db-backup-20240115"}}],"responses":{"204":{"description":"Backup deleted (or did not exist)."},"400":{"description":"Bad request — missing or invalid parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["validation_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["INVALID_PARAMETER"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"description":"Field path using dot/bracket notation, e.g. \"resource.cpu\"","type":"string"},"message":{"description":"Validation error message for this field","type":"string"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"invalidParameter":{"summary":"Missing backup name","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Database name and backup name are required.","details":[{"field":"backupName","message":"Required"}]}}}}}}},"401":{"description":"Unauthorized — invalid or missing kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authentication_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Unauthorized, please login again."}}}}}}},"403":{"description":"Forbidden — insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authorization_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["PERMISSION_DENIED","INSUFFICIENT_BALANCE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"permissionDenied":{"summary":"Insufficient permissions","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Insufficient permissions to perform this operation.","details":"backups.dataprotection.kubeblocks.io is forbidden: User \"system:serviceaccount:ns-abc\" cannot delete resource \"backups\" in API group \"dataprotection.kubeblocks.io\""}}},"insufficientBalance":{"summary":"Insufficient account balance","value":{"error":{"type":"authorization_error","code":"INSUFFICIENT_BALANCE","message":"Insufficient balance to perform this operation.","details":"admission webhook \"account.sealos.io\" denied the request: account balance less than 0"}}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["operation_error","internal_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["KUBERNETES_ERROR","INTERNAL_ERROR"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"A Kubernetes API call failed.","details":"backups.dataprotection.kubeblocks.io \"my-postgres-db-backup-20240115\": Internal error occurred"}}}}}}},"503":{"description":"Service unavailable — Kubernetes cluster unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"internal_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"SERVICE_UNAVAILABLE"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"The Kubernetes cluster is temporarily unreachable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}}},"/databases/{databaseName}/backups/{backupName}/restore":{"post":{"summary":"Restore database from backup","description":"Creates a new database cluster restored from the specified backup. Useful for point-in-time recovery or cloning a database for testing.\n\nKey points:\n- All body fields are optional — if `name` is omitted a name is auto-generated, if `replicas` is omitted the source cluster replica count is used.\n- The restored database is a completely new cluster — it does not modify or delete the backup.","operationId":"restoreDatabase","tags":["Mutation"],"parameters":[{"name":"databaseName","in":"path","required":true,"description":"Name of the source database (format: lowercase alphanumeric and hyphens)","schema":{"type":"string","minLength":1,"example":"my-postgres-db"}},{"name":"backupName","in":"path","required":true,"description":"Name of the backup to restore from (format: lowercase alphanumeric and hyphens)","schema":{"type":"string","minLength":1,"example":"my-postgres-db-backup-20240115"}}],"requestBody":{"required":false,"description":"Optional configuration for the restored database instance","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":63,"pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","description":"Name for the new restored database instance. Must be a valid Kubernetes resource name. If omitted, a name is auto-generated using the source database name as a prefix.","example":"my-postgres-db-restored"},"replicas":{"type":"integer","minimum":1,"description":"Number of replicas for the restored cluster. If omitted, inherits the source cluster replica count.","example":1}}}}}},"responses":{"204":{"description":"Database restore initiated successfully."},"400":{"description":"Bad request — missing path parameters or invalid request body","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["validation_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["INVALID_PARAMETER"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"description":"Field path using dot/bracket notation, e.g. \"resource.cpu\"","type":"string"},"message":{"description":"Validation error message for this field","type":"string"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingPathParam":{"summary":"Missing path parameter","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Database name and backup name are required."}}}}}}},"401":{"description":"Unauthorized — invalid or missing kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authentication_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Unauthorized, please login again."}}}}}}},"403":{"description":"Forbidden — insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authorization_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["PERMISSION_DENIED","INSUFFICIENT_BALANCE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"permissionDenied":{"summary":"Insufficient permissions","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Insufficient permissions to perform this operation.","details":"clusters.apps.kubeblocks.io is forbidden: User \"system:serviceaccount:ns-abc\" cannot get resource \"clusters\" in API group \"apps.kubeblocks.io\""}}},"insufficientBalance":{"summary":"Insufficient account balance","value":{"error":{"type":"authorization_error","code":"INSUFFICIENT_BALANCE","message":"Insufficient balance to perform this operation.","details":"admission webhook \"account.sealos.io\" denied the request: account balance less than 0"}}}}}}},"404":{"description":"Not found — backup or source database not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"resource_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"NOT_FOUND"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"notFound":{"summary":"Backup not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Backup not found."}}}}}}},"409":{"description":"Conflict — a database with the target name already exists","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"resource_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["ALREADY_EXISTS","CONFLICT"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"alreadyExists":{"summary":"Database already exists","value":{"error":{"type":"resource_error","code":"ALREADY_EXISTS","message":"Database with this name already exists."}}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["operation_error","internal_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["KUBERNETES_ERROR","OPERATION_FAILED","INTERNAL_ERROR"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"A Kubernetes API call failed.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\": Internal error occurred: etcdserver: request timed out"}}}}}}},"503":{"description":"Service unavailable — Kubernetes cluster unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"internal_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"SERVICE_UNAVAILABLE"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"The Kubernetes cluster is temporarily unreachable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}}},"/databases/{databaseName}/enable-public":{"post":{"summary":"Enable public access","description":"Exposes the database externally via a NodePort or LoadBalancer service. After this succeeds, external connection details are available via `GET /databases/{databaseName}`.","operationId":"enablePublicAccess","tags":["Mutation"],"parameters":[{"name":"databaseName","in":"path","required":true,"description":"Name of the database to operate on (format: lowercase alphanumeric and hyphens)","schema":{"type":"string","minLength":1,"example":"my-postgres-db"}}],"responses":{"204":{"description":"Public access enabled successfully."},"400":{"description":"Bad request — invalid database name","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["validation_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["INVALID_PARAMETER"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"description":"Field path using dot/bracket notation, e.g. \"resource.cpu\"","type":"string"},"message":{"description":"Validation error message for this field","type":"string"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"invalidParameter":{"summary":"Invalid database name","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid request parameters.","details":[{"field":"databaseName","message":"Required"}]}}}}}}},"401":{"description":"Unauthorized — invalid or missing kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authentication_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Unauthorized, please login again."}}}}}}},"403":{"description":"Forbidden — insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authorization_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["PERMISSION_DENIED","INSUFFICIENT_BALANCE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"permissionDenied":{"summary":"Insufficient permissions","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Insufficient permissions to perform this operation.","details":"services is forbidden: User \"system:serviceaccount:ns-abc\" cannot create resource \"services\" in API group \"\""}}},"insufficientBalance":{"summary":"Insufficient account balance","value":{"error":{"type":"authorization_error","code":"INSUFFICIENT_BALANCE","message":"Insufficient balance to perform this operation.","details":"admission webhook \"account.sealos.io\" denied the request: account balance less than 0"}}}}}}},"404":{"description":"Not found — database does not exist","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"resource_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"NOT_FOUND"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"notFound":{"summary":"Database not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Database not found."}}}}}}},"409":{"description":"Conflict — public access is already enabled","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"resource_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["ALREADY_EXISTS","CONFLICT"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"alreadyExists":{"summary":"Already enabled","value":{"error":{"type":"resource_error","code":"ALREADY_EXISTS","message":"Resource already exists.","details":"services \"my-postgres-db-export\" already exists"}}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["operation_error","internal_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["KUBERNETES_ERROR","OPERATION_FAILED","INTERNAL_ERROR"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"A Kubernetes API call failed.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\": Internal error occurred: etcdserver: request timed out"}}}}}}},"503":{"description":"Service unavailable — Kubernetes cluster unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"internal_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"SERVICE_UNAVAILABLE"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"The Kubernetes cluster is temporarily unreachable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}}},"/databases/{databaseName}/disable-public":{"post":{"summary":"Disable public access","description":"Removes the external NodePort or LoadBalancer service, restricting database access to within the cluster.","operationId":"disablePublicAccess","tags":["Mutation"],"parameters":[{"name":"databaseName","in":"path","required":true,"description":"Name of the database to operate on (format: lowercase alphanumeric and hyphens)","schema":{"type":"string","minLength":1,"example":"my-postgres-db"}}],"responses":{"204":{"description":"Public access disabled successfully."},"400":{"description":"Bad request — invalid path parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["validation_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["INVALID_PARAMETER"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"description":"Field path using dot/bracket notation, e.g. \"resource.cpu\"","type":"string"},"message":{"description":"Validation error message for this field","type":"string"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"invalidParameter":{"summary":"Invalid database name","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid request parameters.","details":[{"field":"databaseName","message":"Required"}]}}}}}}},"401":{"description":"Unauthorized — invalid or missing kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authentication_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Unauthorized, please login again."}}}}}}},"403":{"description":"Forbidden — insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authorization_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["PERMISSION_DENIED","INSUFFICIENT_BALANCE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"permissionDenied":{"summary":"Insufficient permissions","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Insufficient permissions to perform this operation.","details":"services is forbidden: User \"system:serviceaccount:ns-abc\" cannot delete resource \"services\" in API group \"\""}}},"insufficientBalance":{"summary":"Insufficient account balance","value":{"error":{"type":"authorization_error","code":"INSUFFICIENT_BALANCE","message":"Insufficient balance to perform this operation.","details":"admission webhook \"account.sealos.io\" denied the request: account balance less than 0"}}}}}}},"404":{"description":"Not found — database or public service not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"resource_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"NOT_FOUND"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"notFound":{"summary":"Database not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Database 'my-postgres-db' not found.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\" not found"}}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["operation_error","internal_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["KUBERNETES_ERROR","OPERATION_FAILED","INTERNAL_ERROR"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"A Kubernetes API call failed.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\": Internal error occurred: etcdserver: request timed out"}}}}}}},"503":{"description":"Service unavailable — Kubernetes cluster unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"internal_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"SERVICE_UNAVAILABLE"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"The Kubernetes cluster is temporarily unreachable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}}},"/databases/versions":{"get":{"summary":"List available database versions","description":"Returns all supported database versions per engine type. Use these version strings when creating a new database.\n\nThis endpoint does not require authentication — it queries the cluster using the server’s own service account.","operationId":"listDatabaseVersions","tags":["Query"],"security":[],"responses":{"200":{"description":"Database versions retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"},"data":{"type":"object","propertyNames":{"type":"string","enum":["postgresql","mongodb","apecloud-mysql","redis","kafka","qdrant","nebula","weaviate","milvus","pulsar","clickhouse"]},"additionalProperties":{"type":"array","items":{"type":"string"}}}},"required":["code","message","data"],"additionalProperties":false}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["operation_error","internal_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["KUBERNETES_ERROR","OPERATION_FAILED","INTERNAL_ERROR"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"A Kubernetes API call failed.","details":"etcdserver: request timed out"}}}}}}},"503":{"description":"Service unavailable — Kubernetes cluster unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"internal_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"SERVICE_UNAVAILABLE"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"The Kubernetes cluster is temporarily unreachable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}}},"/logs":{"get":{"summary":"Get database log entries","description":"Returns paginated log entries from a specific database pod. Use `/logs/files` first to discover available log file paths.","operationId":"getDatabaseLogsData","tags":["Query"],"parameters":[{"name":"podName","in":"query","required":true,"schema":{"type":"string","example":"my-postgres-db-postgresql-0"},"description":"Name of the pod to retrieve logs from"},{"name":"dbType","in":"query","required":true,"schema":{"type":"string","enum":["mysql","mongodb","redis","postgresql"],"example":"postgresql"},"description":"Database engine type (determines log format and parsing)"},{"name":"logType","in":"query","required":true,"schema":{"type":"string","enum":["runtimeLog","slowQuery","errorLog"],"example":"errorLog"},"description":"Type of log to retrieve. Allowed values: \"runtimeLog\", \"slowQuery\", \"errorLog\""},{"name":"logPath","in":"query","required":true,"schema":{"type":"string","example":"/var/lib/postgresql/data/log/postgresql.log"},"description":"Absolute path to the log file within the pod"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"example":1},"description":"Page number for pagination (starts at 1)"},{"name":"pageSize","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":1000,"default":100,"example":100},"description":"Number of log entries per page (max 1000)"}],"responses":{"200":{"description":"Log entries retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"},"data":{"type":"object","properties":{"logs":{"type":"array","items":{"type":"object","properties":{"timestamp":{"type":"string"},"level":{"type":"string"},"content":{"type":"string"}},"required":["timestamp","level","content"],"additionalProperties":false}},"metadata":{"type":"object","properties":{"total":{"type":"number"},"page":{"type":"number"},"pageSize":{"type":"number"},"processingTime":{"type":"string"},"hasMore":{"type":"boolean"}},"required":["total","page","pageSize","processingTime","hasMore"],"additionalProperties":false}},"required":["logs","metadata"],"additionalProperties":false}},"required":["code","message","data"],"additionalProperties":false}}}},"400":{"description":"Bad request — invalid query parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["validation_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["INVALID_PARAMETER"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"description":"Field path using dot/bracket notation, e.g. \"resource.cpu\"","type":"string"},"message":{"description":"Validation error message for this field","type":"string"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"invalidParameter":{"summary":"Missing required parameter","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid request parameters.","details":[{"field":"podName","message":"Required"}]}}}}}}},"401":{"description":"Unauthorized — invalid or missing kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authentication_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Unauthorized, please login again."}}}}}}},"403":{"description":"Forbidden — insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authorization_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["PERMISSION_DENIED","INSUFFICIENT_BALANCE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"permissionDenied":{"summary":"Insufficient permissions","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Insufficient permissions to perform this operation.","details":"pods/exec is forbidden: User \"system:serviceaccount:ns-abc\" cannot create resource \"pods/exec\" in API group \"\""}}},"insufficientBalance":{"summary":"Insufficient account balance","value":{"error":{"type":"authorization_error","code":"INSUFFICIENT_BALANCE","message":"Insufficient balance to perform this operation.","details":"admission webhook \"account.sealos.io\" denied the request: account balance less than 0"}}}}}}},"404":{"description":"Not found — pod or log file not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"resource_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"NOT_FOUND"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"notFound":{"summary":"Pod not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Pod \"my-postgres-db-postgresql-0\" not found.","details":"pods \"my-postgres-db-postgresql-0\" not found"}}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["operation_error","internal_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["KUBERNETES_ERROR","OPERATION_FAILED","INTERNAL_ERROR"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"A Kubernetes API call failed.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\": Internal error occurred: etcdserver: request timed out"}}}}}}},"503":{"description":"Service unavailable — Kubernetes cluster unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"internal_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"SERVICE_UNAVAILABLE"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"The Kubernetes cluster is temporarily unreachable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}}},"/logs/files":{"get":{"summary":"List database log files","description":"Returns metadata about available log files for a specific database pod. Use the returned paths with `GET /logs` to fetch log entries.","operationId":"listDatabaseLogFiles","tags":["Query"],"parameters":[{"name":"podName","in":"query","required":true,"schema":{"type":"string","example":"my-postgres-db-postgresql-0"},"description":"Name of the pod to list log files from"},{"name":"dbType","in":"query","required":true,"schema":{"type":"string","enum":["mysql","mongodb","redis","postgresql"],"example":"postgresql"},"description":"Database engine type (determines where to look for log files)"},{"name":"logType","in":"query","required":true,"schema":{"type":"string","enum":["runtimeLog","slowQuery","errorLog"],"example":"errorLog"},"description":"Type of logs to list. Allowed values: \"runtimeLog\", \"slowQuery\", \"errorLog\""}],"responses":{"200":{"description":"Log files retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"number"},"message":{"type":"string"},"data":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"path":{"type":"string"},"dir":{"type":"string"},"kind":{"type":"string"},"attr":{"type":"string"},"hardLinks":{"type":"number"},"owner":{"type":"string"},"group":{"type":"string"},"size":{"type":"number"},"updateTime":{"type":"string"},"linkTo":{"type":"string"},"processed":{"type":"boolean"}},"required":["name","path","dir","kind","attr","hardLinks","owner","group","size","updateTime"],"additionalProperties":false}}},"required":["code","message","data"],"additionalProperties":false}}}},"400":{"description":"Bad request — invalid query parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["validation_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["INVALID_PARAMETER"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"description":"Field path using dot/bracket notation, e.g. \"resource.cpu\"","type":"string"},"message":{"description":"Validation error message for this field","type":"string"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"invalidParameter":{"summary":"Missing required parameter","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid request parameters.","details":[{"field":"podName","message":"Required"}]}}}}}}},"401":{"description":"Unauthorized — invalid or missing kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authentication_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Unauthorized, please login again."}}}}}}},"403":{"description":"Forbidden — insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"authorization_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["PERMISSION_DENIED","INSUFFICIENT_BALANCE"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"permissionDenied":{"summary":"Insufficient permissions","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Insufficient permissions to perform this operation.","details":"pods/exec is forbidden: User \"system:serviceaccount:ns-abc\" cannot create resource \"pods/exec\" in API group \"\""}}},"insufficientBalance":{"summary":"Insufficient account balance","value":{"error":{"type":"authorization_error","code":"INSUFFICIENT_BALANCE","message":"Insufficient balance to perform this operation.","details":"admission webhook \"account.sealos.io\" denied the request: account balance less than 0"}}}}}}},"404":{"description":"Not found — pod not found or no log files available","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"resource_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"NOT_FOUND"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"notFound":{"summary":"Pod not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Pod \"my-postgres-db-postgresql-0\" not found.","details":"pods \"my-postgres-db-postgresql-0\" not found"}}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","enum":["operation_error","internal_error"]},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","enum":["KUBERNETES_ERROR","OPERATION_FAILED","INTERNAL_ERROR"]},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"A Kubernetes API call failed.","details":"clusters.apps.kubeblocks.io \"my-postgres-db\": Internal error occurred: etcdserver: request timed out"}}}}}}},"503":{"description":"Service unavailable — Kubernetes cluster unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"description":"Structured error object containing type, code, message, and optional details","type":"object","properties":{"type":{"description":"High-level error type for categorization","type":"string","const":"internal_error"},"code":{"description":"Specific error code for programmatic handling and i18n","type":"string","const":"SERVICE_UNAVAILABLE"},"message":{"description":"Human-readable error message","type":"string"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"The Kubernetes cluster is temporarily unreachable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}}}},"components":{"schemas":{"UpdateResourceSchema":{"type":"object","properties":{"cpu":{"type":"number","enum":[1,2,3,4,5,6,7,8],"description":"CPU cores. Allowed values: 1, 2, 3, 4, 5, 6, 7, or 8 (converted to millicores in K8s)","example":2},"memory":{"type":"number","enum":[1,2,4,6,8,12,16,32],"description":"Memory in GB. Allowed values: 1, 2, 4, 6, 8, 12, 16, or 32 GB (converted to Gi in K8s)","example":4},"storage":{"type":"number","minimum":1,"maximum":300,"description":"Storage in GB (1–300). Storage can only be expanded, not shrunk (converted to Gi in K8s)","example":20},"replicas":{"type":"integer","minimum":1,"maximum":20,"description":"Number of replicas (1–20)","example":2}},"description":"Resource configuration for database update. All fields are optional."},"K8sClusterResource":{"type":"object","description":"Full Kubernetes cluster resource object","additionalProperties":true},"OpsRequest":{"type":"object","properties":{"apiVersion":{"type":"string","example":"apps.kubeblocks.io/v1alpha1"},"kind":{"type":"string","example":"OpsRequest"},"metadata":{"type":"object","properties":{"name":{"type":"string"},"namespace":{"type":"string"},"labels":{"type":"object","additionalProperties":{"type":"string"}}}},"spec":{"type":"object","additionalProperties":true},"status":{"type":"object","additionalProperties":true}}}},"securitySchemes":{"kubeconfigAuth":{"type":"apiKey","in":"header","name":"Authorization","description":"URL-encoded kubeconfig YAML. Encode with `encodeURIComponent(kubeconfigYaml)` before setting the header value. Obtain your kubeconfig from the Sealos console."}}}} \ No newline at end of file diff --git a/skills/database/scripts/sealos-auth.mjs b/skills/database/scripts/sealos-auth.mjs new file mode 100644 index 0000000..aa25cf5 --- /dev/null +++ b/skills/database/scripts/sealos-auth.mjs @@ -0,0 +1,329 @@ +#!/usr/bin/env node + +/** + * Sealos Cloud Authentication — OAuth2 Device Grant Flow (RFC 8628) + * + * Usage: + * node sealos-auth.mjs check # Check if already authenticated + * node sealos-auth.mjs login [region] # Start device grant login flow + * node sealos-auth.mjs info # Show current auth info + * + * Environment variables: + * SEALOS_REGION — Sealos Cloud region URL (default from config.json) + * + * Flow: + * 1. POST /api/auth/oauth2/device → { device_code, user_code, verification_uri_complete } + * 2. User opens verification_uri_complete in browser to authorize + * 3. Script polls /api/auth/oauth2/token until approved + * 4. Receives access_token → exchanges for kubeconfig → saves to ~/.sealos/kubeconfig + */ + +import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs' +import { execSync } from 'child_process' +import { homedir, platform } from 'os' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' + +// ── Paths ──────────────────────────────────────────────── +const __dirname = dirname(fileURLToPath(import.meta.url)) +const SEALOS_DIR = join(homedir(), '.sealos') +const KC_PATH = join(SEALOS_DIR, 'kubeconfig') +const AUTH_PATH = join(SEALOS_DIR, 'auth.json') + +// ── Skill constants (from config.json) ─────────────────── +const CONFIG_PATH = join(__dirname, '..', 'config.json') +const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')) +const CLIENT_ID = config.client_id +const DEFAULT_REGION = config.default_region + +// ── Check ────────────────────────────────────────────── + +function check () { + if (!existsSync(KC_PATH)) { + return { authenticated: false } + } + + try { + const kc = readFileSync(KC_PATH, 'utf-8') + if (kc.includes('server:') && (kc.includes('token:') || kc.includes('client-certificate'))) { + const auth = existsSync(AUTH_PATH) ? JSON.parse(readFileSync(AUTH_PATH, 'utf-8')) : {} + return { + authenticated: true, + kubeconfig_path: KC_PATH, + region: auth.region || 'unknown' + } + } + } catch { } + + return { authenticated: false } +} + +// ── Device Grant Flow ────────────────────────────────── + +/** + * Step 1: Request device authorization + * POST /api/auth/oauth2/device + * Body: { client_id } + * Response: { device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval } + */ +async function requestDeviceAuthorization (region) { + const res = await fetch(`${region}/api/auth/oauth2/device`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + client_id: CLIENT_ID, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code' + }) + }) + + if (!res.ok) { + const body = await res.text().catch(() => '') + throw new Error(`Device authorization request failed (${res.status}): ${body || res.statusText}`) + } + + return res.json() +} + +/** + * Step 2: Poll for token + * POST /api/auth/oauth2/token + * Body: { client_id, grant_type, device_code } + * + * Possible responses: + * - 200: { access_token, token_type, ... } → success + * - 400: { error: "authorization_pending" } → keep polling + * - 400: { error: "slow_down" } → increase interval by 5s + * - 400: { error: "access_denied" } → user denied + * - 400: { error: "expired_token" } → device code expired + */ +async function pollForToken (region, deviceCode, interval, expiresIn) { + // Hard cap at 10 minutes regardless of server's expires_in + const maxWait = Math.min(expiresIn, 600) * 1000 + const deadline = Date.now() + maxWait + let pollInterval = interval * 1000 + let lastLoggedMinute = -1 + + while (Date.now() < deadline) { + await sleep(pollInterval) + + // Log remaining time every minute + const remaining = Math.ceil((deadline - Date.now()) / 60000) + if (remaining !== lastLoggedMinute && remaining > 0) { + lastLoggedMinute = remaining + process.stderr.write(` Waiting for authorization... (${remaining} min remaining)\n`) + } + + const res = await fetch(`${region}/api/auth/oauth2/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + client_id: CLIENT_ID, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + device_code: deviceCode + }) + }) + + if (res.ok) { + // Success — got the token + return res.json() + } + + const body = await res.json().catch(() => ({})) + + switch (body.error) { + case 'authorization_pending': + // User hasn't authorized yet, keep polling + break + + case 'slow_down': + // Increase polling interval by 5 seconds (RFC 8628 §3.5) + pollInterval += 5000 + break + + case 'access_denied': + throw new Error('Authorization denied by user') + + case 'expired_token': + throw new Error('Device code expired. Please run login again.') + + default: + throw new Error(`Token request failed: ${body.error || res.statusText}`) + } + } + + throw new Error('Authorization timed out (10 minutes). Please run login again.') +} + +/** + * Step 3: Exchange access token for kubeconfig + */ +async function exchangeForKubeconfig (region, accessToken) { + const res = await fetch(`${region}/api/auth/getDefaultKubeconfig`, { + method: 'POST', + headers: { + Authorization: accessToken, + 'Content-Type': 'application/json' + } + }) + + if (!res.ok) { + const body = await res.text().catch(() => '') + throw new Error(`Kubeconfig exchange failed (${res.status}): ${body || res.statusText}`) + } + + return res.json() +} + +function sleep (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +// ── Login (Device Grant Flow) ────────────────────────── + +async function login (region = DEFAULT_REGION) { + region = region.replace(/\/+$/, '') + + // Step 1: Request device authorization + const deviceAuth = await requestDeviceAuthorization(region) + + const { + device_code: deviceCode, + user_code: userCode, + verification_uri: verificationUri, + verification_uri_complete: verificationUriComplete, + expires_in: expiresIn, + interval = 5 + } = deviceAuth + + // Output device authorization info for the AI tool / user to display + const authPrompt = { + action: 'user_authorization_required', + user_code: userCode, + verification_uri: verificationUri, + verification_uri_complete: verificationUriComplete, + expires_in: expiresIn, + message: `Please open the following URL in your browser to authorize:\n\n ${verificationUriComplete || verificationUri}\n\nAuthorization code: ${userCode}\nExpires in: ${Math.floor(expiresIn / 60)} minutes` + } + + // Print the authorization prompt to stderr so it's visible to the user + // while stdout is reserved for JSON output + process.stderr.write('\n' + authPrompt.message + '\n\nWaiting for authorization...\n') + + // Auto-open browser + const url = verificationUriComplete || verificationUri + try { + const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open' + execSync(`${cmd} "${url}"`, { stdio: 'ignore' }) + process.stderr.write('Browser opened automatically.\n') + } catch { + process.stderr.write('Could not open browser automatically. Please open the URL manually.\n') + } + + // Step 2: Poll for token + const tokenResponse = await pollForToken(region, deviceCode, interval, expiresIn) + const accessToken = tokenResponse.access_token + + if (!accessToken) { + throw new Error('Token response missing access_token') + } + + process.stderr.write('Authorization received. Exchanging for kubeconfig...\n') + + // Step 3: Exchange access token for kubeconfig + const kcData = await exchangeForKubeconfig(region, accessToken) + const kubeconfig = kcData.data?.kubeconfig + + if (!kubeconfig) { + throw new Error('API response missing data.kubeconfig field') + } + + // Save kubeconfig to ~/.sealos/kubeconfig (Sealos-specific, avoids conflict with ~/.kube/config) + mkdirSync(SEALOS_DIR, { recursive: true }) + writeFileSync(KC_PATH, kubeconfig, { mode: 0o600 }) + writeFileSync(AUTH_PATH, JSON.stringify({ + region, + authenticated_at: new Date().toISOString(), + auth_method: 'oauth2_device_grant' + }, null, 2), { mode: 0o600 }) + + process.stderr.write('Authentication successful!\n') + + return { kubeconfig_path: KC_PATH, region } +} + +// ── Info ─────────────────────────────────────────────── + +function info () { + const status = check() + if (!status.authenticated) { + return { authenticated: false, message: 'Not authenticated. Run: node sealos-auth.mjs login' } + } + + const auth = existsSync(AUTH_PATH) ? JSON.parse(readFileSync(AUTH_PATH, 'utf-8')) : {} + return { + authenticated: true, + kubeconfig_path: KC_PATH, + region: auth.region || 'unknown', + auth_method: auth.auth_method || 'unknown', + authenticated_at: auth.authenticated_at || 'unknown' + } +} + +// ── CLI ──────────────────────────────────────────────── + +const [, , cmd, ...rawArgs] = process.argv + +// --insecure flag: skip TLS certificate verification (for self-signed certs) +const insecure = rawArgs.includes('--insecure') +const args = rawArgs.filter(a => a !== '--insecure') + +if (insecure) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' +} + +try { + switch (cmd) { + case 'check': { + console.log(JSON.stringify(check())) + break + } + + case 'login': { + const region = args[0] || process.env.SEALOS_REGION || DEFAULT_REGION + const result = await login(region) + console.log(JSON.stringify(result)) + break + } + + case 'info': { + console.log(JSON.stringify(info(), null, 2)) + break + } + + default: { + console.log(`Sealos Cloud Auth — OAuth2 Device Grant Flow + +Usage: + node sealos-auth.mjs check Check authentication status + node sealos-auth.mjs login [region] Start OAuth2 device login flow + node sealos-auth.mjs login --insecure Skip TLS verification (self-signed cert) + node sealos-auth.mjs info Show current auth details + +Environment: + SEALOS_REGION Region URL (default: ${DEFAULT_REGION}) + +Flow: + 1. Run "login" → opens browser for authorization + 2. Approve in browser → script receives token automatically + 3. Token exchanged for kubeconfig → saved to ~/.sealos/kubeconfig`) + } + } +} catch (err) { + // If TLS error and not using --insecure, hint the user + if (!insecure && (err.message.includes('fetch failed') || err.message.includes('self-signed') || err.message.includes('CERT'))) { + console.error(JSON.stringify({ error: err.message, hint: 'Try adding --insecure for self-signed certificates' })) + } else { + console.error(JSON.stringify({ error: err.message })) + } + process.exit(1) +} diff --git a/skills/database/scripts/sealos-db.mjs b/skills/database/scripts/sealos-db.mjs new file mode 100644 index 0000000..cb8350f --- /dev/null +++ b/skills/database/scripts/sealos-db.mjs @@ -0,0 +1,460 @@ +#!/usr/bin/env node +// Sealos Database CLI - single entry point for all database operations. +// Zero external dependencies. Requires Node.js (guaranteed by Claude Code). +// +// Usage: +// node sealos-db.mjs [args...] +// +// Config resolution: +// ~/.sealos/auth.json region field → derives API URL automatically +// Kubeconfig is always read from ~/.sealos/kubeconfig +// +// Commands: +// list-versions List available database versions (no auth needed) +// list List all databases +// get Get database details and connection info +// create Create a new database +// create-wait Create + poll until running (timeout 2min) +// update Update database resources +// delete Delete a database +// start Start a stopped database +// pause Pause a running database +// restart Restart a database +// enable-public Enable public access +// disable-public Disable public access +// list-backups List backups for a database +// create-backup [json] Create a backup (optional: {"description":"...","name":"..."}) +// delete-backup Delete a specific backup +// restore-backup [json] Restore from backup (optional: {"name":"...","replicas":N}) +// log-files List log files for a database pod +// logs [page] [pageSize] Get log entries + +import { readFileSync, existsSync } from 'node:fs'; +import { request as httpsRequest } from 'node:https'; +import { request as httpRequest } from 'node:http'; +import { resolve } from 'node:path'; +import { homedir } from 'node:os'; + +const KC_PATH = resolve(homedir(), '.sealos/kubeconfig'); +const AUTH_PATH = resolve(homedir(), '.sealos/auth.json'); +const API_PATH = '/api/v2alpha'; // API version — update here if the version changes + +// --- config --- + +function loadConfig() { + // Derive API URL from auth.json region + if (!existsSync(AUTH_PATH)) { + throw new Error('Not authenticated. Run: node sealos-auth.mjs login'); + } + + let auth; + try { + auth = JSON.parse(readFileSync(AUTH_PATH, 'utf-8')); + } catch { + throw new Error('Invalid auth.json. Run: node sealos-auth.mjs login'); + } + + if (!auth.region) { + throw new Error('No region in auth.json. Run: node sealos-auth.mjs login'); + } + + // Derive API URL: region "https://gzg.sealos.run" → "https://dbprovider.gzg.sealos.run/api/v2alpha" + const regionUrl = new URL(auth.region); + const apiUrl = `https://dbprovider.${regionUrl.hostname}${API_PATH}`; + + if (!existsSync(KC_PATH)) { + throw new Error(`Kubeconfig not found at ${KC_PATH}. Run: node sealos-auth.mjs login`); + } + + return { apiUrl, kubeconfigPath: KC_PATH }; +} + +// --- auth --- + +function getEncodedKubeconfig(path) { + if (!existsSync(path)) { + throw new Error(`Kubeconfig not found at ${path}`); + } + return encodeURIComponent(readFileSync(path, 'utf-8')); +} + +// --- HTTP --- + +function apiCall(method, endpoint, { apiUrl, auth, body, timeout = 30000 } = {}) { + return new Promise((resolve, reject) => { + const url = new URL(apiUrl + endpoint); + const isHttps = url.protocol === 'https:'; + const reqFn = isHttps ? httpsRequest : httpRequest; + + const headers = {}; + if (auth) headers['Authorization'] = auth; + if (body) headers['Content-Type'] = 'application/json'; + + const bodyStr = body ? JSON.stringify(body) : null; + if (bodyStr) headers['Content-Length'] = Buffer.byteLength(bodyStr); + + const opts = { + hostname: url.hostname, + port: url.port || (isHttps ? 443 : 80), + path: url.pathname + url.search, + method, + headers, + timeout, + rejectUnauthorized: false, // Sealos clusters may use self-signed certificates + }; + + const req = reqFn(opts, (res) => { + const chunks = []; + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => { + const rawBody = Buffer.concat(chunks).toString(); + let parsed = null; + try { parsed = JSON.parse(rawBody); } catch { parsed = rawBody || null; } + resolve({ status: res.statusCode, body: parsed }); + }); + }); + + req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); }); + req.on('error', reject); + if (bodyStr) req.write(bodyStr); + req.end(); + }); +} + +// --- helpers --- + +function requireName(args) { + if (!args[0]) throw new Error('Database name required'); + return args[0]; +} + +function output(data) { + console.log(JSON.stringify(data, null, 2)); +} + +function sleep(ms) { + return new Promise((r) => setTimeout(r, ms)); +} + +// --- individual commands --- + +async function listVersions(cfg) { + const res = await apiCall('GET', '/databases/versions', { apiUrl: cfg.apiUrl }); + if (res.status !== 200) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +async function list(cfg) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('GET', '/databases', { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 200) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +async function get(cfg, name) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('GET', `/databases/${name}`, { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 200) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +function validateCreateBody(body) { + const errors = []; + if (!body.name) { + errors.push('name is required'); + } else { + if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(body.name)) errors.push('name must match ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'); + if (body.name.length > 63) errors.push('name must be at most 63 characters'); + } + if (!body.type) errors.push('type is required'); + if (body.quota) { + const q = body.quota; + if (q.cpu !== undefined && (!Number.isInteger(q.cpu) || q.cpu < 1 || q.cpu > 8)) errors.push('cpu must be an integer 1-8'); + if (q.memory !== undefined && (typeof q.memory !== 'number' || q.memory < 0.1 || q.memory > 32)) errors.push('memory must be 0.1-32 GB'); + if (q.storage !== undefined && (typeof q.storage !== 'number' || q.storage < 1 || q.storage > 300)) errors.push('storage must be 1-300 GB'); + if (q.replicas !== undefined && (!Number.isInteger(q.replicas) || q.replicas < 1 || q.replicas > 20)) errors.push('replicas must be an integer 1-20'); + } + if (body.terminationPolicy && !['delete', 'wipeout'].includes(body.terminationPolicy)) errors.push('terminationPolicy must be "delete" or "wipeout"'); + if (errors.length) throw new Error('Validation failed: ' + errors.join('; ')); +} + +function validateUpdateBody(body) { + const errors = []; + if (body.quota) { + const q = body.quota; + if (q.cpu !== undefined && ![1, 2, 3, 4, 5, 6, 7, 8].includes(q.cpu)) errors.push('cpu must be one of: 1, 2, 3, 4, 5, 6, 7, 8'); + if (q.memory !== undefined && ![1, 2, 4, 6, 8, 12, 16, 32].includes(q.memory)) errors.push('memory must be one of: 1, 2, 4, 6, 8, 12, 16, 32 GB'); + if (q.storage !== undefined && (typeof q.storage !== 'number' || q.storage < 1 || q.storage > 300)) errors.push('storage must be 1-300 GB (expand only)'); + if (q.replicas !== undefined && (!Number.isInteger(q.replicas) || q.replicas < 1 || q.replicas > 20)) errors.push('replicas must be an integer 1-20'); + } + if (errors.length) throw new Error('Validation failed: ' + errors.join('; ')); +} + +async function create(cfg, jsonBody) { + const body = typeof jsonBody === 'string' ? JSON.parse(jsonBody) : jsonBody; + validateCreateBody(body); + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('POST', '/databases', { apiUrl: cfg.apiUrl, auth, body }); + if (res.status !== 201) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +async function update(cfg, name, jsonBody) { + const body = typeof jsonBody === 'string' ? JSON.parse(jsonBody) : jsonBody; + validateUpdateBody(body); + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('PATCH', `/databases/${name}`, { apiUrl: cfg.apiUrl, auth, body }); + if (res.status !== 204) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + // Re-fetch to return updated state + try { + const updated = await get(cfg, name); + return { success: true, message: 'Database update initiated', database: updated }; + } catch { + return { success: true, message: 'Database update initiated' }; + } +} + +async function del(cfg, name) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('DELETE', `/databases/${name}`, { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 204) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return { success: true, message: `Database '${name}' deleted` }; +} + +async function action(cfg, name, actionName) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('POST', `/databases/${name}/${actionName}`, { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 204) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return { success: true, message: `Action '${actionName}' on '${name}' completed` }; +} + +// --- backup commands --- + +async function listBackups(cfg, name) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('GET', `/databases/${name}/backups`, { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 200) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +async function createBackup(cfg, name, jsonBody) { + const body = jsonBody ? (typeof jsonBody === 'string' ? JSON.parse(jsonBody) : jsonBody) : {}; + if (body.description && body.description.length > 31) { + throw new Error('Validation failed: description must be at most 31 characters (Kubernetes label limit)'); + } + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('POST', `/databases/${name}/backups`, { apiUrl: cfg.apiUrl, auth, body }); + if (res.status !== 204) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return { success: true, message: `Backup created for '${name}'` }; +} + +async function deleteBackup(cfg, name, backupName) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('DELETE', `/databases/${name}/backups/${backupName}`, { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 204) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return { success: true, message: `Backup '${backupName}' deleted from '${name}'` }; +} + +async function restoreBackup(cfg, name, backupName, jsonBody) { + const body = jsonBody ? (typeof jsonBody === 'string' ? JSON.parse(jsonBody) : jsonBody) : {}; + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('POST', `/databases/${name}/backups/${backupName}/restore`, { apiUrl: cfg.apiUrl, auth, body }); + if (res.status !== 204) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return { success: true, message: `Restore from backup '${backupName}' initiated` }; +} + +// --- log commands --- + +async function listLogFiles(cfg, podName, dbType, logType) { + const validDbTypes = ['mysql', 'mongodb', 'redis', 'postgresql']; + const validLogTypes = ['runtimeLog', 'slowQuery', 'errorLog']; + if (!validDbTypes.includes(dbType)) throw new Error(`dbType must be one of: ${validDbTypes.join(', ')}`); + if (!validLogTypes.includes(logType)) throw new Error(`logType must be one of: ${validLogTypes.join(', ')}`); + + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const query = `?podName=${encodeURIComponent(podName)}&dbType=${encodeURIComponent(dbType)}&logType=${encodeURIComponent(logType)}`; + const res = await apiCall('GET', `/logs/files${query}`, { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 200) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +async function getLogs(cfg, podName, dbType, logType, logPath, page, pageSize) { + const validDbTypes = ['mysql', 'mongodb', 'redis', 'postgresql']; + const validLogTypes = ['runtimeLog', 'slowQuery', 'errorLog']; + if (!validDbTypes.includes(dbType)) throw new Error(`dbType must be one of: ${validDbTypes.join(', ')}`); + if (!validLogTypes.includes(logType)) throw new Error(`logType must be one of: ${validLogTypes.join(', ')}`); + + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + let query = `?podName=${encodeURIComponent(podName)}&dbType=${encodeURIComponent(dbType)}&logType=${encodeURIComponent(logType)}&logPath=${encodeURIComponent(logPath)}`; + if (page) query += `&page=${encodeURIComponent(page)}`; + if (pageSize) query += `&pageSize=${encodeURIComponent(pageSize)}`; + const res = await apiCall('GET', `/logs${query}`, { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 200) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +// --- batch commands --- + +async function createWait(cfg, jsonBody) { + // 1. Create + const body = typeof jsonBody === 'string' ? JSON.parse(jsonBody) : jsonBody; + const createResult = await create(cfg, body); + const name = body.name || createResult.name; + + // 2. Poll every 5s until running (max 2 min) + const timeout = 120000; + const interval = 5000; + const start = Date.now(); + + let lastStatus = 'creating'; + let consecutiveErrors = 0; + while (Date.now() - start < timeout) { + await sleep(interval); + try { + const info = await get(cfg, name); + consecutiveErrors = 0; + lastStatus = info.status; + process.stderr.write(`Status: ${lastStatus}\n`); + if (lastStatus.toLowerCase() === 'running') { + return info; + } + if (lastStatus.toLowerCase() === 'failed') { + throw new Error(`Database creation failed. Status: ${lastStatus}`); + } + } catch (e) { + consecutiveErrors++; + if (consecutiveErrors >= 5 || Date.now() - start > timeout) throw e; + } + } + + // Timeout - return last known state + try { + const info = await get(cfg, name); + return { ...info, warning: `Timed out after 2 minutes. Last status: ${info.status}` }; + } catch { + return { name, status: lastStatus, warning: 'Timed out after 2 minutes' }; + } +} + +// --- main --- + +async function main() { + const [cmd, ...args] = process.argv.slice(2); + + if (!cmd) { + console.error('ERROR: Command required.'); + console.error('Commands: list-versions|list|get|create|create-wait|update|delete|start|pause|restart|enable-public|disable-public|list-backups|create-backup|delete-backup|restore-backup|log-files|logs'); + process.exit(1); + } + + try { + const cfg = loadConfig(); + let result; + + switch (cmd) { + case 'list-versions': { + result = await listVersions(cfg); + break; + } + + case 'list': { + result = await list(cfg); + break; + } + + case 'get': { + const name = requireName(args); + result = await get(cfg, name); + break; + } + + case 'create': { + if (!args[0]) throw new Error('JSON body required'); + result = await create(cfg, args[0]); + break; + } + + case 'create-wait': { + if (!args[0]) throw new Error('JSON body required'); + result = await createWait(cfg, args[0]); + break; + } + + case 'update': { + const name = requireName(args); + if (!args[1]) throw new Error('JSON body required'); + result = await update(cfg, name, args[1]); + break; + } + + case 'delete': { + const name = requireName(args); + result = await del(cfg, name); + break; + } + + case 'start': + case 'pause': + case 'restart': + case 'enable-public': + case 'disable-public': { + const name = requireName(args); + result = await action(cfg, name, cmd); + break; + } + + case 'list-backups': { + const name = requireName(args); + result = await listBackups(cfg, name); + break; + } + + case 'create-backup': { + const name = requireName(args); + result = await createBackup(cfg, name, args[1]); + break; + } + + case 'delete-backup': { + const name = requireName(args); + if (!args[1]) throw new Error('Backup name required'); + result = await deleteBackup(cfg, name, args[1]); + break; + } + + case 'restore-backup': { + const name = requireName(args); + if (!args[1]) throw new Error('Backup name required'); + result = await restoreBackup(cfg, name, args[1], args[2]); + break; + } + + case 'log-files': { + const podName = requireName(args); + if (!args[1]) throw new Error('dbType required (mysql|mongodb|redis|postgresql)'); + if (!args[2]) throw new Error('logType required (runtimeLog|slowQuery|errorLog)'); + result = await listLogFiles(cfg, podName, args[1], args[2]); + break; + } + + case 'logs': { + const podName = requireName(args); + if (!args[1]) throw new Error('dbType required (mysql|mongodb|redis|postgresql)'); + if (!args[2]) throw new Error('logType required (runtimeLog|slowQuery|errorLog)'); + if (!args[3]) throw new Error('logPath required'); + result = await getLogs(cfg, podName, args[1], args[2], args[3], args[4], args[5]); + break; + } + + default: + throw new Error(`Unknown command '${cmd}'. Commands: list-versions|list|get|create|create-wait|update|delete|start|pause|restart|enable-public|disable-public|list-backups|create-backup|delete-backup|restore-backup|log-files|logs`); + } + + if (result !== undefined) output(result); + } catch (err) { + console.error(`ERROR: ${err.message}`); + process.exit(1); + } +} + +main(); diff --git a/skills/devbox/README.md b/skills/devbox/README.md new file mode 100644 index 0000000..51e724d --- /dev/null +++ b/skills/devbox/README.md @@ -0,0 +1,53 @@ +# Sealos Devbox — Claude Code Skill + +Manage cloud development environments on [Sealos](https://sealos.io) using natural language. Create, scale, and manage devboxes with SSH access, port forwarding, releases, and deployments — without leaving your terminal. + +## Quick Start + +### Prerequisites + +- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) installed +- A [Sealos](https://sealos.io) account (free tier available) + +### Install + +Copy the `sealos-devbox` skill into your project's `.claude/skills/` directory. Claude Code auto-detects it. + +### Usage + +Ask Claude Code in natural language, or run `/sealos-devbox`: + +``` +> Create a Node.js devbox for my project +> Set up a cloud development environment +> Show my devboxes +> Scale my-app-node to 4 cores +> Delete the test devbox +> Pause the staging devbox +> Get SSH connection info for my-api +> Create a release for my-app +> Deploy v1.0.0 to production +> Show CPU/memory metrics for my-devbox +``` + +## Authentication + +On first use, Claude opens your browser for **OAuth2 login** — no manual kubeconfig download needed. The flow: + +1. Claude runs the login script, which opens your browser to the Sealos authorization page +2. You approve the request in your browser +3. Credentials are saved automatically to `~/.sealos/kubeconfig` +4. The API URL is auto-derived from `~/.sealos/auth.json` + +Subsequent sessions reuse saved credentials until they expire. + +## Features + +- **Browser-based auth** — OAuth2 device grant, no manual kubeconfig setup +- **Smart defaults** — detects your tech stack and recommends a runtime, resources, and ports +- **Full lifecycle** — create, list, inspect, update, delete, start, pause, shutdown, restart +- **SSH integration** — auto-saves SSH keys, offers to write SSH config, supports VS Code Remote +- **Port management** — add, remove, and toggle public access for ports +- **Release & deploy** — create versioned releases and deploy to AppLaunchpad +- **Resource monitoring** — view CPU and memory usage metrics over time +- **Safe deletes** — requires explicit name confirmation before destroying anything diff --git a/skills/devbox/SKILL.md b/skills/devbox/SKILL.md new file mode 100644 index 0000000..4ee529c --- /dev/null +++ b/skills/devbox/SKILL.md @@ -0,0 +1,554 @@ +--- +name: sealos-devbox +description: >- + Use when someone needs to manage development environments on Sealos: create, list, + update, scale, delete, start, pause, shutdown, restart devboxes, get SSH connection info, + manage ports, create releases, deploy to production, or monitor resource usage. + Triggers on "I need a devbox", "create a Node.js devbox on sealos", "scale my devbox", + "delete the devbox", "show my devboxes", "deploy my devbox", "get SSH info", + "pause the devbox", "create a release", "my app needs a dev environment", + "set up a cloud development environment", "I want to code on a remote server", + "remote dev machine", "how do I connect to my devbox via SSH", + "cloud IDE", "cloud workspace", or "spin up a dev environment on Sealos". +--- + +## Interaction Principle + +**NEVER output a question as plain text. ALWAYS use `AskUserQuestion` with `options`.** + +Claude Code's text output is non-interactive — if you write a question as plain text, the +user has no clickable options and must guess how to respond. `AskUserQuestion` gives them +clear choices they can click. + +- Every question → `AskUserQuestion` with `options`. Keep any preceding text to one short status line. +- `AskUserQuestion` adds an implicit "Other / Type something" option, so users can always type custom input. +- **Free-text matching:** When the user types instead of clicking, match to the closest option by intent. + "node", "nodejs" → Node.js; "py", "python3" → Python; "next" → Next.js. + Never re-ask because wording didn't match exactly. + +## Fixed Execution Order + +**ALWAYS follow these steps in this exact order. No skipping, no reordering.** + +``` +Step 0: Check Auth (try existing config from previous session) +Step 1: Authenticate (only if Step 0 fails) +Step 2: Route (determine which operation the user wants) +Step 3: Execute operation (follow the operation-specific steps below) +``` + +--- + +## Step 0: Check Auth + +The script auto-derives its API URL from `~/.sealos/auth.json` (saved by login) +and reads credentials from `~/.sealos/kubeconfig`. No separate config file needed. + +1. Run `node scripts/sealos-devbox.mjs list` +2. If works → skip Step 1. Greet with context: + > Connected to Sealos. You have N devboxes. +3. If fails (not authenticated, 401, connection error) → proceed to Step 1 + +--- + +## Step 1: Authenticate + +Run this step only if Step 0 failed. + +### 1a. OAuth2 Login + +Read `config.json` (in this skill's directory) for available regions and the default. +Ask the user which region to connect to using `AskUserQuestion` with the regions as options. + +Run `node scripts/sealos-auth.mjs login {region_url}` (omit region_url for default). + +This command: +1. Opens the user's browser to the Sealos authorization page +2. Displays a user code and verification URL in stderr +3. Polls until the user approves (max 10 minutes) +4. Exchanges the token for a kubeconfig +5. Saves to `~/.sealos/kubeconfig` and `~/.sealos/auth.json` (with region) + +Display while waiting: +> Opening browser for Sealos login... Approve the request in your browser. + +**If TLS error**: Retry with `node scripts/sealos-auth.mjs login --insecure` + +**If other error**: +`AskUserQuestion`: +- header: "Login Failed" +- question: "Browser login failed. Try again?" +- options: ["Try again", "Cancel"] + +### 1b. Verify connection + +After login, run `node scripts/sealos-devbox.mjs list` to verify auth works. + +**If auth error (401):** Token may have expired. Re-run `node scripts/sealos-auth.mjs login`. + +**If success:** Display: +> Connected to Sealos. You have N devboxes. + +Use the devboxes list in Step 3 instead of making a separate `list` call. + +--- + +## Step 2: Route + +Determine the operation from user intent: + +| Intent | Operation | +|--------|-----------| +| "create/set up a devbox/dev environment" | Create | +| "list/show my devboxes" | List | +| "check status/details/SSH info" | Get | +| "scale/resize/update resources/ports" | Update | +| "delete/remove devbox" | Delete | +| "start/pause/shutdown/restart" | Action | +| "SSH/connect/remote access" | SSH Connect | +| "create release/tag/version" | Release | +| "deploy/ship to production" | Deploy | +| "monitor/metrics/CPU/memory usage" | Monitor | +| "autostart/startup command" | Autostart | + +If ambiguous, ask one clarifying question. + +--- + +## Step 3: Operations + +### Create + +**3a. Templates & existing devboxes** + +Use devboxes from the `list` response (Step 0 or 1b). Run `node scripts/sealos-devbox.mjs templates` +to get available runtimes. If `templates` fails, tell the user the API is unavailable, offer to +retry, or let them type a runtime name manually (refer to `references/api-reference.md` Available +Runtimes for the known list). + +**3b. Ask name first** + +`AskUserQuestion`: +- header: "Name" +- question: "Devbox name?" +- options: generate 2-3 name suggestions from project dir + detected runtime + (see `references/defaults.md` for name suffix rules). + If a name already exists (from list), avoid it and note the conflict. +- Constraint: `^[a-z0-9]([-a-z0-9]*[a-z0-9])?$`, max 63 chars + +**3c. Show recommended config and confirm** + +Read `references/defaults.md` for runtime recommendation rules and resource presets. + +Auto-resolve config from context: +- User's request (e.g., "create a python devbox" → runtime is python) +- Project tech stack (e.g., Next.js → recommend next.js runtime) +- Scale hints (e.g., "production devbox" → higher resources) +- Default port from templates API (e.g., next.js → 3000) + +Display the **recommended config summary** (all fields from the create API): + +``` +Devbox config: + + Name: my-app-next + Runtime: next.js (detected from next.config.mjs) + CPU: 1 Core + Memory: 2 GB + Ports: 3000 (http, public) +``` + +Then `AskUserQuestion`: +- header: "Config" +- question: "Create with this config?" +- options: + 1. "Create now (Recommended)" — accept all, proceed to 3d + 2. "Customize" — go to 3c-customize flow + +**3c-customize: Pick fields to change, then configure only those** + +`AskUserQuestion`: +- header: "Customize" +- question: "Which fields do you want to change?" +- multiSelect: true +- options: **(max 4 items)** — group fields into 4: + - "Runtime — {current_runtime}" + - "Resources (CPU, Memory) — {cpu}C / {mem}GB" + - "Ports — {current_ports}" + - "Env & Autostart" + +When "Runtime" selected → ask Runtime (step 1). +When "Resources" selected → ask CPU (step 2), Memory (step 3) sequentially. +When "Ports" selected → ask port config (step 4). +When "Env & Autostart" selected → ask env vars and autostart command (step 5). +Fields not selected keep their current values. + +**1) Runtime** — First output ALL available runtimes from `templates` as a numbered text list. +Then `AskUserQuestion`: +- header: "Runtime" +- question: "Which runtime?" +- options: **(max 4 items)** — top 4 runtimes for the project context + (see `references/defaults.md`), mark current with "(current)". + User can type any other runtime name/number via "Type something". +- After runtime change: auto-update default port from templates API + +**2) CPU** → `AskUserQuestion`: +- header: "CPU" +- question: "CPU cores? (0.1-32)" +- options: **(max 4 items)** — `0.5, 1 (current), 2, 4` cores. + Mark current with "(current)". + +**3) Memory** → `AskUserQuestion`: +- header: "Memory" +- question: "Memory? (0.1-32 GB)" +- options: **(max 4 items)** — `1, 2 (current), 4, 8` GB. + Mark current with "(current)". + +**4) Ports** → `AskUserQuestion`: +- header: "Ports" +- question: "Port configuration?" +- options: + - "Keep default ({port} http public)" — use template default + - "Add custom port" + - "No ports" + +If "Add custom port": ask for port number, protocol (http/grpc/ws), and isPublic. + +**5) Env & Autostart** → Ask for environment variables (name=value pairs) and +autostart command (optional). If user provides env vars, parse them into the array format. +See also: Autostart operation in `references/operations.md` for standalone autostart configuration. + +After all fields, re-display the updated config summary and `AskUserQuestion`: +- header: "Config" +- question: "Create with this config?" +- options: + 1. "Create now (Recommended)" + 2. "Customize" — re-run the customize flow + +**3d. Create and wait** + +Build JSON body: +```json +{"name":"my-devbox","runtime":"next.js","quota":{"cpu":1,"memory":2},"ports":[{"number":3000,"protocol":"http","isPublic":true}]} +``` + +Run `node scripts/sealos-devbox.mjs create-wait ''`. This single command creates the +devbox and polls until `running` (timeout 2 minutes). The response includes SSH info. + +**3e. Show SSH info and offer integration** + +Display SSH connection details (host, port, username, key path). + +Then `AskUserQuestion`: +- header: "Integration" +- question: "Set up SSH access for this devbox?" +- options: + 1. "Write SSH config (Recommended)" — append Host block to `~/.ssh/config` + 2. "Show SSH command only" — display `ssh -i key -p port user@host` + 3. "Open in VS Code" — show VS Code Remote SSH instructions + 4. "Skip" — just show the info, don't write anything +- When writing SSH config, append, don't overwrite. + +--- + +### List + +Run `node scripts/sealos-devbox.mjs list`. Format as table: + +``` +Name Runtime Status CPU Memory +my-app-next next.js Running 1 2GB +api-server go Stopped 2 4GB +``` + +Highlight abnormal statuses (Error, Stopped). + +--- + +### Get + +If no name given, run List first, then `AskUserQuestion` with devbox names as options +(header: "Devbox", question: "Which devbox?"). + +Run `node scripts/sealos-devbox.mjs get {name}`. Display: name, runtime, status, quota, +SSH info, ports, env, pods. + +--- + +### Update + +**3a.** If no name given → List, then `AskUserQuestion` to pick which devbox +(options = devbox names from list). + +**3b.** Run `node scripts/sealos-devbox.mjs get {name}`, show current specs. + +**3c.** `AskUserQuestion` (header: "Update", question: "What to change?", multiSelect: true): +- "CPU & Memory" +- "Ports" + +**For CPU & Memory:** Follow up with `AskUserQuestion` for each field offering allowed values. +See `references/api-reference.md` for allowed ranges. + +**For Ports:** Show current ports. Then `AskUserQuestion`: +- "Add a port" +- "Remove a port" (list existing ports as sub-options) +- "Toggle public access" (list existing ports) +- "Replace all ports" (specify new port list) + +**3c-ports.** When ports are changing, display a clear before/after: + +``` +Ports (before → after): + + ✓ 3000 http public (keeping) + - 8080 http public (removing) + + 9090 grpc private (adding) +``` + +Warn if any existing ports will be removed. Require explicit confirmation for port removals. + +**3d.** Show before/after diff, then `AskUserQuestion` (header: "Confirm", +question: "Apply these changes?"): +- "Apply (Recommended)" +- "Edit again" +- "Cancel" + +**3e.** Run `node scripts/sealos-devbox.mjs update {name} '{json}'`. + +**Important:** When updating ports, the `ports` array in the update body is a **full replacement**. +Include all existing ports you want to keep (with their `portName`), plus any new ports. +Existing ports omitted from the array will be deleted. + +--- + +### Delete + +**This is destructive. Maximum friction.** + +**3a.** If no name given → List, then `AskUserQuestion` to pick which devbox. + +**3b.** Run `node scripts/sealos-devbox.mjs get {name}`, show full details. + +**3c.** Explain consequences: +> Deleting a devbox permanently removes the environment, all files inside it, SSH keys, +> and port configurations. This cannot be undone. + +**3d.** `AskUserQuestion`: +- header: "Confirm Delete" +- question: "Type `{name}` to permanently delete this devbox" +- options: ["Cancel"] — do NOT include the devbox name as a clickable option. + The user must type the exact name via "Type something" to confirm. + +If user types the correct name → proceed to 3e. +If user types something else → reply "Name doesn't match" and re-ask. +If user clicks Cancel → abort. + +**3e.** Run `node scripts/sealos-devbox.mjs delete {name}`. + +--- + +### Action (Start/Pause/Shutdown/Restart) + +**3a.** If no name given → List, then `AskUserQuestion` to pick which devbox. + +**3b.** For pause and shutdown, explain the difference: + +> **Pause** — Quick suspend. The pod is paused but resources stay allocated. Fast resume (~seconds). +> Use when you're stepping away briefly. +> +> **Shutdown** — Full stop. Resources are released. Slower resume (~30s-1min) as the pod must restart. +> Use when you're done for the day or want to save costs. + +**3c.** `AskUserQuestion` to confirm (header: "Action", question: "Confirm {action} on {name}?"): +- "{Action} now" +- "Cancel" + +**3d.** Run `node scripts/sealos-devbox.mjs {action} {name}`. +**3e.** For `start`: poll `node scripts/sealos-devbox.mjs get {name}` until `running`. + +--- + +### SSH Connect + +**3a.** If no name given → List, then `AskUserQuestion` to pick which devbox. + +**3b.** Run `node scripts/sealos-devbox.mjs get {name}`. Extract SSH info. + +**3c.** Check if SSH key exists at `~/.config/sealos-devbox/keys/{name}.pem`. + +**3d.** Display SSH connection info: + +``` +SSH Connection: + + Host: {ssh.host} + Port: {ssh.port} + User: {ssh.user} + Key: ~/.config/sealos-devbox/keys/{name}.pem + Working Dir: {ssh.workingDir} + + Command: ssh -i ~/.config/sealos-devbox/keys/{name}.pem -p {ssh.port} {ssh.user}@{ssh.host} +``` + +**3e.** `AskUserQuestion`: +- header: "SSH Setup" +- question: "How would you like to connect?" +- options: + 1. "Write SSH config" — append Host block to `~/.ssh/config` + 2. "Copy SSH command" — just show the command + 3. "VS Code Remote SSH" — show instructions for VS Code Remote-SSH extension + 4. "Done" + +--- + +### Release + +**3a.** If no name given → List, then `AskUserQuestion` to pick which devbox. + +**3b.** `AskUserQuestion` (header: "Release", question: "What would you like to do?"): +- "Create a release" +- "List releases" +- "Delete a release" + +**Create release:** +`AskUserQuestion` (header: "Release Tag", question: "Tag for this release?"): +- Suggest tags like "v1.0.0", "v0.1.0", or based on existing releases +- Ask for optional description + +Build body: `{"tag":"v1.0.0","releaseDescription":"...","startDevboxAfterRelease":true}` + +Run `node scripts/sealos-devbox.mjs create-release {name} ''`. +Show status (202 Accepted = async). Suggest checking `list-releases` for progress. + +**List releases:** +Run `node scripts/sealos-devbox.mjs list-releases {name}`. Format as table: + +``` +Tag Description Created Image +v1.0.0 First release 2024-01-15 10:00 ghcr.io/... +v0.1.0 Beta 2024-01-10 08:00 ghcr.io/... +``` + +**Delete release:** +Run `node scripts/sealos-devbox.mjs list-releases {name}` first. `AskUserQuestion` with +tags as options (header: "Delete Release", question: "Which release to delete?"). +Confirm before deleting. + +Run `node scripts/sealos-devbox.mjs delete-release {name} {tag}`. + +--- + +### Deploy + +**3a.** If no name given → List, then `AskUserQuestion` to pick which devbox. + +**3b.** Run `node scripts/sealos-devbox.mjs list-releases {name}`. If no releases, +guide user to create one first. + +**3c.** `AskUserQuestion` (header: "Deploy", question: "Which release to deploy?"): +- List available release tags as options + +**3d.** `AskUserQuestion` to confirm (header: "Confirm Deploy", +question: "Deploy `{tag}` of `{name}` to AppLaunchpad?"): +- "Deploy now" +- "Cancel" + +**3e.** Run `node scripts/sealos-devbox.mjs deploy {name} {tag}`. + +**3f.** Show deployment status. Run `node scripts/sealos-devbox.mjs list-deployments {name}` +to verify. + +--- + +### Monitor + +Read `references/operations.md` → Monitor section and follow the steps there. + +--- + +### Autostart + +Read `references/operations.md` → Autostart section and follow the steps there. + +--- + +## Scripts + +Two entry points in `scripts/` (relative to this skill's directory): +- `sealos-auth.mjs` — OAuth2 Device Grant login (shared across all skills) +- `sealos-devbox.mjs` — Devbox operations + +**Auth commands:** +```bash +node $SCRIPTS/sealos-auth.mjs check # Check if authenticated +node $SCRIPTS/sealos-auth.mjs login # Start OAuth2 login +node $SCRIPTS/sealos-auth.mjs login --insecure # Skip TLS verification +node $SCRIPTS/sealos-auth.mjs info # Show auth details +``` + +Zero external dependencies (Node.js only). TLS verification is disabled for self-signed certs. + +**The scripts are bundled with this skill — do NOT check if they exist. Just run them.** + +**Path resolution:** Scripts are in this skill's `scripts/` directory. The full path is +listed in the system environment's "Additional working directories" — use it directly. + +**Config resolution:** The script reads `~/.sealos/auth.json` (region) and `~/.sealos/kubeconfig` +(credentials) — both created by `sealos-auth.mjs login`. + +```bash +# Examples use SCRIPT as placeholder — replace with /scripts/sealos-devbox.mjs + +# After login, everything just works — API URL derived from auth.json region +node $SCRIPT templates +node $SCRIPT list +node $SCRIPT get my-devbox +node $SCRIPT create-wait '{"name":"my-devbox","runtime":"node.js","quota":{"cpu":1,"memory":2},"ports":[{"number":3000}]}' +node $SCRIPT update my-devbox '{"quota":{"cpu":2,"memory":4}}' +node $SCRIPT update my-devbox '{"ports":[{"portName":"existing-port","isPublic":false},{"number":8080}]}' +node $SCRIPT delete my-devbox +node $SCRIPT start|pause|shutdown|restart my-devbox +node $SCRIPT autostart my-devbox '{"execCommand":"npm start"}' +node $SCRIPT list-releases my-devbox +node $SCRIPT create-release my-devbox '{"tag":"v1.0.0","releaseDescription":"First release"}' +node $SCRIPT delete-release my-devbox v1.0.0 +node $SCRIPT deploy my-devbox v1.0.0 +node $SCRIPT list-deployments my-devbox +node $SCRIPT monitor my-devbox [start] [end] [step] +``` + +## Reference Files + +- `references/api-reference.md` — API endpoints, resource constraints, error formats. Read first. +- `references/defaults.md` — Resource presets, runtime recommendations, config templates. Read for create operations. +- `references/openapi.json` — Complete OpenAPI spec. Read only for edge cases. + +## Error Handling + +**Treat each error independently.** Do NOT chain unrelated errors. + +| Scenario | Action | +|----------|--------| +| Kubeconfig not found | Run `node scripts/sealos-auth.mjs login` to authenticate | +| Auth error (401) | Kubeconfig expired. Run `node scripts/sealos-auth.mjs login` to re-authenticate. | +| Name conflict (409) | Suggest alternative name | +| Invalid specs | Explain constraint, suggest valid value | +| Creation timeout (>2 min) | Offer to keep polling, or direct user to the console URL from the response (`consoleUrl` field) | +| Release 202 (async) | Explain it's building, suggest polling releases list | +| Templates API failure | Show error, offer to retry or enter runtime manually (use api-reference.md runtime list as fallback) | +| "namespace not found" (500) | Cluster admin kubeconfig; need Sealos user kubeconfig | + +## Rules + +- NEVER ask a question as plain text — ALWAYS use `AskUserQuestion` with options +- NEVER ask user to manually download kubeconfig — always use `scripts/sealos-auth.mjs login` +- NEVER run `test -f` or `ls` on the skill scripts — they are always present, just run them +- NEVER write kubeconfig to `~/.kube/config` — may overwrite user's existing config +- NEVER echo kubeconfig content to output +- NEVER delete without explicit name confirmation +- NEVER construct HTTP requests inline — always use `scripts/sealos-devbox.mjs` +- When writing to `~/.ssh/config`, append, don't overwrite +- Runtime must come from `node scripts/sealos-devbox.mjs templates` +- SSH keys are auto-saved on create — do not ask the user to manually save them +- When updating ports, remember the array is a full replacement — include all ports to keep +- Explain pause vs shutdown difference when the user asks for either action diff --git a/skills/devbox/config.json b/skills/devbox/config.json new file mode 100644 index 0000000..08876ee --- /dev/null +++ b/skills/devbox/config.json @@ -0,0 +1,9 @@ +{ + "client_id": "af993c98-d19d-4bdc-b338-79b80dc4f8bf", + "default_region": "https://gzg.sealos.run", + "regions": [ + "https://gzg.sealos.run", + "https://bja.sealos.run", + "https://hzh.sealos.run" + ] +} diff --git a/skills/devbox/references/api-reference.md b/skills/devbox/references/api-reference.md new file mode 100644 index 0000000..4ef29ea --- /dev/null +++ b/skills/devbox/references/api-reference.md @@ -0,0 +1,285 @@ +# Sealos Devbox API Reference + +Base URL: `https://devbox.{domain}/api/v2alpha` + +> **API Version:** `v2alpha` — centralized in the script constant `API_PATH` (`scripts/sealos-devbox.mjs`). +> If the API version changes, update `API_PATH` in the script; the rest auto-follows. + +## TLS Note + +The script sets `rejectUnauthorized: false` for HTTPS requests because Sealos clusters +may use self-signed TLS certificates. Without this, Node.js would reject connections +to clusters that don't have publicly trusted certificates. + +## Authentication + +All requests require a URL-encoded kubeconfig YAML in the `Authorization` header, +**except** `GET /devbox/templates` which requires no authentication. + +``` +Authorization: +``` + +## Available Runtimes + +Runtimes are fetched dynamically via `GET /devbox/templates`. The following are typically available: + +### Languages + +| Runtime | Identifier | +|---------|-----------| +| Python | `python` | +| Node.js | `node.js` | +| Go | `go` | +| Java | `java` | +| Rust | `rust` | +| C | `c` | +| C++ | `cpp` | +| PHP | `php` | + +### Web Frameworks + +| Runtime | Identifier | +|---------|-----------| +| Next.js | `next.js` | +| React | `react` | +| Vue | `vue` | +| Angular | `angular` | +| Svelte | `svelte` | +| Nuxt 3 | `nuxt3` | +| Astro | `astro` | +| Umi | `umi` | +| Express.js | `express.js` | +| Django | `django` | +| Flask | `flask` | +| Gin | `gin` | +| Echo | `echo` | +| Chi | `chi` | +| Iris | `iris` | +| Rocket | `rocket` | +| Vert.x | `vert.x` | +| Quarkus | `quarkus` | +| .NET | `net` | + +### Static & Documentation + +| Runtime | Identifier | +|---------|-----------| +| Nginx | `nginx` | +| Hexo | `hexo` | +| Docusaurus | `docusaurus` | +| VitePress | `vitepress` | + +### Platforms + +| Runtime | Identifier | +|---------|-----------| +| Ubuntu | `ubuntu` | +| Debian SSH | `debian-ssh` | +| Sealaf | `sealaf` | +| Claude Code | `claude-code` | + +## Resource Constraints + +### Create (POST /devbox) + +| Field | Type | Range | Default | +|-------|------|-------|---------| +| cpu | number | 0.1 - 32 cores | 1 | +| memory | number | 0.1 - 32 GB | 2 | + +### Update (PATCH /devbox/{name}) + +| Field | Type | Range | Notes | +|-------|------|-------|-------| +| cpu | number | 0.1 - 32 cores | | +| memory | number | 0.1 - 32 GB | | + +All update fields are optional — only provide fields to change. + +## Port Constraints + +| Field | Type | Constraint | +|-------|------|-----------| +| number | number | 1 - 65535 | +| protocol | string | `http`, `grpc`, or `ws` (default: `http`) | +| isPublic | boolean | Enable public domain access (default: `true`) | +| customDomain | string | Optional custom domain | + +## Endpoints + +### GET /devbox/templates — List Available Runtimes + +**No authentication required.** Returns runtime names and default configurations. + +Response: `200 OK` → Array of `{ runtime, config: { appPorts, ports, user, workingDir, releaseCommand, releaseArgs } }` + +### POST /devbox — Create Devbox + +```json +{ + "name": "my-devbox", + "runtime": "node.js", + "quota": { "cpu": 1, "memory": 2 }, + "ports": [{ "number": 3000, "protocol": "http", "isPublic": true }], + "env": [{ "name": "NODE_ENV", "value": "development" }], + "autostart": false +} +``` + +Response: `201 Created` → Devbox info with SSH credentials: + +```json +{ + "name": "my-devbox", + "sshPort": 40001, + "base64PrivateKey": "LS0tLS1CRUdJTi...", + "userName": "devbox", + "workingDir": "/home/devbox/project", + "domain": "cloud.sealos.io", + "ports": [{ "portName": "...", "number": 3000, "protocol": "http", "isPublic": true, "publicDomain": "xyz.cloud.sealos.io", "privateAddress": "..." }], + "autostarted": false, + "summary": { "totalPorts": 1, "successfulPorts": 1, "failedPorts": 0 } +} +``` + +**Important:** The `base64PrivateKey` is the SSH private key encoded in base64. Decode before saving to a file with 0600 permissions. + +### GET /devbox — List All Devboxes + +Response: `200 OK` → Array of `{ name, uid, resourceType, runtime, status, quota }` + +Status values: `pending`, `running`, `stopped`, `error` + +### GET /devbox/{name} — Get Devbox Details + +Response: `200 OK` → Full object with SSH info, ports, env, pods. + +Status values: `running`, `stopped`, `pending`, `error` + +SSH info: + +```json +{ + "ssh": { + "host": "devbox.cloud.sealos.io", + "port": 40001, + "user": "devbox", + "workingDir": "/home/devbox/project", + "privateKey": "base64-encoded (optional)" + } +} +``` + +### PATCH /devbox/{name} — Update Resources/Ports + +```json +{ + "quota": { "cpu": 2, "memory": 4 }, + "ports": [ + { "portName": "existing-port", "isPublic": false }, + { "number": 8080, "protocol": "http", "isPublic": true } + ] +} +``` + +Response: `204 No Content` + +**Port update behavior:** Include `portName` to update existing ports. Omit `portName` to create new ports. Existing ports not included in the array will be **deleted**. + +### DELETE /devbox/{name} — Delete Devbox + +Response: `204 No Content` + +### POST /devbox/{name}/start — Start Devbox + +Response: `204 No Content` + +### POST /devbox/{name}/pause — Pause Devbox + +Pauses the devbox (quick resume, keeps resources allocated). + +Response: `204 No Content` + +### POST /devbox/{name}/shutdown — Shutdown Devbox + +Shuts down the devbox (releases resources, slower resume). + +Response: `204 No Content` + +### POST /devbox/{name}/restart — Restart Devbox + +Response: `204 No Content` + +### POST /devbox/{name}/autostart — Configure Autostart + +```json +{ + "execCommand": "/bin/bash /home/devbox/project/entrypoint.sh" +} +``` + +Body is optional — send empty `{}` to enable autostart with default behavior. + +Response: `204 No Content` + +### GET /devbox/{name}/releases — List Releases + +Response: `200 OK` → Array of `{ id, name, devboxName, createdAt, tag, description, image }` + +### POST /devbox/{name}/releases — Create Release + +```json +{ + "tag": "v1.0.0", + "releaseDescription": "First stable release", + "execCommand": "nohup /home/devbox/project/entrypoint.sh > /dev/null 2>&1 &", + "startDevboxAfterRelease": true +} +``` + +Response: `202 Accepted` → `{ "name": "...", "status": "creating" }` + +**Note:** Release creation is asynchronous. Poll `GET /devbox/{name}/releases` to track progress. + +### DELETE /devbox/{name}/releases/{tag} — Delete Release + +Response: `204 No Content` + +### POST /devbox/{name}/releases/{tag}/deploy — Deploy Release + +Deploys the release to AppLaunchpad. No request body needed. + +Response: `204 No Content` + +### GET /devbox/{name}/deployments — List Deployments + +Response: `200 OK` → Array of `{ name, resourceType, tag }` + +Resource types: `deployment`, `statefulset` + +### GET /devbox/{name}/monitor — Get Metrics + +Query parameters: +- `start` (optional) — Unix timestamp (seconds or milliseconds). Defaults to `end − 3h`. +- `end` (optional) — Unix timestamp. Defaults to current server time. +- `step` (optional) — Sampling interval (e.g. `1m`, `5m`, `1h`). Default: `2m`. + +Response: `200 OK` → Array of `{ timestamp, readableTime, cpu, memory }` + +CPU and memory values are utilization percentages. + +## Error Response Format + +```json +{ + "error": { + "type": "validation_error", + "code": "INVALID_PARAMETER", + "message": "...", + "details": [...] + } +} +``` + +Types: `validation_error`, `resource_error`, `internal_error` diff --git a/skills/devbox/references/defaults.md b/skills/devbox/references/defaults.md new file mode 100644 index 0000000..25ca27a --- /dev/null +++ b/skills/devbox/references/defaults.md @@ -0,0 +1,139 @@ +# Sealos Devbox Defaults & Presets + +## Resource Presets (internal) + +Used to set initial default values based on user intent. These are NOT shown +to the user as "tiers" — the user sees individual CPU/Memory fields. + +| Scenario | CPU | Memory | Trigger phrases | +|----------|-----|--------|-----------------| +| Default | 1 | 2 GB | no size hint, "dev", "testing", "try" | +| Medium | 2 | 4 GB | "medium", "moderate" | +| Production | 4 | 8 GB | "prod", "production", "deploy", "release" | +| Minimal | 0.5 | 1 GB | "minimal", "tiny", "small", "lightweight" | +| Custom | — | — | specific numbers like "4 cores, 8g memory" | + +## Runtime Recommendation Rules + +Match project tech stack to a recommended runtime. + +| Project file signals | Recommended runtime | Why | +|---------------------|--------------------|----| +| `package.json` + `next.config.*` | `next.js` | Next.js framework detected | +| `package.json` + `nuxt.config.*` | `nuxt3` | Nuxt 3 framework detected | +| `package.json` + `angular.json` | `angular` | Angular framework detected | +| `package.json` + `svelte.config.*` | `svelte` | Svelte framework detected | +| `package.json` + `astro.config.*` | `astro` | Astro framework detected | +| `package.json` + `.umirc.*` or `config/config.*` | `umi` | Umi framework detected | +| `package.json` + `vue.config.*` or `vite.config.*` + vue dep | `vue` | Vue framework detected | +| `package.json` + react dep (no Next/Nuxt/Astro) | `react` | React app detected | +| `package.json` + express dep | `express.js` | Express.js app detected | +| `package.json` (generic) | `node.js` | Node.js project | +| `go.mod` + gin import | `gin` | Gin framework detected | +| `go.mod` + echo import | `echo` | Echo framework detected | +| `go.mod` + chi import | `chi` | Chi framework detected | +| `go.mod` + iris import | `iris` | Iris framework detected | +| `go.mod` (generic) | `go` | Go project | +| `requirements.txt` or `pyproject.toml` + django | `django` | Django framework detected | +| `requirements.txt` or `pyproject.toml` + flask | `flask` | Flask framework detected | +| `requirements.txt` or `pyproject.toml` (generic) | `python` | Python project | +| `Cargo.toml` + rocket dep | `rocket` | Rocket framework detected | +| `Cargo.toml` (generic) | `rust` | Rust project | +| `pom.xml` or `build.gradle` + quarkus | `quarkus` | Quarkus framework detected | +| `pom.xml` or `build.gradle` + vertx | `vert.x` | Vert.x framework detected | +| `pom.xml` or `build.gradle` (generic) | `java` | Java project | +| `*.csproj` or `*.sln` | `net` | .NET project | +| `composer.json` | `php` | PHP project | +| `docusaurus.config.*` | `docusaurus` | Docusaurus project | +| `docs/` + `mkdocs.yml` or VitePress config | `vitepress` | Documentation project | +| `_config.yml` (Hexo) | `hexo` | Hexo project | +| `nginx.conf` or static HTML | `nginx` | Static site | +| No project files / general purpose | `ubuntu` | General-purpose dev environment | + +When multiple runtimes fit, prefer the first match (more specific wins). + +## Config Summary Template + +Display this read-only summary before asking the user to confirm or customize. + +``` +Devbox config: + + Name: [name] + Runtime: [runtime] ([reason]) + CPU: [n] Core(s) + Memory: [n] GB + Ports: [port1] (http, public), [port2] (grpc, private) +``` + +## Field Generation Rules + +- **Runtime list**: always derive from `sealos-devbox.mjs templates` output, not hardcoded +- **Runtime suffix for name**: py, node, go, rs, java, next, vue, react, ng, svelte, nuxt, astro, umi, express, django, flask, gin, echo, chi, iris, rocket, vertx, quarkus, net, php, nginx, hexo, docusaurus, vitepress, ubuntu, debian, c, cpp, claude-code, sealaf +- **Name**: `[project-directory-name]-[runtime-suffix]`, lowercased, truncated to 63 chars + +## AskUserQuestion Option Guidelines + +**Hard limit: max 4 options per `AskUserQuestion` call.** The tool auto-appends implicit +options ("Type something", "Chat about this") which consume slots. More than 4 user-provided +options will be truncated and invisible to the user. + +When building options for `AskUserQuestion`: +- **Name options**: generate 2-3 name suggestions from project dir + runtime. If a name + already exists (from list), avoid it and note the conflict. +- **Runtime options**: Always output ALL runtimes as a numbered text list first, then + AskUserQuestion with max 4 clickable options (top 4 runtimes for the context). + Mark recommended with "(Recommended)". User can type any other runtime name/number. +- **CPU options**: max 4 items: 0.5, 1, 2, 4 cores. +- **Memory options**: max 4 items: 1, 2, 4, 8 GB. +- For all resource options, mark current value with "(current)". + User can type other valid values via "Type something". +- **Devbox picker** (for get/update/delete/action): list devbox names from + `sealos-devbox.mjs list` as options, up to 4. If more than 4, show most recent ones. + +## Default Ports by Runtime + +These come from the templates API `config.appPorts` field. Common defaults: + +| Runtime | Default Port | Protocol | +|---------|-------------|----------| +| Most frameworks | 8080 | http | +| next.js | 3000 | http | +| react | 3000 | http | +| vue | 3000 | http | +| angular | 4200 | http | +| svelte | 5173 | http | +| astro | 4321 | http | +| nuxt3 | 3000 | http | +| nginx | 80 | http | +| express.js | 3000 | http | +| django | 8000 | http | +| flask | 5000 | http | + +**Note:** Always prefer the port from the templates API over this table. This table is a +fallback reference only. + +## SSH Key Management + +- **Key path**: `~/.config/sealos-devbox/keys/{name}.pem` +- **Permissions**: `0600` (read/write owner only) +- **Auto-save**: The script automatically saves the private key on `create` and `create-wait` + +### SSH Config Format + +When offering to write SSH config, use this format: + +``` +Host sealos-{name} + HostName {ssh.host} + Port {ssh.port} + User {ssh.user} + IdentityFile ~/.config/sealos-devbox/keys/{name}.pem + StrictHostKeyChecking no +``` + +### VS Code Remote SSH + +After writing SSH config, the user can connect via: +- VS Code: `Remote-SSH: Connect to Host...` → select `sealos-{name}` +- Terminal: `ssh sealos-{name}` diff --git a/skills/devbox/references/openapi.json b/skills/devbox/references/openapi.json new file mode 100644 index 0000000..22e6040 --- /dev/null +++ b/skills/devbox/references/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"Devbox API","version":"2.0.0-alpha","description":"Manage Devbox development environments — create, configure, control lifecycle, and monitor container-based isolated dev environments.\n\n## Authentication\n\nAll endpoints require authentication via URL-encoded kubeconfig. Set the `Authorization` header to `encodeURIComponent(kubeconfigYaml)` before sending the request. Obtain your kubeconfig from the Sealos console.\n\n## Errors\n\nAll error responses use a unified format:\n\n```json\n{\n \"error\": {\n \"type\": \"validation_error\",\n \"code\": \"INVALID_PARAMETER\",\n \"message\": \"...\",\n \"details\": [...]\n }\n}\n```\n\n- `type` — high-level category (e.g. `validation_error`, `resource_error`, `internal_error`)\n- `code` — stable identifier for programmatic handling\n- `message` — human-readable explanation\n- `details` — optional extra context; shape varies by `code`\n\n## Operations\n\n**Query** (read-only): returns `200 OK` with data in the response body.\n\n**Mutation** (write):\n- Create (sync) → `201 Created` with the created resource in the response body.\n- Create (async) → `202 Accepted` with `{ \"name\": \"...\", \"status\": \"creating\" }`. Poll the corresponding `GET` endpoint to track progress.\n- Update / Delete / Action → `204 No Content` with no response body."},"tags":[{"name":"Query","description":"Read-only operations. Success: `200 OK` with data in the response body."},{"name":"Mutation","description":"Write operations. Sync create: `201 Created` with the new resource. Async create: `202 Accepted` with `{ name, status }` (poll GET to track progress). Update/Delete/Action: `204 No Content`."}],"servers":[{"url":"http://localhost:3000/api/v2alpha","description":"Local development"},{"url":"https://devbox.192.168.12.53.nip.io/api/v2alpha","description":"Production"},{"url":"{baseUrl}/api/v2alpha","description":"Custom","variables":{"baseUrl":{"default":"https://devbox.example.com","description":"Base URL of your instance (e.g. https://devbox.192.168.x.x.nip.io)"}}}],"security":[{"kubeconfigAuth":[]},{"jwtAuth":[]}],"paths":{"/devbox":{"get":{"tags":["Query"],"operationId":"listDevboxes","summary":"List all devboxes","description":"Retrieve all Devbox instances in the current namespace with resource and runtime information.","responses":{"200":{"description":"Devbox list retrieved successfully.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Devbox name"},"uid":{"type":"string","description":"Devbox UID"},"resourceType":{"type":"string","default":"devbox","description":"Resource type"},"runtime":{"type":"string","description":"Runtime environment (e.g., go, python, node.js)"},"status":{"type":"string","enum":["pending","running","stopped","error"],"description":"Devbox status (pending, running, stopped, error)"},"quota":{"type":"object","properties":{"cpu":{"type":"number","description":"CPU in cores (e.g., 1.0 = 1 core)"},"memory":{"type":"number","description":"Memory in GB (e.g., 2.0 = 2GB)"}},"required":["cpu","memory"],"description":"Resource quota allocation"}},"required":["name","uid","resourceType","runtime","status","quota"]}},"examples":{"success":{"summary":"Two devboxes","value":[{"name":"my-python-api","uid":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","resourceType":"devbox","runtime":"python","status":"running","quota":{"cpu":1,"memory":2}},{"name":"my-go-service","uid":"b2c3d4e5-f6a7-8901-bcde-f12345678901","resourceType":"devbox","runtime":"go","status":"stopped","quota":{"cpu":2,"memory":4}}]},"empty":{"summary":"No devboxes","value":[]}}}}},"401":{"description":"No valid credentials provided, or credentials have expired.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"unauthorized":{"summary":"Authentication required","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"No valid credentials provided."}}}}}}},"500":{"description":"Failed to retrieve the devbox list.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"]},"code":{"type":"string","enum":["INTERNAL_ERROR"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"serverError":{"summary":"Internal error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Failed to list devboxes from Kubernetes."}}}}}}}}},"post":{"tags":["Mutation"],"operationId":"createDevbox","summary":"Create a new devbox","description":"Create a new Devbox instance with the specified runtime, resources, and network ports. CPU and memory quota must be in the range [0.1, 32].","requestBody":{"description":"Devbox creation parameters.\n\n**Example — Python devbox with a public HTTP port:**\n```json\n{\n \"name\": \"my-python-api\",\n \"runtime\": \"python\",\n \"quota\": { \"cpu\": 1, \"memory\": 2 },\n \"ports\": [{ \"number\": 8080, \"protocol\": \"http\", \"isPublic\": true }],\n \"env\": [],\n \"autostart\": false\n}\n```\n\n**Example — Go devbox with environment variables and autostart:**\n```json\n{\n \"name\": \"my-go-service\",\n \"runtime\": \"go\",\n \"quota\": { \"cpu\": 0.5, \"memory\": 1 },\n \"ports\": [],\n \"env\": [{ \"name\": \"GO_ENV\", \"value\": \"development\" }],\n \"autostart\": true\n}\n```\n\n**Example — minimal resources (floor values):**\n```json\n{\n \"name\": \"my-minimal-devbox\",\n \"runtime\": \"node.js\",\n \"quota\": { \"cpu\": 0.1, \"memory\": 0.1 },\n \"ports\": [],\n \"env\": [],\n \"autostart\": false\n}\n```","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","minLength":1,"maxLength":63,"description":"Devbox name (must be DNS compliant: lowercase, numbers, hyphens, 1-63 chars)"},"runtime":{"type":"string","enum":["nuxt3","angular","quarkus","ubuntu","flask","java","chi","net","iris","hexo","python","docusaurus","vitepress","cpp","vue","nginx","rocket","debian-ssh","vert.x","express.js","django","next.js","sealaf","go","react","php","svelte","c","astro","umi","gin","node.js","echo","claude-code","rust"],"description":"Runtime environment name"},"quota":{"type":"object","properties":{"cpu":{"type":"number","minimum":0.1,"maximum":32,"description":"CPU allocation in cores","example":1},"memory":{"type":"number","minimum":0.1,"maximum":32,"description":"Memory allocation in GB","example":2}},"required":["cpu","memory"],"description":"Resource allocation for CPU and memory"},"ports":{"type":"array","items":{"type":"object","properties":{"number":{"type":"number","minimum":1,"maximum":65535,"description":"Port number (1-65535)"},"protocol":{"type":"string","enum":["http","grpc","ws"],"description":"Protocol type, defaults to HTTP","default":"http"},"isPublic":{"type":"boolean","default":true,"description":"Enable public domain access, defaults to true"},"customDomain":{"type":"string","description":"Custom domain (optional)"}},"required":["number"]},"default":[],"description":"Port configurations (optional, can be empty)"},"env":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Environment variable name"},"value":{"type":"string","description":"Environment variable value"},"valueFrom":{"type":"object","properties":{"secretKeyRef":{"type":"object","properties":{"key":{"type":"string","description":"Secret key"},"name":{"type":"string","description":"Secret name"}},"required":["key","name"]}},"required":["secretKeyRef"],"description":"Source for the environment variable value"}},"required":["name"]},"default":[],"description":"Environment variables (optional, can be empty)"},"autostart":{"type":"boolean","default":false,"description":"Auto start devbox after creation (defaults to false)"}},"required":["name","runtime","quota"]}}}},"responses":{"201":{"description":"Devbox created. SSH connection details (port, private key, domain) are provisioned synchronously and returned in this response. The container pod starts asynchronously — poll `GET /devbox/{name}` until `status` is `running` before establishing an SSH connection.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Devbox name","example":"my-python-api"},"sshPort":{"type":"number","description":"SSH node port for connecting to the Devbox","example":40001},"base64PrivateKey":{"type":"string","description":"Base64-encoded SSH private key. Decode before use.","example":"LS0tLS1CRUdJTi..."},"userName":{"type":"string","description":"SSH login username","example":"devbox"},"workingDir":{"type":"string","description":"Default working directory inside the Devbox","example":"/home/devbox/project"},"domain":{"type":"string","description":"Base domain of the Sealos instance","example":"cloud.sealos.io"},"ports":{"type":"array","items":{"type":"object","properties":{"portName":{"type":"string","description":"Generated port name"},"number":{"type":"number","description":"Port number"},"protocol":{"type":"string","enum":["http","grpc","ws"],"description":"Protocol type"},"networkName":{"type":"string","description":"Network/Ingress name"},"isPublic":{"type":"boolean","description":"Whether public domain is enabled"},"publicDomain":{"type":"string","description":"Generated public domain"},"customDomain":{"type":"string","description":"Custom domain (if provided)"},"serviceName":{"type":"string","description":"Service name"},"privateAddress":{"type":"string","description":"Private address for internal access"},"error":{"type":"string","description":"Error message if port creation failed"}},"required":["portName","number","protocol","networkName","isPublic","publicDomain","customDomain","serviceName","privateAddress"]},"description":"Created port configurations (may be empty)"},"autostarted":{"type":"boolean","description":"Whether the autostart job was triggered (`autostart: true` in request)"},"portErrors":{"type":"array","items":{"type":"object","properties":{"port":{"type":"number","description":"Port number that failed"},"error":{"type":"string","description":"Error message"}},"required":["port","error"]},"description":"Ports that failed to be created (only present when partial failure occurred)"},"summary":{"type":"object","properties":{"totalPorts":{"type":"number","description":"Total number of ports"},"successfulPorts":{"type":"number","description":"Number of successfully created ports"},"failedPorts":{"type":"number","description":"Number of failed ports"}},"required":["totalPorts","successfulPorts","failedPorts"],"description":"Aggregate counts for port creation"}},"required":["name","sshPort","base64PrivateKey","userName","workingDir","ports","summary"],"description":"Devbox creation response. The pod is starting asynchronously — poll GET /devbox/{name} until `status` is `running` before establishing an SSH connection."},"examples":{"success":{"summary":"Devbox created (pod starting)","value":{"name":"my-python-api","sshPort":40001,"base64PrivateKey":"LS0tLS1CRUdJTi...","userName":"devbox","workingDir":"/home/devbox/project","domain":"cloud.sealos.io","ports":[{"portName":"port-abc123def456","number":8080,"protocol":"http","networkName":"my-python-api-xyz789abc123","isPublic":true,"publicDomain":"xyz789abc.cloud.sealos.io","customDomain":"","serviceName":"my-python-api","privateAddress":"http://my-python-api.ns-user123:8080"}],"summary":{"totalPorts":1,"successfulPorts":1,"failedPorts":0}}}}}}},"400":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"]},"code":{"type":"string","enum":["INVALID_PARAMETER"]},"message":{"type":"string"},"details":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},{"type":"string"}]}},"required":["type","code","message"]}},"required":["error"]},"examples":{"invalidParam":{"summary":"Missing required field","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid request body","details":[{"field":"name","message":"String must contain at least 1 character(s)"}]}}}}}}},"401":{"description":"No valid credentials provided, or credentials have expired.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"unauthorized":{"summary":"Authentication required","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"No valid credentials provided."}}}}}}},"404":{"description":"The specified runtime does not exist or is not available.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error"},"code":{"type":"string","const":"NOT_FOUND"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"runtimeNotFound":{"summary":"Runtime not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Runtime 'invalid-runtime' not found or not available."}}}}}}},"409":{"description":"A Devbox with the specified name already exists.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error"},"code":{"type":"string","enum":["ALREADY_EXISTS"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"alreadyExists":{"summary":"Name conflict","value":{"error":{"type":"resource_error","code":"ALREADY_EXISTS","message":"Devbox already exists."}}}}}}},"500":{"description":"Failed to create the Devbox.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"]},"code":{"type":"string","enum":["INTERNAL_ERROR"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"serverError":{"summary":"Internal error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Internal server error."}}}}}}}}}},"/devbox/{name}":{"get":{"tags":["Query"],"operationId":"getDevbox","summary":"Get devbox details","description":"Retrieve complete configuration, runtime status, SSH connection information, ports, and pod list for a specific Devbox.","parameters":[{"name":"name","in":"path","required":true,"description":"Devbox name (format: lowercase alphanumeric with hyphens, 1–63 characters)","schema":{"type":"string","pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","minLength":1,"maxLength":63,"example":"my-python-api"}}],"responses":{"200":{"description":"Devbox details retrieved successfully.","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Devbox name","example":"my-devbox"},"createdAt":{"type":"string","description":"Creation time in ISO format","example":"2023-12-07T10:00:00.000Z"},"upTime":{"type":"string","description":"Running duration since first pod started (human-readable)","example":"2d3h"},"uid":{"type":"string","description":"Unique identifier","example":"abc123-def456"},"resourceType":{"type":"string","default":"devbox","description":"Resource type","example":"devbox"},"runtime":{"type":"string","description":"Runtime environment name","example":"node.js"},"image":{"type":"string","description":"Container image","example":"ghcr.io/labring/sealos-devbox-nodejs:latest"},"status":{"type":"string","description":"Devbox status (running, stopped, pending, etc.)","example":"running"},"quota":{"type":"object","properties":{"cpu":{"type":"number","description":"CPU allocation in cores","example":1},"memory":{"type":"number","description":"Memory allocation in GB","example":2}},"required":["cpu","memory"],"description":"CPU and memory quota allocation"},"ssh":{"type":"object","properties":{"host":{"type":"string","description":"SSH host address","example":"devbox.cloud.sealos.io"},"port":{"type":["number","null"],"description":"SSH port number, null if not yet assigned","example":40001},"user":{"type":"string","description":"SSH username","example":"devbox"},"workingDir":{"type":"string","description":"Working directory path","example":"/home/devbox/project"},"privateKey":{"type":"string","description":"Base64 encoded private key (optional)"}},"required":["host","port","user","workingDir"],"description":"SSH connection details"},"env":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Environment variable name"},"value":{"type":"string","description":"Direct value of the environment variable"},"valueFrom":{"type":"object","properties":{"secretKeyRef":{"type":"object","properties":{"name":{"type":"string","description":"Secret name"},"key":{"type":"string","description":"Secret key"}},"required":["name","key"]}},"required":["secretKeyRef"],"description":"Reference to a secret value"}},"required":["name"],"description":"Environment variable configuration"},"description":"Environment variables (optional)"},"ports":{"type":"array","items":{"type":"object","properties":{"number":{"type":"number","description":"Port number","example":8080},"portName":{"type":"string","description":"Port name identifier"},"protocol":{"type":"string","description":"Protocol type (http, grpc, ws)","example":"http"},"privateAddress":{"type":"string","description":"Private access address","example":"http://my-devbox.ns-user123:8080"},"publicAddress":{"type":"string","description":"Public access address","example":"https://xyz789.cloud.sealos.io"},"customDomain":{"type":"string","description":"Custom domain (if configured)"}},"required":["number"],"description":"Port configuration details"},"description":"Port configurations"},"pods":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Pod name"},"status":{"type":"string","description":"Pod status (Running, Pending, Failed, etc.)","example":"Running"}},"required":["name","status"],"description":"Pod information"},"description":"Pod information"},"operationalStatus":{"description":"Operational status details (optional)"}},"required":["name","createdAt","uid","resourceType","runtime","image","status","quota","ssh","ports","pods"],"title":"Get DevBox Detail Response","description":"Response schema for getting Devbox details"},"examples":{"success":{"summary":"Running devbox","value":{"name":"my-python-api","uid":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","resourceType":"devbox","runtime":"python","image":"ghcr.io/labring/sealos-devbox-python:latest","status":"running","quota":{"cpu":1,"memory":2},"ssh":{"host":"devbox.cloud.sealos.io","port":40001,"user":"devbox","workingDir":"/home/devbox/project","privateKey":"LS0tLS1CRUdJTi..."},"env":[{"name":"NODE_ENV","value":"development"}],"ports":[{"number":8080,"portName":"port-abc123","protocol":"http","privateAddress":"http://my-python-api.ns-user123:8080","publicAddress":"https://xyz789abc.cloud.sealos.io"}],"pods":[{"name":"my-python-api-7d8f9b6c5d-abc12","status":"running"}]}},"pending":{"summary":"Pending devbox (SSH not yet ready)","value":{"name":"my-python-api","uid":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","resourceType":"devbox","runtime":"python","image":"ghcr.io/labring/sealos-devbox-python:latest","status":"pending","quota":{"cpu":1,"memory":2},"ssh":{"host":"devbox.cloud.sealos.io","port":null,"user":"devbox","workingDir":"/home/devbox/project"},"env":[],"ports":[],"pods":[]}}}}}},"400":{"description":"Invalid devbox name.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"]},"code":{"type":"string","enum":["INVALID_PARAMETER"]},"message":{"type":"string"},"details":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},{"type":"string"}]}},"required":["type","code","message"]}},"required":["error"]},"examples":{"invalidName":{"summary":"Missing name","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Devbox name is required."}}}}}}},"401":{"description":"No valid credentials provided, or credentials have expired.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"unauthorized":{"summary":"Authentication required","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"No valid credentials provided."}}}}}}},"404":{"description":"The specified Devbox does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error"},"code":{"type":"string","const":"NOT_FOUND"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"notFound":{"summary":"Devbox not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Devbox not found."}}}}}}},"500":{"description":"Failed to retrieve devbox details.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"]},"code":{"type":"string","enum":["INTERNAL_ERROR"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"serverError":{"summary":"Internal error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Internal server error."}}}}}}}}},"patch":{"tags":["Mutation"],"operationId":"updateDevbox","summary":"Update devbox configuration","description":"Update Devbox resource quota and/or port configuration. CPU and memory quota must be in the range [0.1, 32].\n\nKey points:\n- At least one of `quota` or `ports` must be provided.\n- To update an existing port supply its `portName`. To add a new port omit `portName`.\n- Ports not present in the `ports` array are deleted.","parameters":[{"name":"name","in":"path","required":true,"description":"Devbox name (format: lowercase alphanumeric with hyphens, 1–63 characters)","schema":{"type":"string","pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","minLength":1,"maxLength":63,"example":"my-python-api"}}],"requestBody":{"description":"Fields to update. At least one of `quota` or `ports` is required.\n\n**Example — update resources only:**\n```json\n{\n \"quota\": { \"cpu\": 2, \"memory\": 4 }\n}\n```\n\n**Example — replace all ports with a single new public port:**\n```json\n{\n \"ports\": [{ \"number\": 8080, \"protocol\": \"http\", \"isPublic\": true }]\n}\n```\n\n**Example — update an existing port and add a new one:**\n```json\n{\n \"ports\": [\n { \"portName\": \"port-abc123\", \"number\": 8080, \"protocol\": \"http\", \"isPublic\": true },\n { \"number\": 3000, \"protocol\": \"http\", \"isPublic\": false }\n ]\n}\n```","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"quota":{"type":"object","properties":{"cpu":{"type":"number","minimum":0.1,"maximum":32,"description":"CPU allocation in cores (optional)","example":1},"memory":{"type":"number","minimum":0.1,"maximum":32,"description":"Memory allocation in GB (optional)","example":2}},"description":"Resource allocation for CPU and memory (optional)","example":{"cpu":1,"memory":2}},"ports":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"portName":{"type":"string","description":"Existing port name to update (include to update specific port)"},"number":{"type":"number","minimum":1,"maximum":65535,"description":"Port number (1-65535) - optional for updates"},"protocol":{"type":"string","enum":["http","grpc","ws"],"description":"Protocol type - optional for updates"},"isPublic":{"type":"boolean","description":"Enable public domain access - optional for updates"},"customDomain":{"type":"string","description":"Custom domain - optional for updates"}}},{"type":"object","properties":{"number":{"type":"number","minimum":1,"maximum":65535,"description":"Port number (1-65535) - required for new ports"},"protocol":{"type":"string","enum":["http","grpc","ws"],"description":"Protocol type, defaults to HTTP","default":"http"},"isPublic":{"type":"boolean","default":true,"description":"Enable public domain access, defaults to true"},"customDomain":{"type":"string","description":"Custom domain (optional)"}},"required":["number"]}],"description":"Port configuration - include portName to update existing port, omit to create new port"},"description":"Array of port configurations. Include portName to update existing ports, exclude portName to create new ports. Existing ports not included will be deleted. (optional)"}},"title":"Update DevBox Request","description":"Request schema for updating DevBox resource and/or port configurations"}}}},"responses":{"204":{"description":"Devbox updated successfully."},"400":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"]},"code":{"type":"string","enum":["INVALID_PARAMETER"]},"message":{"type":"string"},"details":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},{"type":"string"}]}},"required":["type","code","message"]}},"required":["error"]},"examples":{"invalidParam":{"summary":"Invalid parameter","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid request body","details":[{"field":"quota.cpu","message":"Expected number, received string"}]}}}}}}},"401":{"description":"No valid credentials provided, or credentials have expired.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"unauthorized":{"summary":"Authentication required","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"No valid credentials provided."}}}}}}},"404":{"description":"Devbox not found, or a referenced `portName` does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error"},"code":{"type":"string","const":"NOT_FOUND"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"devboxNotFound":{"summary":"Devbox not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Devbox not found."}}},"portNotFound":{"summary":"Port name not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Port with name 'port-abc123' not found."}}}}}}},"409":{"description":"Port number is already in use by another port.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error"},"code":{"type":"string","enum":["ALREADY_EXISTS","CONFLICT"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"portConflict":{"summary":"Port number conflict","value":{"error":{"type":"resource_error","code":"CONFLICT","message":"Port 8080 already exists in service."}}}}}}},"422":{"description":"Resource specification rejected by the Kubernetes cluster.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"operation_error"},"code":{"type":"string","const":"INVALID_RESOURCE_SPEC"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"invalidSpec":{"summary":"Admission webhook rejection","value":{"error":{"type":"operation_error","code":"INVALID_RESOURCE_SPEC","message":"Invalid resource specification.","details":"admission webhook \"devbox.sealos.io\" denied the request: quota exceeded"}}}}}}},"500":{"description":"Failed to update the Devbox.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"]},"code":{"type":"string","enum":["INTERNAL_ERROR"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"serverError":{"summary":"Internal error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Internal server error."}}}}}}}}},"delete":{"tags":["Mutation"],"operationId":"deleteDevbox","summary":"Delete a devbox","description":"Delete a Devbox and all associated resources (services, ingress rules, certificates, persistent volumes).\n\nKey points:\n- **Idempotent** — if the Devbox does not exist the request still returns `204`.","parameters":[{"name":"name","in":"path","required":true,"description":"Devbox name (format: lowercase alphanumeric with hyphens, 1–63 characters)","schema":{"type":"string","pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","minLength":1,"maxLength":63,"example":"my-python-api"}}],"responses":{"204":{"description":"Devbox deleted successfully, or did not exist (idempotent)."},"400":{"description":"Invalid devbox name.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"]},"code":{"type":"string","enum":["INVALID_PARAMETER"]},"message":{"type":"string"},"details":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},{"type":"string"}]}},"required":["type","code","message"]}},"required":["error"]},"examples":{"invalidName":{"summary":"Invalid name format","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid devbox name format."}}}}}}},"401":{"description":"No valid credentials provided, or credentials have expired.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"unauthorized":{"summary":"Authentication required","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"No valid credentials provided."}}}}}}},"500":{"description":"Failed to delete the Devbox or its associated resources.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"]},"code":{"type":"string","enum":["INTERNAL_ERROR"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"serverError":{"summary":"Internal error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Internal server error."}}}}}}}}}},"/devbox/{name}/autostart":{"post":{"tags":["Mutation"],"operationId":"autostartDevbox","summary":"Configure devbox autostart","description":"Configure the command that runs automatically when the Devbox starts. If `execCommand` is omitted the default template entrypoint is used.","parameters":[{"name":"name","in":"path","required":true,"description":"Devbox name (format: lowercase alphanumeric with hyphens, 1–63 characters)","schema":{"type":"string","pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","minLength":1,"maxLength":63,"example":"my-python-api"}}],"requestBody":{"description":"Autostart configuration. The body is optional — send `{}` to use the default entrypoint.\n\n**Example — custom startup script:**\n```json\n{\n \"execCommand\": \"/bin/bash /home/devbox/project/startup.sh\"\n}\n```","required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"execCommand":{"type":"string","description":"Custom command to execute in the devbox (optional)","example":"/bin/bash /home/devbox/project/entrypoint.sh"}},"default":{},"description":"Request body for autostart configuration (optional, can be empty)"}}}},"responses":{"204":{"description":"Autostart configured successfully."},"400":{"description":"Invalid request parameters.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"]},"code":{"type":"string","enum":["INVALID_PARAMETER"]},"message":{"type":"string"},"details":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},{"type":"string"}]}},"required":["type","code","message"]}},"required":["error"]},"examples":{"invalidParam":{"summary":"Invalid devbox name","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid devbox name format."}}}}}}},"401":{"description":"No valid credentials provided, or credentials have expired.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"unauthorized":{"summary":"Authentication required","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"No valid credentials provided."}}}}}}},"404":{"description":"The specified Devbox does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error"},"code":{"type":"string","const":"NOT_FOUND"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"notFound":{"summary":"Devbox not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Devbox not found."}}}}}}},"500":{"description":"Failed to configure autostart.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"]},"code":{"type":"string","enum":["INTERNAL_ERROR"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"serverError":{"summary":"Internal error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Failed to create autostart resources."}}}}}}}}}},"/devbox/{name}/start":{"post":{"tags":["Mutation"],"operationId":"startDevbox","summary":"Start a devbox","description":"Start a paused or stopped Devbox and restore its network ingress rules to active state.\n\nKey points:\n- **Idempotent** — calling start on an already-running Devbox returns `204`.","parameters":[{"name":"name","in":"path","required":true,"description":"Devbox name (format: lowercase alphanumeric with hyphens, 1–63 characters)","schema":{"type":"string","pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","minLength":1,"maxLength":63,"example":"my-python-api"}}],"responses":{"204":{"description":"Devbox started successfully."},"400":{"description":"Invalid devbox name.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"]},"code":{"type":"string","enum":["INVALID_PARAMETER"]},"message":{"type":"string"},"details":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},{"type":"string"}]}},"required":["type","code","message"]}},"required":["error"]},"examples":{"invalidName":{"summary":"Invalid name format","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid devbox name format."}}}}}}},"401":{"description":"No valid credentials provided, or credentials have expired.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"unauthorized":{"summary":"Authentication required","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"No valid credentials provided."}}}}}}},"404":{"description":"The specified Devbox does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error"},"code":{"type":"string","const":"NOT_FOUND"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"notFound":{"summary":"Devbox not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Devbox not found."}}}}}}},"500":{"description":"Failed to start the Devbox.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"]},"code":{"type":"string","enum":["INTERNAL_ERROR"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"serverError":{"summary":"Internal error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Failed to start devbox."}}}}}}}}}},"/devbox/{name}/pause":{"post":{"tags":["Mutation"],"operationId":"pauseDevbox","summary":"Pause a devbox","description":"Pause a Devbox to stop its compute resources while preserving port allocations, reducing costs.\n\nKey points:\n- **Idempotent** — calling pause on an already-paused Devbox returns `204`.","parameters":[{"name":"name","in":"path","required":true,"description":"Devbox name (format: lowercase alphanumeric with hyphens, 1–63 characters)","schema":{"type":"string","pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","minLength":1,"maxLength":63,"example":"my-python-api"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","description":"Pause devbox request body (empty)"}}}},"responses":{"204":{"description":"Devbox paused successfully."},"400":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"]},"code":{"type":"string","enum":["INVALID_PARAMETER"]},"message":{"type":"string"},"details":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},{"type":"string"}]}},"required":["type","code","message"]}},"required":["error"]},"examples":{"invalidParam":{"summary":"Invalid body","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid request body."}}}}}}},"401":{"description":"No valid credentials provided, or credentials have expired.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"unauthorized":{"summary":"Authentication required","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"No valid credentials provided."}}}}}}},"404":{"description":"The specified Devbox does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error"},"code":{"type":"string","const":"NOT_FOUND"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"notFound":{"summary":"Devbox not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Devbox not found."}}}}}}},"500":{"description":"Failed to pause the Devbox.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"]},"code":{"type":"string","enum":["INTERNAL_ERROR"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"serverError":{"summary":"Internal error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Failed to pause devbox."}}}}}}}}}},"/devbox/{name}/shutdown":{"post":{"tags":["Mutation"],"operationId":"shutdownDevbox","summary":"Shutdown a devbox","description":"Completely shut down a Devbox, releasing all compute resources and port allocations to minimise costs.","parameters":[{"name":"name","in":"path","required":true,"description":"Devbox name (format: lowercase alphanumeric with hyphens, 1–63 characters)","schema":{"type":"string","pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","minLength":1,"maxLength":63,"example":"my-python-api"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","description":"Shutdown devbox request body (empty)"}}}},"responses":{"204":{"description":"Devbox shut down successfully."},"400":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"]},"code":{"type":"string","enum":["INVALID_PARAMETER"]},"message":{"type":"string"},"details":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},{"type":"string"}]}},"required":["type","code","message"]}},"required":["error"]},"examples":{"invalidParam":{"summary":"Invalid body","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid request body."}}}}}}},"401":{"description":"No valid credentials provided, or credentials have expired.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"unauthorized":{"summary":"Authentication required","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"No valid credentials provided."}}}}}}},"404":{"description":"The specified Devbox does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error"},"code":{"type":"string","const":"NOT_FOUND"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"notFound":{"summary":"Devbox not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Devbox not found."}}}}}}},"500":{"description":"Failed to shut down the Devbox.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"]},"code":{"type":"string","enum":["INTERNAL_ERROR"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"serverError":{"summary":"Internal error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Failed to shutdown devbox."}}}}}}}}}},"/devbox/{name}/restart":{"post":{"tags":["Mutation"],"operationId":"restartDevbox","summary":"Restart a devbox","description":"Trigger a complete restart cycle: stop all pods, wait for termination, restore ingress, then start the Devbox.\n\nKey points:\n- **Idempotent** — always triggers a restart regardless of the current state.","parameters":[{"name":"name","in":"path","required":true,"description":"Devbox name (format: lowercase alphanumeric with hyphens, 1–63 characters)","schema":{"type":"string","pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","minLength":1,"maxLength":63,"example":"my-python-api"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","description":"Restart devbox request body (empty)"}}}},"responses":{"204":{"description":"Devbox restarted successfully."},"400":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"]},"code":{"type":"string","enum":["INVALID_PARAMETER"]},"message":{"type":"string"},"details":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},{"type":"string"}]}},"required":["type","code","message"]}},"required":["error"]},"examples":{"invalidParam":{"summary":"Invalid body","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid request body."}}}}}}},"401":{"description":"No valid credentials provided, or credentials have expired.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"unauthorized":{"summary":"Authentication required","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"No valid credentials provided."}}}}}}},"404":{"description":"The specified Devbox does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error"},"code":{"type":"string","const":"NOT_FOUND"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"notFound":{"summary":"Devbox not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Devbox not found."}}}}}}},"500":{"description":"Restart cycle failed.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"]},"code":{"type":"string","enum":["INTERNAL_ERROR"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"serverError":{"summary":"Internal error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Failed to restart devbox."}}},"timeout":{"summary":"Restart timeout","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Max retries reached while waiting for devbox pod to be deleted."}}}}}}}}}},"/devbox/{name}/releases":{"get":{"tags":["Query"],"operationId":"listDevboxReleases","summary":"List devbox releases","description":"Retrieve all release versions for a Devbox, ordered by creation time descending.","parameters":[{"name":"name","in":"path","required":true,"description":"Devbox name (format: lowercase alphanumeric with hyphens, 1–63 characters)","schema":{"type":"string","pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","minLength":1,"maxLength":63,"example":"my-python-api"}}],"responses":{"200":{"description":"Release list retrieved successfully.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Version ID"},"name":{"type":"string","description":"Version name"},"devboxName":{"type":"string","description":"Devbox name"},"createdAt":{"type":"string","description":"Creation time in YYYY-MM-DD HH:mm format"},"tag":{"type":"string","description":"Version tag"},"description":{"type":"string","description":"Version description"},"image":{"type":"string","description":"Release image address"}},"required":["id","name","devboxName","createdAt","tag","description","image"]},"description":"List of devbox versions"},"examples":{"success":{"summary":"One release","value":[{"id":"release-a1b2c3","name":"my-python-api-v1-0-0","devboxName":"my-python-api","createdAt":"2024-01-15 10:30","tag":"v1-0-0","description":"First stable release","image":"registry.cloud.sealos.io/ns-user123/my-python-api:v1-0-0"}]},"empty":{"summary":"No releases yet","value":[]}}}}},"400":{"description":"Invalid devbox name.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"]},"code":{"type":"string","enum":["INVALID_PARAMETER"]},"message":{"type":"string"},"details":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},{"type":"string"}]}},"required":["type","code","message"]}},"required":["error"]},"examples":{"invalidParam":{"summary":"Invalid name format","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid devbox name format."}}}}}}},"401":{"description":"No valid credentials provided, or credentials have expired.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"unauthorized":{"summary":"Authentication required","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"No valid credentials provided."}}}}}}},"404":{"description":"The specified Devbox does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error"},"code":{"type":"string","const":"NOT_FOUND"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"notFound":{"summary":"Devbox not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Devbox not found."}}}}}}},"500":{"description":"Failed to retrieve the release list.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"]},"code":{"type":"string","enum":["INTERNAL_ERROR"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"serverError":{"summary":"Internal error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Failed to list DevboxRelease resources."}}}}}}}}},"post":{"tags":["Mutation"],"operationId":"createDevboxRelease","summary":"Create a devbox release","description":"Snapshot the current Devbox state and trigger a container image build for the given version tag.\n\nKey points:\n- **Asynchronous** — returns `202 Accepted` immediately. The build pipeline (stop devbox → build image → restart devbox) runs in the background; poll `GET /devbox/{name}/releases` to track progress.\n- By default the Devbox is restarted after the build succeeds (`startDevboxAfterRelease: true`). Set to `false` to keep the Devbox stopped after the release.","parameters":[{"name":"name","in":"path","required":true,"description":"Devbox name (format: lowercase alphanumeric with hyphens, 1–63 characters)","schema":{"type":"string","pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","minLength":1,"maxLength":63,"example":"my-python-api"}}],"requestBody":{"description":"Release parameters.\n\n**Example — minimal (restart after build, no custom startup command):**\n```json\n{\n \"tag\": \"v1-2-0\",\n \"releaseDescription\": \"Added API improvements and bug fixes.\"\n}\n```\n\n**Example — keep Devbox stopped after release:**\n```json\n{\n \"tag\": \"v1-0-0\",\n \"releaseDescription\": \"Hotfix\",\n \"startDevboxAfterRelease\": false\n}\n```\n\n**Example — restart with autostart command:**\n```json\n{\n \"tag\": \"v1-3-0\",\n \"releaseDescription\": \"Adds startup script\",\n \"execCommand\": \"nohup /home/devbox/project/entrypoint.sh > /dev/null 2>&1 &\"\n}\n```","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"tag":{"type":"string","minLength":1,"description":"Release tag"},"releaseDescription":{"type":"string","default":"","description":"Release description"},"execCommand":{"type":"string","description":"Command to execute in the devbox after release restart (autostart)","example":"nohup /home/devbox/project/entrypoint.sh > /dev/null 2>&1 &"},"startDevboxAfterRelease":{"type":"boolean","default":true,"description":"Restart devbox automatically after the release build completes. Defaults to `true`."}},"required":["tag"]}}}},"responses":{"202":{"description":"Release accepted. The container image build pipeline has started in the background. Poll `GET /devbox/{name}/releases` to track progress.","content":{"application/json":{"schema":{"type":"object","required":["name","status"],"properties":{"name":{"type":"string","description":"Devbox name","example":"my-python-api"},"status":{"type":"string","enum":["creating"],"description":"Always `creating` — the build is running asynchronously.","example":"creating"}}},"examples":{"accepted":{"summary":"Release accepted","value":{"name":"my-python-api","status":"creating"}}}}}},"400":{"description":"Request body failed validation.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"]},"code":{"type":"string","enum":["INVALID_PARAMETER"]},"message":{"type":"string"},"details":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},{"type":"string"}]}},"required":["type","code","message"]}},"required":["error"]},"examples":{"invalidTag":{"summary":"Invalid tag format","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid request body","details":[{"field":"tag","message":"Tag must comply with DNS naming conventions."}]}}}}}}},"401":{"description":"No valid credentials provided, or credentials have expired.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"unauthorized":{"summary":"Authentication required","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"No valid credentials provided."}}}}}}},"404":{"description":"The specified Devbox does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error"},"code":{"type":"string","const":"NOT_FOUND"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"notFound":{"summary":"Devbox not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Devbox not found."}}}}}}},"409":{"description":"A release with this tag already exists for this Devbox.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error"},"code":{"type":"string","enum":["ALREADY_EXISTS"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"alreadyExists":{"summary":"Tag already exists","value":{"error":{"type":"resource_error","code":"ALREADY_EXISTS","message":"Devbox release already exists."}}}}}}},"500":{"description":"Failed to create the release.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"]},"code":{"type":"string","enum":["INTERNAL_ERROR"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"serverError":{"summary":"Internal error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Internal server error."}}}}}}}}}},"/devbox/{name}/deployments":{"get":{"tags":["Query"],"operationId":"listDevboxDeployments","summary":"List deployed applications from a devbox","description":"Retrieve all AppLaunchpad applications that were deployed from this Devbox's releases.","parameters":[{"name":"name","in":"path","required":true,"description":"Devbox name (format: lowercase alphanumeric with hyphens, 1–63 characters)","schema":{"type":"string","pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","minLength":1,"maxLength":63,"example":"my-python-api"}}],"responses":{"200":{"description":"Deployment list retrieved successfully.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Deployment or StatefulSet name"},"resourceType":{"type":"string","enum":["deployment","statefulset"],"description":"Resource type"},"tag":{"type":"string","description":"Devbox tag extracted from image name"}},"required":["name","resourceType","tag"]},"description":"List of deployed devbox releases"},"examples":{"success":{"summary":"Two deployments","value":[{"name":"my-python-api-release-abc123","resourceType":"deployment","tag":"v1-0-0"},{"name":"my-python-api-release-def456","resourceType":"statefulset","tag":"v0-9-0"}]},"empty":{"summary":"No deployments","value":[]}}}}},"400":{"description":"Invalid devbox name.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"]},"code":{"type":"string","enum":["INVALID_PARAMETER"]},"message":{"type":"string"},"details":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},{"type":"string"}]}},"required":["type","code","message"]}},"required":["error"]},"examples":{"invalidParam":{"summary":"Invalid name format","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid devbox name format."}}}}}}},"401":{"description":"No valid credentials provided, or credentials have expired.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"unauthorized":{"summary":"Authentication required","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"No valid credentials provided."}}}}}}},"404":{"description":"The specified Devbox does not exist.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error"},"code":{"type":"string","const":"NOT_FOUND"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"notFound":{"summary":"Devbox not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Devbox not found."}}}}}}},"500":{"description":"Failed to query deployments.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"]},"code":{"type":"string","enum":["INTERNAL_ERROR"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"serverError":{"summary":"Internal error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Internal server error."}}}}}}}}}},"/devbox/{name}/releases/{tag}":{"delete":{"tags":["Mutation"],"operationId":"deleteDevboxRelease","summary":"Delete a devbox release","description":"Delete a specific release version and its associated container image.\n\nKey points:\n- **Idempotent** — if the release does not exist the request still returns `204`.","parameters":[{"name":"name","in":"path","required":true,"description":"Devbox name (format: lowercase alphanumeric with hyphens, 1–63 characters)","schema":{"type":"string","pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","minLength":1,"maxLength":63,"example":"my-python-api"}},{"name":"tag","in":"path","required":true,"description":"Release version tag (format: lowercase alphanumeric with hyphens, 1–63 characters)","schema":{"type":"string","pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","minLength":1,"maxLength":63,"example":"v1-0-0"}}],"responses":{"204":{"description":"Release deleted successfully, or did not exist (idempotent)."},"400":{"description":"Invalid path parameters.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"]},"code":{"type":"string","enum":["INVALID_PARAMETER"]},"message":{"type":"string"},"details":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},{"type":"string"}]}},"required":["type","code","message"]}},"required":["error"]},"examples":{"invalidParam":{"summary":"Invalid parameter format","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid devbox name or release tag format."}}}}}}},"401":{"description":"No valid credentials provided, or credentials have expired.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"unauthorized":{"summary":"Authentication required","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"No valid credentials provided."}}}}}}},"500":{"description":"Failed to delete the release.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"]},"code":{"type":"string","enum":["INTERNAL_ERROR"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"serverError":{"summary":"Internal error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Internal server error."}}}}}}}}}},"/devbox/{name}/releases/{tag}/deploy":{"post":{"tags":["Mutation"],"operationId":"deployDevboxRelease","summary":"Deploy a release to AppLaunchpad","description":"Deploy a successfully built release version as a production application in AppLaunchpad.\n\nKey points:\n- The release must be in `Success` status before deploying.\n- Each call creates a new AppLaunchpad application; prior deployments are not replaced.","parameters":[{"name":"name","in":"path","required":true,"description":"Devbox name (format: lowercase alphanumeric with hyphens, 1–63 characters)","schema":{"type":"string","pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","minLength":1,"maxLength":63,"example":"my-python-api"}},{"name":"tag","in":"path","required":true,"description":"Release version tag (format: lowercase alphanumeric with hyphens, 1–63 characters)","schema":{"type":"string","pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","minLength":1,"maxLength":63,"example":"v1-0-0"}}],"responses":{"204":{"description":"Release deployed successfully. Application is now running in AppLaunchpad."},"400":{"description":"Invalid path parameters.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"]},"code":{"type":"string","enum":["INVALID_PARAMETER"]},"message":{"type":"string"},"details":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},{"type":"string"}]}},"required":["type","code","message"]}},"required":["error"]},"examples":{"invalidParam":{"summary":"Invalid parameter format","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid devbox name or release tag format."}}}}}}},"401":{"description":"No valid credentials provided, or credentials have expired.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"unauthorized":{"summary":"Authentication required","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"No valid credentials provided."}}}}}}},"404":{"description":"Devbox or release tag not found, or release is not in Success status.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error"},"code":{"type":"string","const":"NOT_FOUND"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"notFound":{"summary":"Release not found or not successful","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Devbox release tag v1-0-0 is not found or not successful."}}}}}}},"500":{"description":"Deployment failed.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["operation_error","internal_error"]},"code":{"type":"string","enum":["OPERATION_FAILED","INTERNAL_ERROR"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"deployError":{"summary":"AppLaunchpad error","value":{"error":{"type":"operation_error","code":"OPERATION_FAILED","message":"Failed to deploy to AppLaunchpad."}}}}}}}}}},"/devbox/{name}/monitor":{"get":{"tags":["Query"],"operationId":"getDevboxMonitor","summary":"Get devbox monitoring data","description":"Retrieve time-series CPU and memory usage metrics for a specific Devbox.","parameters":[{"name":"name","in":"path","required":true,"description":"Devbox name (format: lowercase alphanumeric with hyphens, 1–63 characters)","schema":{"type":"string","pattern":"^[a-z0-9]([-a-z0-9]*[a-z0-9])?$","minLength":1,"maxLength":63,"example":"my-python-api"}},{"name":"start","in":"query","required":false,"description":"Start of the monitoring window. Accepts a Unix timestamp in either **seconds** or **milliseconds** (values > 10¹² are automatically divided by 1000). Defaults to `end − 3 h`.","schema":{"type":"string","example":"1760510280"}},{"name":"end","in":"query","required":false,"description":"End of the monitoring window. Accepts a Unix timestamp in either **seconds** or **milliseconds** (values > 10¹² are automatically divided by 1000). Defaults to the current server time.","schema":{"type":"string","example":"1760513880"}},{"name":"step","in":"query","required":false,"description":"Sampling interval (e.g. `1m`, `5m`, `1h`).","schema":{"type":"string","default":"2m","example":"2m"}}],"responses":{"200":{"description":"Monitoring data retrieved successfully.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"timestamp":{"type":"number","description":"Unix timestamp in seconds","example":1760510280},"readableTime":{"type":"string","description":"Formatted timestamp in YYYY/MM/DD HH:mm","example":"2025/10/15 14:38"},"cpu":{"type":"number","description":"CPU utilisation percentage","example":1.08},"memory":{"type":"number","description":"Memory utilisation percentage","example":10.32}},"required":["timestamp","readableTime","cpu","memory"]},"description":"Chronological sequence of devbox CPU and memory usage"},"examples":{"success":{"summary":"CPU and memory metrics","value":[{"timestamp":1760510280,"readableTime":"2025/10/15 14:38","cpu":1.08,"memory":10.32},{"timestamp":1760510340,"readableTime":"2025/10/15 14:39","cpu":1.18,"memory":10.37}]}}}}},"400":{"description":"Invalid devbox name or query parameters.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"]},"code":{"type":"string","enum":["INVALID_PARAMETER","INVALID_VALUE"]},"message":{"type":"string"},"details":{"anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"message":{"type":"string"}},"required":["field","message"]}},{"type":"string"}]}},"required":["type","code","message"]}},"required":["error"]},"examples":{"invalidParam":{"summary":"Invalid devbox name","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Invalid devbox name format."}}},"invalidTimeRange":{"summary":"start is not earlier than end","value":{"error":{"type":"validation_error","code":"INVALID_VALUE","message":"Start timestamp must be earlier than end timestamp."}}}}}}},"401":{"description":"No valid credentials provided, or credentials have expired.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"unauthorized":{"summary":"Authentication required","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"No valid credentials provided."}}}}}}},"500":{"description":"Failed to fetch monitoring data.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"]},"code":{"type":"string","enum":["INTERNAL_ERROR"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"serverError":{"summary":"Internal error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Failed to fetch devbox monitor data."}}}}}}}}}},"/devbox/templates":{"get":{"tags":["Query"],"operationId":"listDevboxTemplates","summary":"List available devbox templates","description":"Retrieve available runtime environments and their default port/command configurations for creating Devboxes.","responses":{"200":{"description":"Template list retrieved successfully.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"runtime":{"type":"string","description":"Runtime name (from iconId or repository uid)"},"config":{"type":"object","properties":{"appPorts":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"port":{"type":"number"},"protocol":{"type":"string"}},"required":["name","port","protocol"]}},"ports":{"type":"array","items":{"type":"object","properties":{"containerPort":{"type":"number"},"name":{"type":"string"},"protocol":{"type":"string"}},"required":["containerPort","name","protocol"]}},"releaseArgs":{"type":"array","items":{"type":"string"}},"releaseCommand":{"type":"array","items":{"type":"string"}},"user":{"type":"string"},"workingDir":{"type":"string"}},"description":"Parsed template configuration"}},"required":["runtime","config"]}},"examples":{"success":{"summary":"Available runtimes","value":[{"runtime":"python","config":{"appPorts":[{"name":"devbox-app-port","port":8080,"protocol":"TCP"}],"ports":[{"containerPort":22,"name":"devbox-ssh-port","protocol":"TCP"}],"releaseArgs":["/home/devbox/project/entrypoint.sh prod"],"releaseCommand":["/bin/bash","-c"],"user":"devbox","workingDir":"/home/devbox/project"}},{"runtime":"go","config":{"appPorts":[{"name":"devbox-app-port","port":8080,"protocol":"TCP"}],"ports":[{"containerPort":22,"name":"devbox-ssh-port","protocol":"TCP"}],"releaseArgs":["/home/devbox/project/entrypoint.sh prod"],"releaseCommand":["/bin/bash","-c"],"user":"devbox","workingDir":"/home/devbox/project"}}]}}}}},"401":{"description":"No valid credentials provided, or credentials have expired.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED"},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"unauthorized":{"summary":"Authentication required","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"No valid credentials provided."}}}}}}},"500":{"description":"Failed to retrieve the template list.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"]},"code":{"type":"string","enum":["INTERNAL_ERROR"]},"message":{"type":"string"},"details":{"type":"string"}},"required":["type","code","message"]}},"required":["error"]},"examples":{"serverError":{"summary":"Internal error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Failed to query template repositories from database."}}}}}}}}}}},"components":{"securitySchemes":{"kubeconfigAuth":{"type":"apiKey","in":"header","name":"Authorization","description":"URL-encoded kubeconfig YAML. Encode with `encodeURIComponent(kubeconfigYaml)` before setting the header value. Obtain your kubeconfig from the Sealos console."},"jwtAuth":{"type":"apiKey","in":"header","name":"Authorization-Bearer","description":"JWT token for authentication. Header: `Authorization-Bearer: `"}}}} \ No newline at end of file diff --git a/skills/devbox/references/operations.md b/skills/devbox/references/operations.md new file mode 100644 index 0000000..b1f2abe --- /dev/null +++ b/skills/devbox/references/operations.md @@ -0,0 +1,52 @@ +# Operations Reference + +## Monitor + +**3a.** If no name given → List, then `AskUserQuestion` to pick which devbox. + +**3b.** `AskUserQuestion` (header: "Time Range", question: "Monitoring period?"): +- "Last 1 hour" +- "Last 3 hours (default)" +- "Last 24 hours" +- "Custom range" + +For "Custom range": ask for start time, end time, and step interval. + +**3c.** Run `node scripts/sealos-devbox.mjs monitor {name} [start] [end] [step]`. + +**3d.** Display metrics as a table: + +``` +Time CPU % Memory % +14:38 1.08 10.32 +14:40 1.18 10.37 +14:42 1.25 10.41 +``` + +Highlight high utilization (>80%) with a warning. + +--- + +## Autostart + +**3a.** If no name given → List, then `AskUserQuestion` to pick which devbox. + +**3b.** Run `node scripts/sealos-devbox.mjs get {name}` to show current state and runtime. + +**3c.** Suggest a startup command based on the runtime: + +| Runtime | Suggested command | +|---------|-------------------| +| node.js, next.js, express.js, react, vue, etc. | `npm start` or `npm run dev` | +| python, django, flask | `python manage.py runserver` or `flask run` | +| go, gin, echo, chi, iris | `go run .` | +| rust, rocket | `cargo run` | +| java, quarkus, vert.x | `mvn quarkus:dev` or `java -jar app.jar` | + +**3d.** `AskUserQuestion` (header: "Autostart", question: "Startup command?"): +- Suggested command from table above +- "No command (just enable autostart)" +- "Custom command" + +**3e.** Run `node scripts/sealos-devbox.mjs autostart {name} '{"execCommand":"..."}'` +or `node scripts/sealos-devbox.mjs autostart {name}` for default behavior. diff --git a/skills/devbox/scripts/sealos-auth.mjs b/skills/devbox/scripts/sealos-auth.mjs new file mode 100644 index 0000000..aa25cf5 --- /dev/null +++ b/skills/devbox/scripts/sealos-auth.mjs @@ -0,0 +1,329 @@ +#!/usr/bin/env node + +/** + * Sealos Cloud Authentication — OAuth2 Device Grant Flow (RFC 8628) + * + * Usage: + * node sealos-auth.mjs check # Check if already authenticated + * node sealos-auth.mjs login [region] # Start device grant login flow + * node sealos-auth.mjs info # Show current auth info + * + * Environment variables: + * SEALOS_REGION — Sealos Cloud region URL (default from config.json) + * + * Flow: + * 1. POST /api/auth/oauth2/device → { device_code, user_code, verification_uri_complete } + * 2. User opens verification_uri_complete in browser to authorize + * 3. Script polls /api/auth/oauth2/token until approved + * 4. Receives access_token → exchanges for kubeconfig → saves to ~/.sealos/kubeconfig + */ + +import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs' +import { execSync } from 'child_process' +import { homedir, platform } from 'os' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' + +// ── Paths ──────────────────────────────────────────────── +const __dirname = dirname(fileURLToPath(import.meta.url)) +const SEALOS_DIR = join(homedir(), '.sealos') +const KC_PATH = join(SEALOS_DIR, 'kubeconfig') +const AUTH_PATH = join(SEALOS_DIR, 'auth.json') + +// ── Skill constants (from config.json) ─────────────────── +const CONFIG_PATH = join(__dirname, '..', 'config.json') +const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')) +const CLIENT_ID = config.client_id +const DEFAULT_REGION = config.default_region + +// ── Check ────────────────────────────────────────────── + +function check () { + if (!existsSync(KC_PATH)) { + return { authenticated: false } + } + + try { + const kc = readFileSync(KC_PATH, 'utf-8') + if (kc.includes('server:') && (kc.includes('token:') || kc.includes('client-certificate'))) { + const auth = existsSync(AUTH_PATH) ? JSON.parse(readFileSync(AUTH_PATH, 'utf-8')) : {} + return { + authenticated: true, + kubeconfig_path: KC_PATH, + region: auth.region || 'unknown' + } + } + } catch { } + + return { authenticated: false } +} + +// ── Device Grant Flow ────────────────────────────────── + +/** + * Step 1: Request device authorization + * POST /api/auth/oauth2/device + * Body: { client_id } + * Response: { device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval } + */ +async function requestDeviceAuthorization (region) { + const res = await fetch(`${region}/api/auth/oauth2/device`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + client_id: CLIENT_ID, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code' + }) + }) + + if (!res.ok) { + const body = await res.text().catch(() => '') + throw new Error(`Device authorization request failed (${res.status}): ${body || res.statusText}`) + } + + return res.json() +} + +/** + * Step 2: Poll for token + * POST /api/auth/oauth2/token + * Body: { client_id, grant_type, device_code } + * + * Possible responses: + * - 200: { access_token, token_type, ... } → success + * - 400: { error: "authorization_pending" } → keep polling + * - 400: { error: "slow_down" } → increase interval by 5s + * - 400: { error: "access_denied" } → user denied + * - 400: { error: "expired_token" } → device code expired + */ +async function pollForToken (region, deviceCode, interval, expiresIn) { + // Hard cap at 10 minutes regardless of server's expires_in + const maxWait = Math.min(expiresIn, 600) * 1000 + const deadline = Date.now() + maxWait + let pollInterval = interval * 1000 + let lastLoggedMinute = -1 + + while (Date.now() < deadline) { + await sleep(pollInterval) + + // Log remaining time every minute + const remaining = Math.ceil((deadline - Date.now()) / 60000) + if (remaining !== lastLoggedMinute && remaining > 0) { + lastLoggedMinute = remaining + process.stderr.write(` Waiting for authorization... (${remaining} min remaining)\n`) + } + + const res = await fetch(`${region}/api/auth/oauth2/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + client_id: CLIENT_ID, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + device_code: deviceCode + }) + }) + + if (res.ok) { + // Success — got the token + return res.json() + } + + const body = await res.json().catch(() => ({})) + + switch (body.error) { + case 'authorization_pending': + // User hasn't authorized yet, keep polling + break + + case 'slow_down': + // Increase polling interval by 5 seconds (RFC 8628 §3.5) + pollInterval += 5000 + break + + case 'access_denied': + throw new Error('Authorization denied by user') + + case 'expired_token': + throw new Error('Device code expired. Please run login again.') + + default: + throw new Error(`Token request failed: ${body.error || res.statusText}`) + } + } + + throw new Error('Authorization timed out (10 minutes). Please run login again.') +} + +/** + * Step 3: Exchange access token for kubeconfig + */ +async function exchangeForKubeconfig (region, accessToken) { + const res = await fetch(`${region}/api/auth/getDefaultKubeconfig`, { + method: 'POST', + headers: { + Authorization: accessToken, + 'Content-Type': 'application/json' + } + }) + + if (!res.ok) { + const body = await res.text().catch(() => '') + throw new Error(`Kubeconfig exchange failed (${res.status}): ${body || res.statusText}`) + } + + return res.json() +} + +function sleep (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +// ── Login (Device Grant Flow) ────────────────────────── + +async function login (region = DEFAULT_REGION) { + region = region.replace(/\/+$/, '') + + // Step 1: Request device authorization + const deviceAuth = await requestDeviceAuthorization(region) + + const { + device_code: deviceCode, + user_code: userCode, + verification_uri: verificationUri, + verification_uri_complete: verificationUriComplete, + expires_in: expiresIn, + interval = 5 + } = deviceAuth + + // Output device authorization info for the AI tool / user to display + const authPrompt = { + action: 'user_authorization_required', + user_code: userCode, + verification_uri: verificationUri, + verification_uri_complete: verificationUriComplete, + expires_in: expiresIn, + message: `Please open the following URL in your browser to authorize:\n\n ${verificationUriComplete || verificationUri}\n\nAuthorization code: ${userCode}\nExpires in: ${Math.floor(expiresIn / 60)} minutes` + } + + // Print the authorization prompt to stderr so it's visible to the user + // while stdout is reserved for JSON output + process.stderr.write('\n' + authPrompt.message + '\n\nWaiting for authorization...\n') + + // Auto-open browser + const url = verificationUriComplete || verificationUri + try { + const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open' + execSync(`${cmd} "${url}"`, { stdio: 'ignore' }) + process.stderr.write('Browser opened automatically.\n') + } catch { + process.stderr.write('Could not open browser automatically. Please open the URL manually.\n') + } + + // Step 2: Poll for token + const tokenResponse = await pollForToken(region, deviceCode, interval, expiresIn) + const accessToken = tokenResponse.access_token + + if (!accessToken) { + throw new Error('Token response missing access_token') + } + + process.stderr.write('Authorization received. Exchanging for kubeconfig...\n') + + // Step 3: Exchange access token for kubeconfig + const kcData = await exchangeForKubeconfig(region, accessToken) + const kubeconfig = kcData.data?.kubeconfig + + if (!kubeconfig) { + throw new Error('API response missing data.kubeconfig field') + } + + // Save kubeconfig to ~/.sealos/kubeconfig (Sealos-specific, avoids conflict with ~/.kube/config) + mkdirSync(SEALOS_DIR, { recursive: true }) + writeFileSync(KC_PATH, kubeconfig, { mode: 0o600 }) + writeFileSync(AUTH_PATH, JSON.stringify({ + region, + authenticated_at: new Date().toISOString(), + auth_method: 'oauth2_device_grant' + }, null, 2), { mode: 0o600 }) + + process.stderr.write('Authentication successful!\n') + + return { kubeconfig_path: KC_PATH, region } +} + +// ── Info ─────────────────────────────────────────────── + +function info () { + const status = check() + if (!status.authenticated) { + return { authenticated: false, message: 'Not authenticated. Run: node sealos-auth.mjs login' } + } + + const auth = existsSync(AUTH_PATH) ? JSON.parse(readFileSync(AUTH_PATH, 'utf-8')) : {} + return { + authenticated: true, + kubeconfig_path: KC_PATH, + region: auth.region || 'unknown', + auth_method: auth.auth_method || 'unknown', + authenticated_at: auth.authenticated_at || 'unknown' + } +} + +// ── CLI ──────────────────────────────────────────────── + +const [, , cmd, ...rawArgs] = process.argv + +// --insecure flag: skip TLS certificate verification (for self-signed certs) +const insecure = rawArgs.includes('--insecure') +const args = rawArgs.filter(a => a !== '--insecure') + +if (insecure) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' +} + +try { + switch (cmd) { + case 'check': { + console.log(JSON.stringify(check())) + break + } + + case 'login': { + const region = args[0] || process.env.SEALOS_REGION || DEFAULT_REGION + const result = await login(region) + console.log(JSON.stringify(result)) + break + } + + case 'info': { + console.log(JSON.stringify(info(), null, 2)) + break + } + + default: { + console.log(`Sealos Cloud Auth — OAuth2 Device Grant Flow + +Usage: + node sealos-auth.mjs check Check authentication status + node sealos-auth.mjs login [region] Start OAuth2 device login flow + node sealos-auth.mjs login --insecure Skip TLS verification (self-signed cert) + node sealos-auth.mjs info Show current auth details + +Environment: + SEALOS_REGION Region URL (default: ${DEFAULT_REGION}) + +Flow: + 1. Run "login" → opens browser for authorization + 2. Approve in browser → script receives token automatically + 3. Token exchanged for kubeconfig → saved to ~/.sealos/kubeconfig`) + } + } +} catch (err) { + // If TLS error and not using --insecure, hint the user + if (!insecure && (err.message.includes('fetch failed') || err.message.includes('self-signed') || err.message.includes('CERT'))) { + console.error(JSON.stringify({ error: err.message, hint: 'Try adding --insecure for self-signed certificates' })) + } else { + console.error(JSON.stringify({ error: err.message })) + } + process.exit(1) +} diff --git a/skills/devbox/scripts/sealos-devbox.mjs b/skills/devbox/scripts/sealos-devbox.mjs new file mode 100644 index 0000000..75dd2d8 --- /dev/null +++ b/skills/devbox/scripts/sealos-devbox.mjs @@ -0,0 +1,501 @@ +#!/usr/bin/env node +// Sealos Devbox CLI - single entry point for all devbox operations. +// Zero external dependencies. Requires Node.js (guaranteed by Claude Code). +// +// Usage: +// node sealos-devbox.mjs [args...] +// +// Config resolution: +// ~/.sealos/auth.json region field → derives API URL automatically +// Kubeconfig is always read from ~/.sealos/kubeconfig +// +// Commands: +// templates List available runtimes (no auth needed) +// list List all devboxes +// get Get devbox details + SSH info +// create Create devbox (saves SSH key automatically) +// create-wait Create + poll until running (timeout 2min) +// update Update quota/ports +// delete Delete devbox +// start Start stopped devbox +// pause Pause running devbox +// shutdown Shutdown devbox +// restart Restart devbox +// autostart [json] Configure autostart command +// list-releases List releases +// create-release Create release (tag, description, etc.) +// delete-release Delete release +// deploy Deploy release to AppLaunchpad +// list-deployments List deployments +// monitor [start] [end] [step] Get CPU/memory metrics + +import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'node:fs'; +import { request as httpsRequest } from 'node:https'; +import { request as httpRequest } from 'node:http'; +import { resolve } from 'node:path'; +import { homedir } from 'node:os'; + +const KC_PATH = resolve(homedir(), '.sealos/kubeconfig'); +const AUTH_PATH = resolve(homedir(), '.sealos/auth.json'); +const KEYS_DIR = resolve(homedir(), '.config/sealos-devbox/keys'); +const API_PATH = '/api/v2alpha'; // API version — update here if the version changes + +// --- config --- + +function loadConfig() { + // Derive API URL from auth.json region + if (!existsSync(AUTH_PATH)) { + throw new Error('Not authenticated. Run: node sealos-auth.mjs login'); + } + + let auth; + try { + auth = JSON.parse(readFileSync(AUTH_PATH, 'utf-8')); + } catch { + throw new Error('Invalid auth.json. Run: node sealos-auth.mjs login'); + } + + if (!auth.region) { + throw new Error('No region in auth.json. Run: node sealos-auth.mjs login'); + } + + // Derive API URL: region "https://gzg.sealos.run" → "https://devbox.gzg.sealos.run/api/v2alpha" + const regionUrl = new URL(auth.region); + const apiUrl = `https://devbox.${regionUrl.hostname}${API_PATH}`; + + if (!existsSync(KC_PATH)) { + throw new Error(`Kubeconfig not found at ${KC_PATH}. Run: node sealos-auth.mjs login`); + } + + return { apiUrl, regionUrl: regionUrl.origin, kubeconfigPath: KC_PATH }; +} + +// --- SSH key management --- + +function savePrivateKey(name, base64Key) { + mkdirSync(KEYS_DIR, { recursive: true }); + const keyPath = resolve(KEYS_DIR, `${name}.pem`); + const keyData = Buffer.from(base64Key, 'base64').toString('utf-8'); + writeFileSync(keyPath, keyData, { mode: 0o600 }); + try { chmodSync(keyPath, 0o600); } catch { /* best effort */ } + return keyPath; +} + +// --- auth --- + +function getEncodedKubeconfig(path) { + if (!existsSync(path)) { + throw new Error(`Kubeconfig not found at ${path}`); + } + return encodeURIComponent(readFileSync(path, 'utf-8')); +} + +// --- HTTP --- + +function apiCall(method, endpoint, { apiUrl, auth, body, timeout = 30000 } = {}) { + return new Promise((resolve, reject) => { + const url = new URL(apiUrl + endpoint); + const isHttps = url.protocol === 'https:'; + const reqFn = isHttps ? httpsRequest : httpRequest; + + const headers = {}; + if (auth) headers['Authorization'] = auth; + if (body) headers['Content-Type'] = 'application/json'; + + const bodyStr = body ? JSON.stringify(body) : null; + if (bodyStr) headers['Content-Length'] = Buffer.byteLength(bodyStr); + + const opts = { + hostname: url.hostname, + port: url.port || (isHttps ? 443 : 80), + path: url.pathname + url.search, + method, + headers, + timeout, + rejectUnauthorized: false, // Sealos clusters may use self-signed certificates + }; + + const req = reqFn(opts, (res) => { + const chunks = []; + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => { + const rawBody = Buffer.concat(chunks).toString(); + let parsed = null; + try { parsed = JSON.parse(rawBody); } catch { parsed = rawBody || null; } + resolve({ status: res.statusCode, body: parsed }); + }); + }); + + req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); }); + req.on('error', reject); + if (bodyStr) req.write(bodyStr); + req.end(); + }); +} + +// --- helpers --- + +function requireName(args) { + if (!args[0]) throw new Error('Devbox name required'); + return args[0]; +} + +function output(data) { + console.log(JSON.stringify(data, null, 2)); +} + +function sleep(ms) { + return new Promise((r) => setTimeout(r, ms)); +} + +// --- individual commands --- + +async function listTemplates(cfg) { + const res = await apiCall('GET', '/devbox/templates', { apiUrl: cfg.apiUrl }); + if (res.status !== 200) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +async function list(cfg) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('GET', '/devbox', { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 200) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +async function get(cfg, name) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('GET', `/devbox/${name}`, { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 200) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +function validateCreateBody(body) { + const errors = []; + if (!body.name) { + errors.push('name is required'); + } else { + if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(body.name)) errors.push('name must match ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'); + if (body.name.length > 63) errors.push('name must be at most 63 characters'); + } + if (!body.runtime) errors.push('runtime is required'); + if (body.quota) { + const q = body.quota; + if (q.cpu !== undefined && (typeof q.cpu !== 'number' || q.cpu < 0.1 || q.cpu > 32)) errors.push('cpu must be 0.1-32'); + if (q.memory !== undefined && (typeof q.memory !== 'number' || q.memory < 0.1 || q.memory > 32)) errors.push('memory must be 0.1-32 GB'); + } + if (body.ports) { + if (!Array.isArray(body.ports)) { + errors.push('ports must be an array'); + } else { + for (const p of body.ports) { + if (p.number !== undefined && (typeof p.number !== 'number' || p.number < 1 || p.number > 65535)) errors.push(`port number must be 1-65535, got ${p.number}`); + if (p.protocol && !['http', 'grpc', 'ws'].includes(p.protocol)) errors.push(`port protocol must be http, grpc, or ws, got ${p.protocol}`); + } + } + } + if (errors.length) throw new Error('Validation failed: ' + errors.join('; ')); +} + +function validateUpdateBody(body) { + const errors = []; + if (body.quota) { + const q = body.quota; + if (q.cpu !== undefined && (typeof q.cpu !== 'number' || q.cpu < 0.1 || q.cpu > 32)) errors.push('cpu must be 0.1-32'); + if (q.memory !== undefined && (typeof q.memory !== 'number' || q.memory < 0.1 || q.memory > 32)) errors.push('memory must be 0.1-32 GB'); + } + if (body.ports) { + if (!Array.isArray(body.ports)) { + errors.push('ports must be an array'); + } else { + for (const p of body.ports) { + if (p.number !== undefined && (typeof p.number !== 'number' || p.number < 1 || p.number > 65535)) errors.push(`port number must be 1-65535, got ${p.number}`); + if (p.protocol && !['http', 'grpc', 'ws'].includes(p.protocol)) errors.push(`port protocol must be http, grpc, or ws, got ${p.protocol}`); + } + } + } + if (errors.length) throw new Error('Validation failed: ' + errors.join('; ')); +} + +async function create(cfg, jsonBody) { + const body = typeof jsonBody === 'string' ? JSON.parse(jsonBody) : jsonBody; + validateCreateBody(body); + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('POST', '/devbox', { apiUrl: cfg.apiUrl, auth, body }); + if (res.status !== 201) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + + // Auto-save SSH private key + if (res.body && res.body.base64PrivateKey) { + const keyPath = savePrivateKey(body.name || res.body.name, res.body.base64PrivateKey); + res.body.savedKeyPath = keyPath; + } + + return res.body; +} + +async function update(cfg, name, jsonBody) { + const body = typeof jsonBody === 'string' ? JSON.parse(jsonBody) : jsonBody; + validateUpdateBody(body); + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('PATCH', `/devbox/${name}`, { apiUrl: cfg.apiUrl, auth, body }); + if (res.status !== 204) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + // Re-fetch to return updated state + try { + const updated = await get(cfg, name); + return { success: true, message: 'Devbox update initiated', devbox: updated }; + } catch { + return { success: true, message: 'Devbox update initiated' }; + } +} + +async function del(cfg, name) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('DELETE', `/devbox/${name}`, { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 204) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return { success: true, message: `Devbox '${name}' deleted` }; +} + +async function action(cfg, name, actionName) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const bodyNeeded = ['pause', 'shutdown', 'restart'].includes(actionName); + const opts = { apiUrl: cfg.apiUrl, auth }; + if (bodyNeeded) opts.body = {}; + const res = await apiCall('POST', `/devbox/${name}/${actionName}`, opts); + if (res.status !== 204) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return { success: true, message: `Action '${actionName}' on '${name}' completed` }; +} + +async function autostart(cfg, name, jsonBody) { + const body = jsonBody ? (typeof jsonBody === 'string' ? JSON.parse(jsonBody) : jsonBody) : {}; + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('POST', `/devbox/${name}/autostart`, { apiUrl: cfg.apiUrl, auth, body }); + if (res.status !== 204) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return { success: true, message: `Autostart configured for '${name}'` }; +} + +// --- release commands --- + +async function listReleases(cfg, name) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('GET', `/devbox/${name}/releases`, { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 200) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +async function createRelease(cfg, name, jsonBody) { + const body = typeof jsonBody === 'string' ? JSON.parse(jsonBody) : jsonBody; + if (!body.tag) throw new Error('Validation failed: tag is required'); + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('POST', `/devbox/${name}/releases`, { apiUrl: cfg.apiUrl, auth, body }); + if (res.status !== 202) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body || { name, tag: body.tag, status: 'creating' }; +} + +async function deleteRelease(cfg, name, tag) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('DELETE', `/devbox/${name}/releases/${encodeURIComponent(tag)}`, { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 204) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return { success: true, message: `Release '${tag}' deleted from '${name}'` }; +} + +// --- deployment commands --- + +async function listDeployments(cfg, name) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('GET', `/devbox/${name}/deployments`, { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 200) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +async function deploy(cfg, name, tag) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('POST', `/devbox/${name}/releases/${encodeURIComponent(tag)}/deploy`, { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 204) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return { success: true, message: `Release '${tag}' of '${name}' deployed to AppLaunchpad` }; +} + +// --- monitor command --- + +async function monitor(cfg, name, start, end, step) { + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + let query = ''; + const params = []; + if (start) params.push(`start=${encodeURIComponent(start)}`); + if (end) params.push(`end=${encodeURIComponent(end)}`); + if (step) params.push(`step=${encodeURIComponent(step)}`); + if (params.length) query = '?' + params.join('&'); + const res = await apiCall('GET', `/devbox/${name}/monitor${query}`, { apiUrl: cfg.apiUrl, auth }); + if (res.status !== 200) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +// --- batch commands --- + +async function createWait(cfg, jsonBody) { + // 1. Create + const body = typeof jsonBody === 'string' ? JSON.parse(jsonBody) : jsonBody; + const createResult = await create(cfg, body); + const name = body.name || createResult.name; + + // 2. Poll every 5s until running (max 2 min) + const timeout = 120000; + const interval = 5000; + const start = Date.now(); + + let lastStatus = 'pending'; + let consecutiveErrors = 0; + while (Date.now() - start < timeout) { + await sleep(interval); + try { + const info = await get(cfg, name); + consecutiveErrors = 0; + lastStatus = info.status; + process.stderr.write(`Status: ${lastStatus}\n`); + if (lastStatus.toLowerCase() === 'running') { + // Merge SSH key info from create response + if (createResult.savedKeyPath) info.savedKeyPath = createResult.savedKeyPath; + if (createResult.base64PrivateKey) info.base64PrivateKey = createResult.base64PrivateKey; + return info; + } + if (lastStatus.toLowerCase() === 'error' || lastStatus.toLowerCase() === 'failed') { + throw new Error(`Devbox creation failed. Status: ${lastStatus}`); + } + } catch (e) { + consecutiveErrors++; + if (consecutiveErrors >= 5 || Date.now() - start > timeout) throw e; + } + } + + // Timeout - return last known state + try { + const info = await get(cfg, name); + if (createResult.savedKeyPath) info.savedKeyPath = createResult.savedKeyPath; + return { ...info, consoleUrl: cfg.regionUrl, warning: `Timed out after 2 minutes. Last status: ${info.status}` }; + } catch { + return { name, status: lastStatus, consoleUrl: cfg.regionUrl, warning: 'Timed out after 2 minutes' }; + } +} + +// --- main --- + +async function main() { + const [cmd, ...args] = process.argv.slice(2); + + if (!cmd) { + console.error('ERROR: Command required.'); + console.error('Commands: templates|list|get|create|create-wait|update|delete|start|pause|shutdown|restart|autostart|list-releases|create-release|delete-release|deploy|list-deployments|monitor'); + process.exit(1); + } + + try { + const cfg = loadConfig(); + let result; + + switch (cmd) { + case 'templates': { + result = await listTemplates(cfg); + break; + } + + case 'list': { + result = await list(cfg); + break; + } + + case 'get': { + const name = requireName(args); + result = await get(cfg, name); + break; + } + + case 'create': { + if (!args[0]) throw new Error('JSON body required'); + result = await create(cfg, args[0]); + break; + } + + case 'create-wait': { + if (!args[0]) throw new Error('JSON body required'); + result = await createWait(cfg, args[0]); + break; + } + + case 'update': { + const name = requireName(args); + if (!args[1]) throw new Error('JSON body required'); + result = await update(cfg, name, args[1]); + break; + } + + case 'delete': { + const name = requireName(args); + result = await del(cfg, name); + break; + } + + case 'start': + case 'pause': + case 'shutdown': + case 'restart': { + const name = requireName(args); + result = await action(cfg, name, cmd); + break; + } + + case 'autostart': { + const name = requireName(args); + result = await autostart(cfg, name, args[1]); + break; + } + + case 'list-releases': { + const name = requireName(args); + result = await listReleases(cfg, name); + break; + } + + case 'create-release': { + const name = requireName(args); + if (!args[1]) throw new Error('JSON body required'); + result = await createRelease(cfg, name, args[1]); + break; + } + + case 'delete-release': { + const name = requireName(args); + if (!args[1]) throw new Error('Release tag required'); + result = await deleteRelease(cfg, name, args[1]); + break; + } + + case 'deploy': { + const name = requireName(args); + if (!args[1]) throw new Error('Release tag required'); + result = await deploy(cfg, name, args[1]); + break; + } + + case 'list-deployments': { + const name = requireName(args); + result = await listDeployments(cfg, name); + break; + } + + case 'monitor': { + const name = requireName(args); + result = await monitor(cfg, name, args[1], args[2], args[3]); + break; + } + + default: + throw new Error(`Unknown command '${cmd}'. Commands: templates|list|get|create|create-wait|update|delete|start|pause|shutdown|restart|autostart|list-releases|create-release|delete-release|deploy|list-deployments|monitor`); + } + + if (result !== undefined) output(result); + } catch (err) { + console.error(`ERROR: ${err.message}`); + process.exit(1); + } +} + +main(); diff --git a/skills/template/README.md b/skills/template/README.md new file mode 100644 index 0000000..f31c7c1 --- /dev/null +++ b/skills/template/README.md @@ -0,0 +1,73 @@ +# Sealos Template — AI Agent Skill + +Browse and deploy applications from the [Sealos](https://sealos.io) template catalog using natural language. Deploy AI tools, databases, web apps, and more — without leaving your terminal. + +## Quick Start + +### Prerequisites + +- [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Gemini CLI](https://github.com/google-gemini/gemini-cli), or [Codex](https://github.com/openai/codex) installed +- Node.js (bundled with Claude Code; required for scripts) + +### Install + +Use the Seakills installer: + +```bash +curl -fsSL https://seakills.com/install.sh | bash +``` + +Or copy the `sealos-template` skill into your agent's skills directory manually. + +### Usage + +Ask your AI agent in natural language, or run `/sealos-template`: + +``` +> What apps can I deploy on Sealos? +> Show me AI templates +> Deploy Perplexica +> I need a search engine +> Self-host NocoDB on Sealos +> Deploy this YAML file +``` + +The agent walks you through browsing, configuration, and deployment interactively. + +## Features + +- **Template catalog** — browse available applications by category (AI, database, tools, etc.) +- **Smart browsing** — templates sorted by popularity, grouped by category +- **Guided deployment** — collects required args interactively, shows resource requirements +- **Raw YAML deploy** — deploy custom Sealos Template CRDs from project files with dry-run preview +- **Multi-language** — supports English and Chinese (`--language=zh`) +- **Multi-region** — supports multiple Sealos Cloud regions (gzg, bja, hzh) + +## Authentication + +Browsing templates is public — no authentication needed. First-time users can explore the full catalog without logging in. + +On first deploy, the agent starts an OAuth2 device grant login flow via `sealos-auth.mjs`: +1. Opens your browser for authorization +2. Polls until you approve (up to 10 minutes) +3. Exchanges the token for a kubeconfig +4. Saves credentials to `~/.sealos/kubeconfig` and `~/.sealos/auth.json` + +No manual kubeconfig download required. The API URL is auto-derived from your selected region. + +## Scripts + +| Script | Purpose | +|--------|---------| +| `scripts/sealos-auth.mjs` | OAuth2 Device Grant login, auth check, auth info | +| `scripts/sealos-template.mjs` | Template list, get details, create instance, deploy raw YAML | + +Both scripts are zero-dependency (Node.js built-ins only) and output JSON to stdout. + +## Reference Files + +| File | Purpose | +|------|---------| +| `references/api-reference.md` | API endpoints, instance name constraints, error formats | +| `references/defaults.md` | Display rules, arg collection rules, secret masking | +| `references/openapi.json` | Complete OpenAPI spec for edge cases | diff --git a/skills/template/SKILL.md b/skills/template/SKILL.md new file mode 100644 index 0000000..09e995f --- /dev/null +++ b/skills/template/SKILL.md @@ -0,0 +1,261 @@ +--- +name: sealos-template +description: >- + Use when someone needs to deploy applications from templates on Sealos: browse + templates, view template details, deploy from catalog, or deploy custom YAML. + Triggers on "deploy perplexica", "show available templates", "deploy from template", + "list Sealos apps", "deploy this YAML", "what apps can I deploy on Sealos", + "self-host X on Sealos", "deploy open-source app", "run X on Sealos", + "what templates are available", or "I need a database/AI tool/search engine on Sealos". + Also use when deploying custom template YAML files, Sealos Template CRDs, or + any application from the Sealos template catalog. +--- + +## Interaction Principle — MANDATORY + +**NEVER output a question as plain text. ALWAYS use `AskUserQuestion` with an `options` array.** + +WHY: Plain-text questions force the user to type free-form answers instead of clicking. +Every user-facing question must go through `AskUserQuestion` with options — no exceptions. + +- Keep text output before `AskUserQuestion` to one short sentence max (status update only) +- `AskUserQuestion` always adds an implicit "Other / Type something" option automatically + +**Free-text matching:** When the user types free text instead of clicking an option, +match it to the closest option by intent. Examples: +- "show all", "browse all" → treat as "Browse all" +- "deploy it", "yes deploy" → treat as "Deploy now" +- "perplexica", "perplexica template" → treat as selecting that template + +Never re-ask the same question because the wording didn't match exactly. + +## Fixed Execution Order + +**ALWAYS follow these steps in this exact order. No skipping, no reordering.** + +``` +Step 0: Check Auth (try existing config from previous session) +Step 1: Authenticate (only if Step 0 fails) +Step 2: Route (determine which operation the user wants) +Step 3: Execute operation (follow the operation-specific steps below) +``` + +--- + +## Step 0: Check Auth & Language + +The script auto-derives its API URL from `~/.sealos/auth.json` (saved by login), +falling back to the default region in `config.json` for public operations. +Credentials are read from `~/.sealos/kubeconfig` only when deploying. + +**Language:** If the user writes in Chinese or mentions a Chinese region (gzg, bja, hzh), +use `--language=zh` for all list/get commands. Otherwise default to English. + +1. Run `node scripts/sealos-template.mjs list` (works without auth — uses default region as fallback) +2. If works → skip Step 1. Greet: "Connected to Sealos. N templates available." + Use this result in Step 3 instead of calling list again. +3. If fails (connection error) → proceed to Step 1 + +**Note:** Browsing is public (no auth needed). Auth is only validated on deploy operations. +First-time users can browse the full catalog without logging in. + +--- + +## Step 1: Authenticate + +### 1a. OAuth2 Login + +Read `config.json` (in this skill's directory) for available regions. +Ask user which region via `AskUserQuestion` with regions as options. + +Run `node scripts/sealos-auth.mjs login`. + +This command: +1. Opens the user's browser to the Sealos authorization page +2. Displays a user code and verification URL in stderr +3. Polls until the user approves (max 10 minutes) +4. Exchanges the token for a kubeconfig +5. Saves to `~/.sealos/kubeconfig` and `~/.sealos/auth.json` + +Display while waiting: +> Opening browser for Sealos login... Approve the request in your browser. + +**If TLS error**: Retry with `node scripts/sealos-auth.mjs login --insecure` + +**If other error**: +`AskUserQuestion`: +- header: "Login Failed" +- question: "Browser login failed. Try again?" +- options: ["Try again", "Cancel"] + +### 1b. Verify connection + +After login, run `node scripts/sealos-template.mjs list` to verify auth works. + +- If auth error (401) → re-run login +- If success → "Connected to Sealos. N templates available." + +Use the template list in Step 3 instead of making a separate list call. + +--- + +## Step 2: Route + +Determine the operation from user intent: + +| Intent | Operation | +|--------|-----------| +| "list/show/browse templates" | Browse | +| "what apps can I deploy" | Browse | +| "deploy X" / "I need X" | Deploy | +| "deploy this YAML" / "deploy custom template" / "deploy this Sealos Template CRD" | Deploy Raw | +| "show template details" / "what does X need" | Details | + + +If ambiguous, ask one clarifying question. + +--- + +## Step 3: Operations + +### Browse + +1. Run `node scripts/sealos-template.mjs list` command, get template array + `menuKeys` categories +2. If categories exist (`menuKeys` is non-empty), group templates by category, sort by `deployCount` within each +3. Display categorized list: name, description, deployCount +4. `AskUserQuestion`: top 4 categories as options (header: "Category", question: "Browse by category?") +5. After user picks category → show templates in that category +6. `AskUserQuestion`: top 4 templates as options (header: "Template", question: "Which template?") +7. After selection → proceed to Details + +--- + +### Details + +1. Run `node scripts/sealos-template.mjs get ` command +2. Display all fields: + - Name, description, categories, gitRepo + - Resource quota: CPU, Memory, Storage, NodePort + - Required args: name, description, type (highlight required with no default) + - Optional args: name, description, type, default value + - Deploy count +3. `AskUserQuestion`: "Deploy this template" / "Browse more" / "Done" + +--- + +### Deploy (`POST /templates/instances`) + +1. If template name not known → run Browse first +2. Run `node scripts/sealos-template.mjs get ` to fetch template details with quota and args +3. Show resource requirements (quota from API response): + ``` + Resource requirements: + CPU: 1 vCPU + Memory: 2.25 GiB + Storage: 2 GiB + NodePort: 0 + ``` + +4. **Ensure auth:** If not yet authenticated (no kubeconfig), run Step 1 now. + Browse is public but deploy requires auth. + +5. Collect required args (where `required: true` AND `default` is empty string): + - For each: `AskUserQuestion` with arg description and type + - Password/secret types (type is `"password"` or name contains KEY, SECRET, TOKEN, PASSWORD): + no pre-filled options, user must type + - Boolean types: options `["true", "false"]` + - String with obvious values: suggest up to 4 options +6. Show optional args with their defaults, `AskUserQuestion`: "Use defaults (Recommended)" / "Customize" + - If customize: iterate through optional args one by one +7. Ask instance name: `AskUserQuestion` with 2-3 suggestions (`my-{template}`, `{project}-{template}`) + - **User can type any name — passed to API exactly as typed** + - Constraint shown in question: lowercase, alphanumeric + hyphens, 1-63 chars +8. Display confirmation summary: + - Template name, instance name, all args (mask password/secret values: first 3 chars + `*****`) + - Resource requirements +9. `AskUserQuestion`: "Deploy now" / "Edit args" / "Cancel" +10. Run `node scripts/sealos-template.mjs create ''` with exact `{name, template, args}` — **no modification of any values** +11. Display API response: instance name, uid, createdAt, resources list with quotas + +--- + +### Deploy Raw (`POST /templates/raw`) + +1. `AskUserQuestion`: "From a file in my project" / "I'll provide it" +2. If file → ask path, read file content as YAML string +3. `AskUserQuestion`: "Dry-run first (Recommended)" / "Deploy directly" (maps to `dryRun: true/false`) +4. If template YAML has required args without defaults → collect via `AskUserQuestion` +5. Build the JSON body: `{yaml, args, dryRun}` — **no modification** +6. Run `node scripts/sealos-template.mjs create-raw ''` + - For large JSON bodies, write to a temp file and pass the file path instead +7. If dry-run → show preview (200 response: auto-generated name, resources), then confirm actual deploy +8. If deploy → show result (201 response) + +--- + +## Scripts + +Two entry points in `scripts/` (relative to this skill's directory): +- `sealos-auth.mjs` — OAuth2 Device Grant login (shared across all skills) +- `sealos-template.mjs` — Template browsing and deployment operations + +**The scripts are bundled with this skill — do NOT check if they exist. Just run them.** + +**Path resolution:** Scripts are in this skill's `scripts/` directory. The full path is +listed in the system environment's "Additional working directories" — use it directly. + +**Config resolution:** The script reads `~/.sealos/auth.json` (region) and `~/.sealos/kubeconfig` +(credentials) — both created by `sealos-auth.mjs login`. + +**Auth commands** (`$DIR` = this skill's `scripts/` directory): +```bash +node $DIR/sealos-auth.mjs check # Check if authenticated +node $DIR/sealos-auth.mjs login # Start OAuth2 login +node $DIR/sealos-auth.mjs login --insecure # Skip TLS verification +node $DIR/sealos-auth.mjs info # Show auth details +``` + +**Template commands:** +```bash +node $DIR/sealos-template.mjs list # list all templates (public, no auth) +node $DIR/sealos-template.mjs list --language=zh # list in Chinese +node $DIR/sealos-template.mjs get perplexica # get template details (public, no auth) +node $DIR/sealos-template.mjs get perplexica --language=zh # get details in Chinese +node $DIR/sealos-template.mjs create '{"name":"my-app","template":"perplexica","args":{"OPENAI_API_KEY":"sk-xxx"}}' +node $DIR/sealos-template.mjs create-raw '{"yaml":"apiVersion: app.sealos.io/v1\nkind: Template\n...","dryRun":true}' +node $DIR/sealos-template.mjs create-raw /path/to/body.json # read JSON body from file +``` + +## Reference Files + +- `references/api-reference.md` — API endpoints, instance name constraints, error formats. Read first. +- `references/defaults.md` — Display rules, arg collection rules, masking. Read for deploy operations. +- `references/openapi.json` — Complete OpenAPI spec. Read only for edge cases. + +## Error Handling + +**Treat each error independently.** Do NOT chain unrelated errors. + +| HTTP Status | Error Code | Action | +|-------------|------------|--------| +| 400 | INVALID_PARAMETER | Show which field is invalid from details array, re-ask | +| 400 | INVALID_VALUE | Show validation message (e.g., name format rules), re-ask | +| 401 | AUTHENTICATION_REQUIRED | Run `node scripts/sealos-auth.mjs login` to re-authenticate | +| 403 | PERMISSION_DENIED | Show details, suggest checking permissions | +| 404 | NOT_FOUND | Template doesn't exist in catalog, show list for user to pick | +| 409 | ALREADY_EXISTS | Instance name taken, ask for alternative name | +| 422 | INVALID_RESOURCE_SPEC | Show K8s rejection reason from details | +| 500 | KUBERNETES_ERROR / INTERNAL_ERROR | Show error message and details | +| 503 | SERVICE_UNAVAILABLE | Cluster unreachable, retry later | + +## Rules + +- NEVER ask a question as plain text — ALWAYS use `AskUserQuestion` with options +- NEVER ask user to manually download kubeconfig — always use `scripts/sealos-auth.mjs login` +- NEVER run `test -f` or `ls` on the skill scripts — they are always present, just run them +- NEVER write kubeconfig to `~/.kube/config` — may overwrite user's existing config +- NEVER echo kubeconfig content to output +- NEVER construct HTTP requests inline — always use `scripts/sealos-template.mjs` +- NEVER modify user-provided values (name, args) before passing to API +- Mask password/secret arg values in display only (pass real values to API) +- Instance name passed to API exactly as user provides it diff --git a/skills/template/config.json b/skills/template/config.json new file mode 100644 index 0000000..08876ee --- /dev/null +++ b/skills/template/config.json @@ -0,0 +1,9 @@ +{ + "client_id": "af993c98-d19d-4bdc-b338-79b80dc4f8bf", + "default_region": "https://gzg.sealos.run", + "regions": [ + "https://gzg.sealos.run", + "https://bja.sealos.run", + "https://hzh.sealos.run" + ] +} diff --git a/skills/template/references/api-reference.md b/skills/template/references/api-reference.md new file mode 100644 index 0000000..aee3347 --- /dev/null +++ b/skills/template/references/api-reference.md @@ -0,0 +1,230 @@ +# Sealos Template API Reference + +Base URL: `https://template.{domain}/api/v2alpha` + +> **API Version:** `v2alpha` — centralized in the script constant `API_PATH` (`scripts/sealos-template.mjs`). +> If the API version changes, update `API_PATH` in the script; the rest auto-follows. + +## TLS Note + +The script sets `rejectUnauthorized: false` for HTTPS requests because Sealos clusters +may use self-signed TLS certificates. Without this, Node.js would reject connections +to clusters that don't have publicly trusted certificates. + +## Authentication + +Browsing templates (`GET /templates`, `GET /templates/{name}`) is **public** — no auth needed. + +Deploying instances (`POST /templates/instances`, `POST /templates/raw`) requires a +URL-encoded kubeconfig YAML in the `Authorization` header: + +``` +Authorization: +``` + +## Instance Name Constraints + +Pattern: `^[a-z0-9]([-a-z0-9]*[a-z0-9])?$` +Length: 1–63 characters (Kubernetes DNS subdomain rules) + +## Endpoints + +### GET /templates — List All Templates + +**No authentication required.** + +Query parameters: +- `language` (optional, default: `"en"`) — Language code (e.g., `"en"`, `"zh"`) + +Response headers: +- `Cache-Control: public, max-age=300, s-maxage=600` +- `ETag: "template-list-{language}"` +- `X-Menu-Keys` — Top category keys, comma-separated (e.g., `"ai,database"`). Present only when categories exist. + +Response: `200 OK` → Array of template objects: + +```json +[ + { + "name": "perplexica", + "resourceType": "template", + "readme": "https://...", + "icon": "https://...", + "description": "AI-powered search engine", + "gitRepo": "https://github.com/ItzCrazyKns/Perplexica", + "category": ["ai"], + "args": { + "OPENAI_API_KEY": { + "description": "OpenAI API Key", + "type": "string", + "default": "", + "required": true + } + }, + "deployCount": 156 + } +] +``` + +**Note:** List response does NOT include `quota` — use `GET /templates/{name}` for resource requirements. + +### GET /templates/{name} — Get Template Details + +**No authentication required.** + +Path parameters: +- `name` (required) — Template name identifier + +Query parameters: +- `language` (optional, default: `"en"`) — Language code + +Response headers: +- `Cache-Control: public, max-age=300, s-maxage=600` +- `ETag: "{name}-{language}"` + +Response: `200 OK` → Full template object with `quota`: + +```json +{ + "name": "perplexica", + "resourceType": "template", + "readme": "https://...", + "icon": "https://...", + "description": "AI-powered search engine", + "gitRepo": "https://github.com/ItzCrazyKns/Perplexica", + "category": ["ai"], + "args": { + "OPENAI_API_KEY": { + "description": "The API Key of the OpenAI-compatible service", + "type": "string", + "default": "", + "required": true + } + }, + "deployCount": 156, + "quota": { + "cpu": 1, + "memory": 2.25, + "storage": 2, + "nodeport": 0 + } +} +``` + +### POST /templates/instances — Create Template Instance + +**Authentication required.** + +Request body: + +```json +{ + "name": "my-perplexica-instance", + "template": "perplexica", + "args": { + "OPENAI_API_KEY": "sk-xxxx", + "OPENAI_MODEL_NAME": "gpt-4o" + } +} +``` + +| Field | Required | Type | Constraint | +|-------|----------|------|------------| +| name | yes | string | DNS pattern, 1–63 chars | +| template | yes | string | Must exist in catalog | +| args | no | object | Key-value pairs; only args without default are required | + +Response: `201 Created` → + +```json +{ + "name": "my-perplexica-instance", + "uid": "778bf3c6-...", + "resourceType": "instance", + "displayName": "", + "createdAt": "2026-01-28T03:31:01Z", + "args": { "OPENAI_API_KEY": "sk-xxxx", "OPENAI_API_URL": "https://...", "OPENAI_MODEL_NAME": "gpt-4o" }, + "resources": [ + { + "name": "my-perplexica-instance-searxng", + "uid": "5bd2c77d-...", + "resourceType": "deployment", + "quota": { "cpu": 0.1, "memory": 0.25, "storage": 0, "replicas": 1 } + } + ] +} +``` + +### POST /templates/raw — Deploy from Raw YAML + +**Authentication required.** + +Request body: + +```json +{ + "yaml": "apiVersion: app.sealos.io/v1\nkind: Template\n...", + "args": { "MY_SECRET": "value" }, + "dryRun": true +} +``` + +| Field | Required | Type | Description | +|-------|----------|------|-------------| +| yaml | yes | string | Full template YAML (must start with `kind: Template`) | +| args | no | object | Override/supply `spec.inputs` fields | +| dryRun | no | boolean | Validate without creating (default: false) | + +Response `200 OK` (dry-run): + +```json +{ + "name": "myapp-abcdefgh", + "resourceType": "instance", + "dryRun": true, + "args": {}, + "resources": [...] +} +``` + +Response `201 Created` (actual deploy): + +```json +{ + "name": "myapp-abcdefgh", + "uid": "778bf3c6-...", + "resourceType": "instance", + "displayName": "", + "createdAt": "2026-01-28T03:31:01Z", + "args": {}, + "resources": [...] +} +``` + +## Error Response Format + +```json +{ + "error": { + "type": "validation_error", + "code": "INVALID_PARAMETER", + "message": "...", + "details": [...] + } +} +``` + +### Error Codes + +| HTTP | Code | Meaning | +|------|------|---------| +| 400 | INVALID_PARAMETER | Missing/invalid field — `details` has `[{field, message}]` | +| 400 | INVALID_VALUE | Value validation failed (e.g., name format, YAML structure) | +| 401 | AUTHENTICATION_REQUIRED | Missing or invalid kubeconfig | +| 403 | PERMISSION_DENIED | Insufficient K8s privileges | +| 404 | NOT_FOUND | Template doesn't exist in catalog | +| 409 | ALREADY_EXISTS | Instance name already taken | +| 422 | INVALID_RESOURCE_SPEC | K8s rejected resource spec (admission webhook, quota exceeded) | +| 500 | KUBERNETES_ERROR | K8s API error | +| 500 | INTERNAL_ERROR | Unexpected server error | +| 503 | SERVICE_UNAVAILABLE | K8s cluster unreachable | diff --git a/skills/template/references/defaults.md b/skills/template/references/defaults.md new file mode 100644 index 0000000..63adbfc --- /dev/null +++ b/skills/template/references/defaults.md @@ -0,0 +1,73 @@ +# Sealos Template Defaults & Display Rules + +## Category Display + +- Group templates by `X-Menu-Keys` response header (comma-separated category keys) +- Within each category, sort templates by `deployCount` descending (popular first) +- If no categories in response, show flat list sorted by deployCount + +## Template List View + +Show for each template in browse mode: +- Name +- Description (truncated to ~80 chars if long) +- Categories (comma-separated) +- Deploy count + +## Template Detail View + +Show all fields from `GET /templates/{name}`: +- Name, description, categories, gitRepo +- Resource quota: CPU, Memory, Storage, NodePort +- Required args: name, description, type (highlight args with `required: true` AND empty `default`) +- Optional args: name, description, type, default value +- Deploy count + +## Instance Name Suggestions + +Offer 2-3 clickable suggestions, but user can type anything: +- `my-{template}` — e.g., `my-perplexica` +- `{project}-{template}` — e.g., `myapp-perplexica` (from working directory name) + +User-typed names are passed to API exactly as provided. No transformation. + +Constraint shown in question: lowercase, alphanumeric + hyphens, 1-63 chars. + +## Arg Collection Rules + +### Required args (where `required: true` AND `default` is empty) +- Must be collected before deploy +- Password/secret types (`type: "password"` or name contains `KEY`, `SECRET`, `TOKEN`, `PASSWORD`): + **No pre-filled clickable values** — user must type the value +- Boolean types: options `["true", "false"]` +- String with obvious common values: suggest up to 4 options +- Other types: no default options, user must type + +### Optional args (where `required: false` OR `default` is non-empty) +- Show grouped summary with their defaults +- Offer "Use defaults (Recommended)" / "Customize" +- If customize: iterate one by one + +## AskUserQuestion Option Guidelines + +**Hard limit: max 4 options per `AskUserQuestion` call.** The tool auto-appends implicit +options ("Type something", "Chat about this") which consume slots. More than 4 user-provided +options will be truncated and invisible to the user. + +## Display Rules for Secrets + +- Mask password/secret arg values in display: show first 3 chars + `*****` + - Example: `sk-a*****` for `sk-abcdefghijk` +- Pass real (unmasked) values to the API + +## Resource Requirements Display + +Show quota from template detail response before deploy: + +``` +Resource requirements: + CPU: 1 vCPU + Memory: 2.25 GiB + Storage: 2 GiB + NodePort: 0 +``` diff --git a/skills/template/references/openapi.json b/skills/template/references/openapi.json new file mode 100644 index 0000000..56451f5 --- /dev/null +++ b/skills/template/references/openapi.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"Template API","version":"2.0.0-alpha","description":"This API provides endpoints for managing application templates and instances in the Sealos platform.\n\n## Authentication\n\nBrowsing templates is public. Deploying instances requires a URL-encoded kubeconfig in the `Authorization` header. Encode with `encodeURIComponent(kubeconfigYaml)` before setting the header value. Obtain your kubeconfig from the Sealos console.\n\n## Errors\n\nAll error responses use a unified format:\n\n```json\n{\n \"error\": {\n \"type\": \"validation_error\",\n \"code\": \"INVALID_PARAMETER\",\n \"message\": \"...\",\n \"details\": [...]\n }\n}\n```\n\n- `type` — high-level category (e.g. `validation_error`, `resource_error`, `internal_error`)\n- `code` — stable identifier for programmatic handling\n- `message` — human-readable explanation\n- `details` — optional extra context; shape varies by `code` (field list, string, or object)\n\n## Operations\n\n**Query** (read-only): returns `200 OK` with data in the response body.\n\n**Mutation** (write):\n- Create → `201 Created` with the created resource in the response body.\n- Update / Delete → `204 No Content` with no response body."},"servers":[{"url":"http://localhost:3000/api/v2alpha","description":"Local development"},{"url":"{baseUrl}/api/v2alpha","description":"Custom","variables":{"baseUrl":{"default":"https://template.example.com","description":"Base URL of your instance (e.g. https://template.192.168.x.x.nip.io)"}}}],"security":[{"kubeconfigAuth":[]}],"tags":[{"name":"Query","description":"Read-only operations. Success: `200 OK` with data in the response body."},{"name":"Mutation","description":"Write operations. Create: `201 Created` with the new resource. Update/Delete: `204 No Content`."}],"paths":{"/templates":{"get":{"tags":["Query"],"summary":"List all templates","description":"Returns metadata only (no resource calculation). Response headers: `Cache-Control` (public, max-age=300, s-maxage=600), `ETag`. When categories exist, top category keys are returned in `X-Menu-Keys` (comma-separated). For full details including resource requirements, use `/templates/{name}`.","operationId":"listTemplates","security":[],"parameters":[{"in":"query","name":"language","schema":{"description":"Language code for internationalization (e.g., \"en\", \"zh\"). Defaults to \"en\"","example":"en","type":"string"},"description":"Language code for internationalization (e.g., \"en\", \"zh\"). Defaults to \"en\""}],"responses":{"200":{"description":"Successfully retrieved template list","headers":{"Cache-Control":{"description":"Caching directive: public, max-age=300, s-maxage=600","schema":{"type":"string","example":"public, max-age=300, s-maxage=600"}},"ETag":{"description":"Entity tag for caching, format: \"template-list-{language}\"","schema":{"type":"string","example":"\"template-list-en\""}},"X-Menu-Keys":{"description":"Top category keys (comma-separated). Present only when categories exist.","schema":{"type":"string","example":"ai,database"}}},"content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Unique template identifier"},"resourceType":{"type":"string","const":"template","description":"Resource type, always \"template\""},"readme":{"type":"string","description":"URL to README documentation. Empty string when not available."},"icon":{"type":"string","description":"URL to template icon image. Empty string when not available."},"description":{"type":"string","description":"Brief description of the template"},"gitRepo":{"type":"string","description":"Git repository URL. Empty string when not available."},"category":{"type":"array","items":{"type":"string"},"description":"Template categories (e.g., [\"ai\", \"database\"])"},"args":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"description":{"type":"string","description":"Human-readable explanation of the argument"},"type":{"type":"string","description":"Argument type (e.g. \"string\", \"number\", \"boolean\", \"password\")"},"default":{"type":"string","description":"Default value used during deployment"},"required":{"type":"boolean","description":"Whether this argument is mandatory"}},"required":["description","type","default","required"],"additionalProperties":false,"description":"Template argument configuration"},"description":"Arguments required for template deployment"},"deployCount":{"type":"number","description":"Number of deployments (includes multiplier for display)"}},"required":["name","resourceType","readme","icon","description","gitRepo","category","args","deployCount"],"additionalProperties":false},"description":"Array of template items (returned directly without wrapper object)"},"examples":{"templateList":{"summary":"Sample template list","value":[{"name":"perplexica","resourceType":"template","readme":"https://raw.githubusercontent.com/ItzCrazyKns/Perplexica/master/README.md","icon":"https://raw.githubusercontent.com/ItzCrazyKns/Perplexica/refs/heads/master/src/app/favicon.ico","description":"AI-powered search engine","gitRepo":"https://github.com/ItzCrazyKns/Perplexica","category":["ai"],"args":{"OPENAI_API_KEY":{"description":"OpenAI API Key","type":"string","default":"","required":true}},"deployCount":156}]}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"],"description":"High-level error type for categorization"},"code":{"type":"string","enum":["INTERNAL_ERROR"],"description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"internalError":{"summary":"Failed to load templates","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Failed to load templates.","details":"File read error"}}}}}}}}}},"/templates/{name}":{"get":{"tags":["Query"],"summary":"Get template details","description":"Returns complete template metadata with dynamically calculated resource requirements (CPU, memory, storage, NodePort count) derived from the template YAML. Falls back to static configuration if calculation fails. Response headers: `Cache-Control` (public, max-age=300, s-maxage=600), `ETag`.","operationId":"getTemplate","security":[],"parameters":[{"in":"path","name":"name","schema":{"type":"string","description":"Template name identifier (e.g., \"perplexica\", \"yourls\")","example":"perplexica"},"required":true,"description":"Template name identifier (e.g., \"perplexica\", \"yourls\")"},{"in":"query","name":"language","schema":{"description":"Language code for internationalization (e.g., \"en\", \"zh\"). Defaults to \"en\"","example":"en","type":"string"},"description":"Language code for internationalization (e.g., \"en\", \"zh\"). Defaults to \"en\""}],"responses":{"200":{"description":"Successfully retrieved template details","headers":{"Cache-Control":{"description":"Caching directive: public, max-age=300, s-maxage=600","schema":{"type":"string","example":"public, max-age=300, s-maxage=600"}},"ETag":{"description":"Entity tag for caching, format: \"{name}-{language}\"","schema":{"type":"string","example":"\"perplexica-en\""}}},"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Unique template identifier"},"resourceType":{"type":"string","const":"template","description":"Resource type, always \"template\""},"readme":{"type":"string","description":"URL to README documentation. Empty string when not available."},"icon":{"type":"string","description":"URL to template icon image. Empty string when not available."},"description":{"type":"string","description":"Brief description of the template"},"gitRepo":{"type":"string","description":"Git repository URL. Empty string when not available."},"category":{"type":"array","items":{"type":"string"},"description":"Template categories (e.g., [\"ai\", \"database\"])"},"args":{"type":"object","properties":{},"additionalProperties":{"type":"object","properties":{"description":{"type":"string","description":"Human-readable explanation of the argument"},"type":{"type":"string","description":"Argument type (e.g. \"string\", \"number\", \"boolean\", \"password\")"},"default":{"type":"string","description":"Default value used during deployment"},"required":{"type":"boolean","description":"Whether this argument is mandatory"}},"required":["description","type","default","required"],"additionalProperties":false,"description":"Template argument configuration"},"description":"Arguments required for template deployment"},"deployCount":{"type":"number","description":"Number of deployments (includes multiplier for display)"},"quota":{"type":"object","properties":{"cpu":{"type":"number","description":"Required CPU cores in vCPU"},"memory":{"type":"number","description":"Required memory in GiB"},"storage":{"type":"number","description":"Required storage in GiB"},"nodeport":{"type":"number","description":"Number of NodePort services required"}},"required":["cpu","memory","storage","nodeport"],"additionalProperties":false,"description":"Calculated resource requirements"}},"required":["name","resourceType","readme","icon","description","gitRepo","category","args","deployCount","quota"],"additionalProperties":false},"examples":{"templateDetail":{"summary":"Sample template with resource calculation","value":{"name":"perplexica","resourceType":"template","quota":{"cpu":1,"memory":2.25,"storage":2,"nodeport":0},"readme":"https://raw.githubusercontent.com/ItzCrazyKns/Perplexica/master/README.md","icon":"https://raw.githubusercontent.com/ItzCrazyKns/Perplexica/refs/heads/master/src/app/favicon.ico","description":"AI-powered search engine","gitRepo":"https://github.com/ItzCrazyKns/Perplexica","category":["ai"],"args":{"OPENAI_API_KEY":{"description":"The API Key of the OpenAI-compatible service","type":"string","default":"","required":true}},"deployCount":156}}}}}},"400":{"description":"Bad request - template name is required","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"],"description":"High-level error type for categorization"},"code":{"type":"string","enum":["INVALID_PARAMETER"],"description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","description":"Field path using dot/bracket notation, e.g. \"ports[0].number\""},"message":{"type":"string","description":"Validation error message for this field"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"nameRequired":{"summary":"Template name is required","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Template name is required.","details":[{"field":"name","message":"Required"}]}}}}}}},"404":{"description":"Template not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error","description":"High-level error type for categorization"},"code":{"type":"string","const":"NOT_FOUND","description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"templateNotFound":{"summary":"Template not found","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Template 'nonexistent' not found."}}}}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["internal_error"],"description":"High-level error type for categorization"},"code":{"type":"string","enum":["INTERNAL_ERROR"],"description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"internalError":{"summary":"Failed to get template details","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Failed to get template details.","details":"YAML parsing error"}}}}}}}}}},"/templates/raw":{"post":{"tags":["Mutation"],"summary":"Deploy template from raw YAML","description":"Deploy an arbitrary or custom template by supplying its raw YAML directly in the request body. The instance name is auto-generated from `${{ random(8) }}` inside `spec.defaults.app_name`. Use `dryRun: true` to validate the resources against the Kubernetes API without creating anything.\n\n**Example — dry-run a custom template:**\n```json\n{\n \"yaml\": \"apiVersion: app.sealos.io/v1\\nkind: Template\\n...\",\n \"dryRun\": true\n}\n```","operationId":"deployRawTemplate","requestBody":{"required":true,"description":"Template deployment configuration","content":{"application/json":{"schema":{"type":"object","properties":{"yaml":{"type":"string","description":"Full template YAML string. Must start with a `kind: Template` document, followed by one or more `---`-separated Kubernetes resource documents.","example":"apiVersion: app.sealos.io/v1\nkind: Template\nmetadata:\n name: my-app\nspec:\n defaults:\n app_name:\n type: string\n value: my-app-${{ random(8) }}\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: ${{ defaults.app_name }}\nspec:\n replicas: 1\n selector:\n matchLabels:\n app: ${{ defaults.app_name }}\n template:\n metadata:\n labels:\n app: ${{ defaults.app_name }}\n spec:\n containers:\n - name: app\n image: nginx:latest\n resources:\n limits:\n cpu: 100m\n memory: 256Mi"},"args":{"type":"object","properties":{},"additionalProperties":{"type":"string","description":"Template variable value"},"description":"Template variable key-value pairs that override or supply `spec.inputs` fields. Only args without a non-empty default are required.","example":{"MY_SECRET":"my-secret-value"}},"dryRun":{"type":"boolean","description":"If true, validates the resources against the Kubernetes API but does not create anything. Returns 200 with a preview. Default: false.","example":true}},"required":["yaml"]}}}},"responses":{"200":{"description":"Dry-run preview — resources validated but not created","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Auto-generated instance name from ${{ random(8) }} in spec.defaults"},"resourceType":{"type":"string","const":"instance","description":"Always \"instance\""},"dryRun":{"type":"boolean","const":true,"description":"Always true for dry-run responses"},"args":{"type":"object","properties":{},"additionalProperties":{"type":"string"},"description":"Resolved template arguments after merging user-provided values with defaults"},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Resource name"},"uid":{"type":"string","description":"Kubernetes UID"},"resourceType":{"type":"string","description":"Resource type (lowercase k8s kind)"},"quota":{"type":"object","properties":{"cpu":{"description":"CPU cores","type":"number"},"memory":{"description":"Memory in GiB","type":"number"},"storage":{"description":"Storage in GiB","type":"number"},"replicas":{"description":"Number of replicas","type":"number"}},"additionalProperties":false,"description":"Resource quota (for Deployment/StatefulSet/Cluster)"}},"required":["name","uid","resourceType"],"additionalProperties":false},"description":"Preview of sub-resources that would be created (Instance resource excluded)"}},"required":["name","resourceType","dryRun","args"],"additionalProperties":false},"examples":{"dryRunPreview":{"summary":"Dry-run result","value":{"name":"myapp-abcdefgh","resourceType":"instance","dryRun":true,"args":{},"resources":[{"name":"myapp-abcdefgh","uid":"","resourceType":"deployment","quota":{"cpu":0.1,"memory":0.25,"storage":0,"replicas":1}}]}}}}}},"201":{"description":"Template deployed successfully","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Instance name (matches the name specified in the request)"},"uid":{"type":"string","description":"Kubernetes UID of the Instance resource"},"resourceType":{"type":"string","const":"instance","description":"Always \"instance\""},"displayName":{"type":"string","description":"Display name"},"createdAt":{"type":"string","description":"ISO 8601 creation timestamp"},"args":{"type":"object","properties":{},"additionalProperties":{"type":"string","description":"Template variable value"},"description":"Resolved template arguments after merging user-provided values with defaults"},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Resource name"},"uid":{"type":"string","description":"Kubernetes UID"},"resourceType":{"type":"string","description":"Resource type (lowercase k8s kind)"},"quota":{"type":"object","properties":{"cpu":{"description":"CPU cores","type":"number"},"memory":{"description":"Memory in GiB","type":"number"},"storage":{"description":"Storage in GiB","type":"number"},"replicas":{"description":"Number of replicas","type":"number"}},"additionalProperties":false,"description":"Resource quota (for Deployment/StatefulSet/Cluster)"}},"required":["name","uid","resourceType"],"additionalProperties":false},"description":"Sub-resources created for this instance"}},"required":["name","uid","resourceType","displayName","createdAt","args"],"additionalProperties":false},"examples":{"deployed":{"summary":"Template deployed successfully","value":{"name":"myapp-abcdefgh","uid":"778bf3c6-b412-4a02-908b-cf1470867c93","resourceType":"instance","displayName":"","createdAt":"2026-01-28T03:31:01Z","args":{},"resources":[{"name":"myapp-abcdefgh","uid":"5bd2c77d-b8f4-4aa4-97ee-c205f2d10aa9","resourceType":"deployment","quota":{"cpu":0.1,"memory":0.25,"storage":0,"replicas":1}}]}}}}}},"400":{"description":"Bad request — missing or invalid YAML / missing required args","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"],"description":"High-level error type for categorization"},"code":{"type":"string","enum":["INVALID_PARAMETER","INVALID_VALUE"],"description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","description":"Field path using dot/bracket notation, e.g. \"ports[0].number\""},"message":{"type":"string","description":"Validation error message for this field"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingYaml":{"summary":"YAML is required","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Template YAML is required.","details":[{"field":"yaml","message":"Required"}]}}},"invalidYaml":{"summary":"First document is not kind: Template","value":{"error":{"type":"validation_error","code":"INVALID_VALUE","message":"The first YAML type is not Template"}}},"missingArgs":{"summary":"Required template args missing","value":{"error":{"type":"validation_error","code":"INVALID_VALUE","message":"Missing required parameters: MY_SECRET."}}}}}}},"401":{"description":"Unauthorized — missing or invalid kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error","description":"High-level error type for categorization"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED","description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Invalid or missing kubeconfig."}}}}}}},"403":{"description":"Forbidden — insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authorization_error","description":"High-level error type for categorization"},"code":{"type":"string","enum":["PERMISSION_DENIED"],"description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"forbidden":{"summary":"Insufficient privileges","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Permission denied: insufficient privileges to create resources.","details":"deployments.apps is forbidden: User \"system:serviceaccount:ns-xxx\" cannot create resource \"deployments\""}}}}}}},"409":{"description":"Conflict — instance already exists","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error","description":"High-level error type for categorization"},"code":{"type":"string","enum":["ALREADY_EXISTS"],"description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"alreadyExists":{"summary":"Instance already exists","value":{"error":{"type":"resource_error","code":"ALREADY_EXISTS","message":"Instance 'myapp-abcdefgh' already exists.","details":"deployments.apps \"myapp-abcdefgh\" already exists"}}}}}}},"422":{"description":"Unprocessable Entity — K8s rejected the resource spec","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"operation_error","description":"High-level error type for categorization"},"code":{"type":"string","const":"INVALID_RESOURCE_SPEC","description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"Raw K8s rejection reason (admission webhook message, invalid field error, quota exceeded message).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"invalidSpec":{"summary":"Resource spec rejected by cluster","value":{"error":{"type":"operation_error","code":"INVALID_RESOURCE_SPEC","message":"Template validation failed: invalid resource specification.","details":"admission webhook \"vingress.sealos.io\" denied the request: cannot verify ingress host"}}}}}}},"500":{"description":"Internal Server Error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["operation_error","internal_error"],"description":"High-level error type for categorization"},"code":{"type":"string","enum":["KUBERNETES_ERROR","INTERNAL_ERROR"],"description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"internalError":{"summary":"Unexpected server error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Failed to deploy template.","details":"Template parsing error"}}}}}}},"503":{"description":"Service Unavailable — Kubernetes cluster temporarily unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"internal_error","description":"High-level error type for categorization"},"code":{"type":"string","const":"SERVICE_UNAVAILABLE","description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"Kubernetes cluster is temporarily unavailable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}}},"/templates/instances":{"post":{"tags":["Mutation"],"summary":"Create template instance","description":"Deploy a named instance of a template into the user's Kubernetes namespace. User-provided `args` are merged with the template's declared defaults — only args with no default value are required. The `args` field in the response reflects the fully resolved values after applying defaults.\n\n**Example — create a Perplexica instance:**\n```json\n{\n \"name\": \"my-app-instance\",\n \"template\": \"perplexica\",\n \"args\": {\n \"OPENAI_API_KEY\": \"\",\n \"OPENAI_MODEL_NAME\": \"gpt-4o\"\n }\n}\n```","operationId":"createInstance","requestBody":{"required":true,"description":"Instance creation configuration","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Instance name. 1–63 lowercase alphanumeric characters or hyphens, must start and end with alphanumeric (Kubernetes DNS subdomain rules). e.g. \"my-perplexica-instance\"","example":"my-perplexica-instance"},"template":{"type":"string","description":"Template name from the catalog. Use GET /templates to list available templates. e.g. \"perplexica\"","example":"perplexica"},"args":{"type":"object","properties":{},"additionalProperties":{"type":"string","description":"Template variable value"},"description":"Template variable key-value pairs. Only args without a default value are required. Use GET /templates/{name} to see which args are required and their defaults.","example":{"OPENAI_API_KEY":"sk-xxxxxxxxxxxxxxxxxxxx","OPENAI_MODEL_NAME":"gpt-4o"}}},"required":["name","template"]}}}},"responses":{"201":{"description":"Instance created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Instance name (matches the name specified in the request)"},"uid":{"type":"string","description":"Kubernetes UID of the Instance resource"},"resourceType":{"type":"string","const":"instance","description":"Always \"instance\""},"displayName":{"type":"string","description":"Display name"},"createdAt":{"type":"string","description":"ISO 8601 creation timestamp"},"args":{"type":"object","properties":{},"additionalProperties":{"type":"string","description":"Template variable value"},"description":"Resolved template arguments after merging user-provided values with defaults"},"resources":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Resource name"},"uid":{"type":"string","description":"Kubernetes UID"},"resourceType":{"type":"string","description":"Resource type (lowercase k8s kind)"},"quota":{"type":"object","properties":{"cpu":{"description":"CPU cores","type":"number"},"memory":{"description":"Memory in GiB","type":"number"},"storage":{"description":"Storage in GiB","type":"number"},"replicas":{"description":"Number of replicas","type":"number"}},"additionalProperties":false,"description":"Resource quota (for Deployment/StatefulSet/Cluster)"}},"required":["name","uid","resourceType"],"additionalProperties":false},"description":"Sub-resources created for this instance"}},"required":["name","uid","resourceType","displayName","createdAt","args"],"additionalProperties":false},"examples":{"instanceCreated":{"summary":"Instance created successfully","value":{"name":"my-perplexica-instance","uid":"778bf3c6-b412-4a02-908b-cf1470867c93","resourceType":"instance","displayName":"","createdAt":"2026-01-28T03:31:01Z","args":{"OPENAI_API_KEY":"sk-xxxxxxxxxxxxxxxxxxxxxxxx","OPENAI_API_URL":"https://api.openai.com/v1","OPENAI_MODEL_NAME":"gpt-4o"},"resources":[{"name":"my-perplexica-instance-searxng","uid":"5bd2c77d-b8f4-4aa4-97ee-c205f2d10aa9","resourceType":"deployment","quota":{"cpu":0.1,"memory":0.25,"storage":0,"replicas":1}},{"name":"my-perplexica-instance","uid":"256e2577-fa3a-4471-a94c-8cbd5410187c","resourceType":"statefulset","quota":{"cpu":0.2,"memory":0.5,"storage":1,"replicas":1}}]}}}}}},"400":{"description":"Bad request - missing or invalid parameters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["validation_error"],"description":"High-level error type for categorization"},"code":{"type":"string","enum":["INVALID_PARAMETER","INVALID_VALUE"],"description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"For INVALID_PARAMETER: Array<{ field, message }>. For INVALID_VALUE: optional string. Omitted for other codes.","anyOf":[{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","description":"Field path using dot/bracket notation, e.g. \"ports[0].number\""},"message":{"type":"string","description":"Validation error message for this field"}},"required":["field","message"],"additionalProperties":false}},{"type":"string"}]}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"invalidParameter":{"summary":"Instance or template name required","value":{"error":{"type":"validation_error","code":"INVALID_PARAMETER","message":"Instance name is required.","details":[{"field":"name","message":"Required"}]}}},"invalidValue":{"summary":"Invalid instance name format","value":{"error":{"type":"validation_error","code":"INVALID_VALUE","message":"Instance name must start and end with a lowercase letter or number, and can only contain lowercase letters, numbers, and hyphens."}}}}}}},"401":{"description":"Unauthorized - Missing or invalid kubeconfig","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authentication_error","description":"High-level error type for categorization"},"code":{"type":"string","const":"AUTHENTICATION_REQUIRED","description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"missingAuth":{"summary":"Missing authentication","value":{"error":{"type":"authentication_error","code":"AUTHENTICATION_REQUIRED","message":"Invalid or missing kubeconfig."}}}}}}},"403":{"description":"Forbidden - Insufficient permissions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"authorization_error","description":"High-level error type for categorization"},"code":{"type":"string","enum":["PERMISSION_DENIED"],"description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"insufficientPermissions":{"summary":"Insufficient privileges to create resources","value":{"error":{"type":"authorization_error","code":"PERMISSION_DENIED","message":"Permission denied: insufficient privileges to create resources.","details":"deployments.apps is forbidden: User \"system:serviceaccount:ns-xxx\" cannot create resource \"deployments\" in API group \"apps\" in the namespace \"ns-xxx\""}}}}}}},"404":{"description":"Not Found - Template not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error","description":"High-level error type for categorization"},"code":{"type":"string","const":"NOT_FOUND","description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"templateNotFound":{"summary":"Template does not exist","value":{"error":{"type":"resource_error","code":"NOT_FOUND","message":"Template 'nonexistent-template' not found."}}}}}}},"409":{"description":"Conflict - Instance already exists","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"resource_error","description":"High-level error type for categorization"},"code":{"type":"string","enum":["ALREADY_EXISTS"],"description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"Typically omitted. May contain additional context in edge cases.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"alreadyExists":{"summary":"Instance with this name already exists","value":{"error":{"type":"resource_error","code":"ALREADY_EXISTS","message":"Instance 'my-perplexica-instance' already exists.","details":"deployments.apps \"my-perplexica-instance\" already exists"}}}}}}},"422":{"description":"Unprocessable Entity - K8s rejected the resource (admission webhook, invalid field, quota exceeded)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"operation_error","description":"High-level error type for categorization"},"code":{"type":"string","const":"INVALID_RESOURCE_SPEC","description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"Raw K8s rejection reason (admission webhook message, invalid field error, quota exceeded message).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"invalidResourceSpec":{"summary":"Resource spec rejected by cluster","value":{"error":{"type":"operation_error","code":"INVALID_RESOURCE_SPEC","message":"Failed to create instance: invalid resource specification.","details":"admission webhook \"vingress.sealos.io\" denied the request: cannot verify ingress host"}}}}}}},"500":{"description":"Internal Server Error - Kubernetes API error or unexpected failure","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","enum":["operation_error","internal_error"],"description":"High-level error type for categorization"},"code":{"type":"string","enum":["KUBERNETES_ERROR","OPERATION_FAILED","INTERNAL_ERROR"],"description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"Raw error string from the underlying system, for troubleshooting.","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"kubernetesError":{"summary":"Kubernetes API error","value":{"error":{"type":"operation_error","code":"KUBERNETES_ERROR","message":"Failed to create instance in Kubernetes.","details":"Unexpected error from Kubernetes API"}}},"internalError":{"summary":"Unexpected server error","value":{"error":{"type":"internal_error","code":"INTERNAL_ERROR","message":"Failed to create instance.","details":"Template parsing error"}}}}}}},"503":{"description":"Service Unavailable - Kubernetes cluster temporarily unreachable","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"type":{"type":"string","const":"internal_error","description":"High-level error type for categorization"},"code":{"type":"string","const":"SERVICE_UNAVAILABLE","description":"Specific error code for programmatic handling and i18n"},"message":{"type":"string","description":"Human-readable error message"},"details":{"description":"Raw connection error from the underlying system (e.g. ECONNREFUSED).","type":"string"}},"required":["type","code","message"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},"examples":{"clusterUnavailable":{"summary":"Cluster unreachable","value":{"error":{"type":"internal_error","code":"SERVICE_UNAVAILABLE","message":"Kubernetes cluster is temporarily unavailable.","details":"connect ECONNREFUSED 10.0.0.1:6443"}}}}}}}}}}},"components":{"securitySchemes":{"kubeconfigAuth":{"type":"apiKey","in":"header","name":"Authorization","description":"URL-encoded kubeconfig YAML. Encode with `encodeURIComponent(kubeconfigYaml)` before setting the header value. Obtain your kubeconfig from the Sealos console."}}}} \ No newline at end of file diff --git a/skills/template/scripts/sealos-auth.mjs b/skills/template/scripts/sealos-auth.mjs new file mode 100644 index 0000000..aa25cf5 --- /dev/null +++ b/skills/template/scripts/sealos-auth.mjs @@ -0,0 +1,329 @@ +#!/usr/bin/env node + +/** + * Sealos Cloud Authentication — OAuth2 Device Grant Flow (RFC 8628) + * + * Usage: + * node sealos-auth.mjs check # Check if already authenticated + * node sealos-auth.mjs login [region] # Start device grant login flow + * node sealos-auth.mjs info # Show current auth info + * + * Environment variables: + * SEALOS_REGION — Sealos Cloud region URL (default from config.json) + * + * Flow: + * 1. POST /api/auth/oauth2/device → { device_code, user_code, verification_uri_complete } + * 2. User opens verification_uri_complete in browser to authorize + * 3. Script polls /api/auth/oauth2/token until approved + * 4. Receives access_token → exchanges for kubeconfig → saves to ~/.sealos/kubeconfig + */ + +import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs' +import { execSync } from 'child_process' +import { homedir, platform } from 'os' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' + +// ── Paths ──────────────────────────────────────────────── +const __dirname = dirname(fileURLToPath(import.meta.url)) +const SEALOS_DIR = join(homedir(), '.sealos') +const KC_PATH = join(SEALOS_DIR, 'kubeconfig') +const AUTH_PATH = join(SEALOS_DIR, 'auth.json') + +// ── Skill constants (from config.json) ─────────────────── +const CONFIG_PATH = join(__dirname, '..', 'config.json') +const config = JSON.parse(readFileSync(CONFIG_PATH, 'utf-8')) +const CLIENT_ID = config.client_id +const DEFAULT_REGION = config.default_region + +// ── Check ────────────────────────────────────────────── + +function check () { + if (!existsSync(KC_PATH)) { + return { authenticated: false } + } + + try { + const kc = readFileSync(KC_PATH, 'utf-8') + if (kc.includes('server:') && (kc.includes('token:') || kc.includes('client-certificate'))) { + const auth = existsSync(AUTH_PATH) ? JSON.parse(readFileSync(AUTH_PATH, 'utf-8')) : {} + return { + authenticated: true, + kubeconfig_path: KC_PATH, + region: auth.region || 'unknown' + } + } + } catch { } + + return { authenticated: false } +} + +// ── Device Grant Flow ────────────────────────────────── + +/** + * Step 1: Request device authorization + * POST /api/auth/oauth2/device + * Body: { client_id } + * Response: { device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval } + */ +async function requestDeviceAuthorization (region) { + const res = await fetch(`${region}/api/auth/oauth2/device`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + client_id: CLIENT_ID, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code' + }) + }) + + if (!res.ok) { + const body = await res.text().catch(() => '') + throw new Error(`Device authorization request failed (${res.status}): ${body || res.statusText}`) + } + + return res.json() +} + +/** + * Step 2: Poll for token + * POST /api/auth/oauth2/token + * Body: { client_id, grant_type, device_code } + * + * Possible responses: + * - 200: { access_token, token_type, ... } → success + * - 400: { error: "authorization_pending" } → keep polling + * - 400: { error: "slow_down" } → increase interval by 5s + * - 400: { error: "access_denied" } → user denied + * - 400: { error: "expired_token" } → device code expired + */ +async function pollForToken (region, deviceCode, interval, expiresIn) { + // Hard cap at 10 minutes regardless of server's expires_in + const maxWait = Math.min(expiresIn, 600) * 1000 + const deadline = Date.now() + maxWait + let pollInterval = interval * 1000 + let lastLoggedMinute = -1 + + while (Date.now() < deadline) { + await sleep(pollInterval) + + // Log remaining time every minute + const remaining = Math.ceil((deadline - Date.now()) / 60000) + if (remaining !== lastLoggedMinute && remaining > 0) { + lastLoggedMinute = remaining + process.stderr.write(` Waiting for authorization... (${remaining} min remaining)\n`) + } + + const res = await fetch(`${region}/api/auth/oauth2/token`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + client_id: CLIENT_ID, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + device_code: deviceCode + }) + }) + + if (res.ok) { + // Success — got the token + return res.json() + } + + const body = await res.json().catch(() => ({})) + + switch (body.error) { + case 'authorization_pending': + // User hasn't authorized yet, keep polling + break + + case 'slow_down': + // Increase polling interval by 5 seconds (RFC 8628 §3.5) + pollInterval += 5000 + break + + case 'access_denied': + throw new Error('Authorization denied by user') + + case 'expired_token': + throw new Error('Device code expired. Please run login again.') + + default: + throw new Error(`Token request failed: ${body.error || res.statusText}`) + } + } + + throw new Error('Authorization timed out (10 minutes). Please run login again.') +} + +/** + * Step 3: Exchange access token for kubeconfig + */ +async function exchangeForKubeconfig (region, accessToken) { + const res = await fetch(`${region}/api/auth/getDefaultKubeconfig`, { + method: 'POST', + headers: { + Authorization: accessToken, + 'Content-Type': 'application/json' + } + }) + + if (!res.ok) { + const body = await res.text().catch(() => '') + throw new Error(`Kubeconfig exchange failed (${res.status}): ${body || res.statusText}`) + } + + return res.json() +} + +function sleep (ms) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +// ── Login (Device Grant Flow) ────────────────────────── + +async function login (region = DEFAULT_REGION) { + region = region.replace(/\/+$/, '') + + // Step 1: Request device authorization + const deviceAuth = await requestDeviceAuthorization(region) + + const { + device_code: deviceCode, + user_code: userCode, + verification_uri: verificationUri, + verification_uri_complete: verificationUriComplete, + expires_in: expiresIn, + interval = 5 + } = deviceAuth + + // Output device authorization info for the AI tool / user to display + const authPrompt = { + action: 'user_authorization_required', + user_code: userCode, + verification_uri: verificationUri, + verification_uri_complete: verificationUriComplete, + expires_in: expiresIn, + message: `Please open the following URL in your browser to authorize:\n\n ${verificationUriComplete || verificationUri}\n\nAuthorization code: ${userCode}\nExpires in: ${Math.floor(expiresIn / 60)} minutes` + } + + // Print the authorization prompt to stderr so it's visible to the user + // while stdout is reserved for JSON output + process.stderr.write('\n' + authPrompt.message + '\n\nWaiting for authorization...\n') + + // Auto-open browser + const url = verificationUriComplete || verificationUri + try { + const cmd = platform() === 'darwin' ? 'open' : platform() === 'win32' ? 'start' : 'xdg-open' + execSync(`${cmd} "${url}"`, { stdio: 'ignore' }) + process.stderr.write('Browser opened automatically.\n') + } catch { + process.stderr.write('Could not open browser automatically. Please open the URL manually.\n') + } + + // Step 2: Poll for token + const tokenResponse = await pollForToken(region, deviceCode, interval, expiresIn) + const accessToken = tokenResponse.access_token + + if (!accessToken) { + throw new Error('Token response missing access_token') + } + + process.stderr.write('Authorization received. Exchanging for kubeconfig...\n') + + // Step 3: Exchange access token for kubeconfig + const kcData = await exchangeForKubeconfig(region, accessToken) + const kubeconfig = kcData.data?.kubeconfig + + if (!kubeconfig) { + throw new Error('API response missing data.kubeconfig field') + } + + // Save kubeconfig to ~/.sealos/kubeconfig (Sealos-specific, avoids conflict with ~/.kube/config) + mkdirSync(SEALOS_DIR, { recursive: true }) + writeFileSync(KC_PATH, kubeconfig, { mode: 0o600 }) + writeFileSync(AUTH_PATH, JSON.stringify({ + region, + authenticated_at: new Date().toISOString(), + auth_method: 'oauth2_device_grant' + }, null, 2), { mode: 0o600 }) + + process.stderr.write('Authentication successful!\n') + + return { kubeconfig_path: KC_PATH, region } +} + +// ── Info ─────────────────────────────────────────────── + +function info () { + const status = check() + if (!status.authenticated) { + return { authenticated: false, message: 'Not authenticated. Run: node sealos-auth.mjs login' } + } + + const auth = existsSync(AUTH_PATH) ? JSON.parse(readFileSync(AUTH_PATH, 'utf-8')) : {} + return { + authenticated: true, + kubeconfig_path: KC_PATH, + region: auth.region || 'unknown', + auth_method: auth.auth_method || 'unknown', + authenticated_at: auth.authenticated_at || 'unknown' + } +} + +// ── CLI ──────────────────────────────────────────────── + +const [, , cmd, ...rawArgs] = process.argv + +// --insecure flag: skip TLS certificate verification (for self-signed certs) +const insecure = rawArgs.includes('--insecure') +const args = rawArgs.filter(a => a !== '--insecure') + +if (insecure) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' +} + +try { + switch (cmd) { + case 'check': { + console.log(JSON.stringify(check())) + break + } + + case 'login': { + const region = args[0] || process.env.SEALOS_REGION || DEFAULT_REGION + const result = await login(region) + console.log(JSON.stringify(result)) + break + } + + case 'info': { + console.log(JSON.stringify(info(), null, 2)) + break + } + + default: { + console.log(`Sealos Cloud Auth — OAuth2 Device Grant Flow + +Usage: + node sealos-auth.mjs check Check authentication status + node sealos-auth.mjs login [region] Start OAuth2 device login flow + node sealos-auth.mjs login --insecure Skip TLS verification (self-signed cert) + node sealos-auth.mjs info Show current auth details + +Environment: + SEALOS_REGION Region URL (default: ${DEFAULT_REGION}) + +Flow: + 1. Run "login" → opens browser for authorization + 2. Approve in browser → script receives token automatically + 3. Token exchanged for kubeconfig → saved to ~/.sealos/kubeconfig`) + } + } +} catch (err) { + // If TLS error and not using --insecure, hint the user + if (!insecure && (err.message.includes('fetch failed') || err.message.includes('self-signed') || err.message.includes('CERT'))) { + console.error(JSON.stringify({ error: err.message, hint: 'Try adding --insecure for self-signed certificates' })) + } else { + console.error(JSON.stringify({ error: err.message })) + } + process.exit(1) +} diff --git a/skills/template/scripts/sealos-template.mjs b/skills/template/scripts/sealos-template.mjs new file mode 100644 index 0000000..37b32d2 --- /dev/null +++ b/skills/template/scripts/sealos-template.mjs @@ -0,0 +1,264 @@ +#!/usr/bin/env node +// Sealos Template CLI - single entry point for all template operations. +// Zero external dependencies. Requires Node.js (guaranteed by Claude Code). +// +// Usage: +// node sealos-template.mjs [args...] +// +// Config resolution: +// ~/.sealos/auth.json → region → derive API URL +// ~/.sealos/kubeconfig → credentials for auth-required operations +// +// Commands: +// list [--language=en] List all templates (no auth needed) +// get [--language=en] Get template details (no auth needed) +// create Create a template instance (auth required) +// create-raw Deploy from raw YAML (auth required) + +import { readFileSync, existsSync } from 'node:fs'; +import { request as httpsRequest } from 'node:https'; +import { request as httpRequest } from 'node:http'; +import { resolve, dirname } from 'node:path'; +import { homedir } from 'node:os'; +import { fileURLToPath } from 'node:url'; + +const KC_PATH = resolve(homedir(), '.sealos/kubeconfig'); +const AUTH_PATH = resolve(homedir(), '.sealos/auth.json'); +const API_PATH = '/api/v2alpha'; // API version — update here if the version changes + +// --- config --- + +// Default region fallback for unauthenticated operations (list/get) +const SKILL_CONFIG_PATH = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'config.json'); + +function loadConfig({ requireAuth = false } = {}) { + let region = null; + + // Try to read region from auth.json (set by login) + if (existsSync(AUTH_PATH)) { + try { + const auth = JSON.parse(readFileSync(AUTH_PATH, 'utf-8')); + if (auth.region) region = auth.region; + } catch { /* ignore invalid auth.json for public operations */ } + } + + // Fall back to default region from skill config.json for public operations + if (!region) { + try { + const skillConfig = JSON.parse(readFileSync(SKILL_CONFIG_PATH, 'utf-8')); + region = skillConfig.default_region; + } catch { /* no fallback available */ } + } + + if (!region) { + throw new Error('No region configured. Run: node sealos-auth.mjs login'); + } + + const regionUrl = new URL(region); + const apiUrl = `https://template.${regionUrl.hostname}${API_PATH}`; + + if (requireAuth && !existsSync(KC_PATH)) { + throw new Error(`Kubeconfig not found at ${KC_PATH}. Run: node sealos-auth.mjs login`); + } + + return { apiUrl, kubeconfigPath: KC_PATH }; +} + +// --- auth --- + +function getEncodedKubeconfig(path) { + if (!existsSync(path)) { + throw new Error(`Kubeconfig not found at ${path}`); + } + return encodeURIComponent(readFileSync(path, 'utf-8')); +} + +// --- HTTP --- + +function apiCall(method, endpoint, { apiUrl, auth, body, timeout = 30000 } = {}) { + return new Promise((resolve, reject) => { + const url = new URL(apiUrl + endpoint); + const isHttps = url.protocol === 'https:'; + const reqFn = isHttps ? httpsRequest : httpRequest; + + const headers = {}; + if (auth) headers['Authorization'] = auth; + if (body) headers['Content-Type'] = 'application/json'; + + const bodyStr = body ? JSON.stringify(body) : null; + if (bodyStr) headers['Content-Length'] = Buffer.byteLength(bodyStr); + + const opts = { + hostname: url.hostname, + port: url.port || (isHttps ? 443 : 80), + path: url.pathname + url.search, + method, + headers, + timeout, + rejectUnauthorized: false, // Sealos clusters may use self-signed certificates + }; + + const req = reqFn(opts, (res) => { + const chunks = []; + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => { + const rawBody = Buffer.concat(chunks).toString(); + let parsed = null; + try { parsed = JSON.parse(rawBody); } catch { parsed = rawBody || null; } + resolve({ status: res.statusCode, body: parsed, headers: res.headers }); + }); + }); + + req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); }); + req.on('error', reject); + if (bodyStr) req.write(bodyStr); + req.end(); + }); +} + +// --- helpers --- + +function output(data) { + console.log(JSON.stringify(data, null, 2)); +} + +// --- parse CLI flags --- + +function parseFlags(args) { + const flags = {}; + const positional = []; + for (const arg of args) { + if (arg.startsWith('--')) { + const eqIdx = arg.indexOf('='); + if (eqIdx > 0) { + flags[arg.slice(2, eqIdx)] = arg.slice(eqIdx + 1); + } else { + flags[arg.slice(2)] = true; + } + } else { + positional.push(arg); + } + } + return { flags, positional }; +} + +// --- validation --- + +function validateCreateBody(body) { + const errors = []; + if (!body.name) { + errors.push('name is required'); + } else { + if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(body.name)) errors.push('name must match ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'); + if (body.name.length > 63) errors.push('name must be at most 63 characters'); + } + if (!body.template) errors.push('template is required'); + if (errors.length) throw new Error('Validation failed: ' + errors.join('; ')); +} + +function validateCreateRawBody(body) { + const errors = []; + if (!body.yaml) errors.push('yaml is required'); + if (errors.length) throw new Error('Validation failed: ' + errors.join('; ')); +} + +// --- individual commands --- + +async function listTemplates(cfg, language) { + const langParam = language ? `?language=${encodeURIComponent(language)}` : ''; + const res = await apiCall('GET', `/templates${langParam}`, { apiUrl: cfg.apiUrl }); + if (res.status !== 200) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + const menuKeys = res.headers['x-menu-keys'] || ''; + return { templates: res.body, menuKeys }; +} + +async function getTemplate(cfg, name, language) { + const langParam = language ? `?language=${encodeURIComponent(language)}` : ''; + const res = await apiCall('GET', `/templates/${encodeURIComponent(name)}${langParam}`, { apiUrl: cfg.apiUrl }); + if (res.status !== 200) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +async function createInstance(cfg, jsonBody) { + const body = typeof jsonBody === 'string' ? JSON.parse(jsonBody) : jsonBody; + validateCreateBody(body); + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('POST', '/templates/instances', { apiUrl: cfg.apiUrl, auth, body }); + if (res.status !== 201) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +async function createRaw(cfg, jsonOrFilepath) { + let body; + if (typeof jsonOrFilepath === 'string' && (jsonOrFilepath.startsWith('/') || jsonOrFilepath.startsWith('~'))) { + const filePath = jsonOrFilepath.replace(/^~/, homedir()); + const absPath = resolve(filePath); + if (!existsSync(absPath)) throw new Error(`File not found: ${absPath}`); + const content = readFileSync(absPath, 'utf-8'); + body = JSON.parse(content); + } else { + body = typeof jsonOrFilepath === 'string' ? JSON.parse(jsonOrFilepath) : jsonOrFilepath; + } + validateCreateRawBody(body); + const auth = getEncodedKubeconfig(cfg.kubeconfigPath); + const res = await apiCall('POST', '/templates/raw', { apiUrl: cfg.apiUrl, auth, body }); + if (res.status !== 200 && res.status !== 201) throw new Error(`HTTP ${res.status}: ${JSON.stringify(res.body)}`); + return res.body; +} + +// --- main --- + +async function main() { + const [cmd, ...args] = process.argv.slice(2); + + if (!cmd) { + console.error('ERROR: Command required.'); + console.error('Commands: list|get|create|create-raw'); + process.exit(1); + } + + try { + let result; + + switch (cmd) { + case 'list': { + const { flags } = parseFlags(args); + const publicCfg = loadConfig(); + result = await listTemplates(publicCfg, flags.language || 'en'); + break; + } + + case 'get': { + const { flags, positional } = parseFlags(args); + if (!positional[0]) throw new Error('Template name required'); + const publicCfg = loadConfig(); + result = await getTemplate(publicCfg, positional[0], flags.language || 'en'); + break; + } + + case 'create': { + if (!args[0]) throw new Error('JSON body required'); + const authCfg = loadConfig({ requireAuth: true }); + result = await createInstance(authCfg, args[0]); + break; + } + + case 'create-raw': { + if (!args[0]) throw new Error('JSON body or file path required'); + const authCfg = loadConfig({ requireAuth: true }); + result = await createRaw(authCfg, args[0]); + break; + } + + default: + throw new Error(`Unknown command '${cmd}'. Commands: list|get|create|create-raw`); + } + + if (result !== undefined) output(result); + } catch (err) { + console.error(`ERROR: ${err.message}`); + process.exit(1); + } +} + +main();