diff --git a/cmd/serve.go b/cmd/serve.go index 2aa4b3d..7037e48 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -18,7 +18,7 @@ import ( "github.com/glueops/autoglue/internal/auth" "github.com/glueops/autoglue/internal/bg" "github.com/glueops/autoglue/internal/config" - "github.com/glueops/autoglue/internal/web" + "github.com/glueops/autoglue/internal/models" "github.com/google/uuid" "github.com/spf13/cobra" ) @@ -34,13 +34,13 @@ var serveCmd = &cobra.Command{ return err } - var pgwebInst *web.Pgweb - jobs, err := bg.NewJobs(rt.DB, cfg.DbURL) if err != nil { log.Fatalf("failed to init background jobs: %v", err) } + rt.DB.Where("status IN ?", []string{"scheduled", "queued", "pending"}).Delete(&models.Job{}) + // Start workers in background ONCE go func() { if err := jobs.Start(); err != nil { @@ -53,7 +53,7 @@ var serveCmd = &cobra.Command{ { // schedule next 03:30 local time next := time.Now().Truncate(24 * time.Hour).Add(24 * time.Hour).Add(3*time.Hour + 30*time.Minute) - _, _ = jobs.Enqueue( + _, err = jobs.Enqueue( context.Background(), uuid.NewString(), "archer_cleanup", @@ -61,10 +61,13 @@ var serveCmd = &cobra.Command{ archer.WithScheduleTime(next), archer.WithMaxRetries(1), ) + if err != nil { + log.Fatalf("failed to enqueue archer cleanup job: %v", err) + } // schedule next 03:45 local time next2 := time.Now().Truncate(24 * time.Hour).Add(24 * time.Hour).Add(3*time.Hour + 45*time.Minute) - _, _ = jobs.Enqueue( + _, err = jobs.Enqueue( context.Background(), uuid.NewString(), "tokens_cleanup", @@ -72,45 +75,47 @@ var serveCmd = &cobra.Command{ archer.WithScheduleTime(next2), archer.WithMaxRetries(1), ) + if err != nil { + log.Fatalf("failed to enqueue token cleanup job: %v", err) + } - _, _ = jobs.Enqueue( + _, err = jobs.Enqueue( context.Background(), uuid.NewString(), "db_backup_s3", - bg.DbBackupArgs{}, + bg.DbBackupArgs{IntervalS: 3600}, archer.WithMaxRetries(1), archer.WithScheduleTime(time.Now().Add(1*time.Hour)), ) - } - - // Periodic scheduler - schedCtx, schedCancel := context.WithCancel(context.Background()) - defer schedCancel() - - ticker := time.NewTicker(10 * time.Second) - defer ticker.Stop() - - go func() { - for { - select { - case <-ticker.C: - _, err := jobs.Enqueue( - context.Background(), - uuid.NewString(), - "bootstrap_bastion", - bg.BastionBootstrapArgs{}, - archer.WithMaxRetries(3), - // while debugging, avoid extra schedule delay: - archer.WithScheduleTime(time.Now().Add(60*time.Second)), - ) - if err != nil { - log.Printf("failed to enqueue bootstrap_bastion: %v", err) - } - case <-schedCtx.Done(): - return - } + if err != nil { + log.Fatalf("failed to enqueue backup jobs: %v", err) } - }() + + _, err = jobs.Enqueue( + context.Background(), + uuid.NewString(), + "dns_reconcile", + bg.DNSReconcileArgs{MaxDomains: 25, MaxRecords: 100, IntervalS: 10}, + archer.WithScheduleTime(time.Now().Add(5*time.Second)), + archer.WithMaxRetries(1), + ) + if err != nil { + log.Fatalf("failed to enqueue dns reconcile: %v", err) + } + + _, err := jobs.Enqueue( + context.Background(), + uuid.NewString(), + "bootstrap_bastion", + bg.BastionBootstrapArgs{IntervalS: 10}, + archer.WithMaxRetries(3), + // while debugging, avoid extra schedule delay: + archer.WithScheduleTime(time.Now().Add(60*time.Second)), + ) + if err != nil { + log.Printf("failed to enqueue bootstrap_bastion: %v", err) + } + } _ = auth.Refresh(rt.DB, rt.Cfg.JWTPrivateEncKey) go func() { @@ -121,7 +126,6 @@ var serveCmd = &cobra.Command{ } }() - var studioHandler http.Handler r := api.NewRouter(rt.DB, jobs, nil) if cfg.DBStudioEnabled { @@ -130,20 +134,16 @@ var serveCmd = &cobra.Command{ dbURL = cfg.DbURL } - pgwebInst, err = web.StartPgweb( + studio, err := api.PgwebHandler( dbURL, - cfg.DBStudioBind, - cfg.DBStudioPort, - true, - cfg.DBStudioUser, - cfg.DBStudioPass, + "db-studio", + false, ) if err != nil { - log.Printf("pgweb failed to start: %v", err) + log.Fatalf("failed to init db studio: %v", err) } else { - studioHandler = http.HandlerFunc(pgwebInst.Proxy()) - r = api.NewRouter(rt.DB, jobs, studioHandler) - log.Printf("pgweb running on http://%s:%s (proxied at /db-studio/)", cfg.DBStudioBind, pgwebInst.Port()) + r = api.NewRouter(rt.DB, jobs, studio) + log.Printf("pgweb mounted at /db-studio/") } } @@ -169,9 +169,6 @@ var serveCmd = &cobra.Command{ <-ctx.Done() fmt.Println("\n⏳ Shutting down...") - if pgwebInst != nil { - _ = pgwebInst.Stop(context.Background()) - } shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() return srv.Shutdown(shutdownCtx) diff --git a/docs/docs.go b/docs/docs.go index eb66110..2e04ad8 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -5,7 +5,7 @@ package docs import "github.com/swaggo/swag/v2" const docTemplate = `{ - "schemes": {{ marshal .Schemes }},"swagger":"2.0","info":{"description":"{{escape .Description}}","title":"{{.Title}}","contact":{"name":"GlueOps"},"version":"{{.Version}}"},"host":"{{.Host}}","basePath":"{{.BasePath}}","paths":{"/.well-known/jwks.json":{"get":{"description":"Returns the JSON Web Key Set for token verification","produces":["application/json"],"tags":["Auth"],"summary":"Get JWKS","operationId":"getJWKS","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.JWKS"}}}}},"/admin/archer/jobs":{"get":{"security":[{"BearerAuth":[]}],"description":"Paginated background jobs with optional filters. Search ` + "`" + `q` + "`" + ` may match id, type, error, payload (implementation-dependent).","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"List Archer jobs (admin)","operationId":"AdminListArcherJobs","parameters":[{"enum":["queued","running","succeeded","failed","canceled","retrying","scheduled"],"type":"string","description":"Filter by status","name":"status","in":"query"},{"type":"string","description":"Filter by queue name / worker name","name":"queue","in":"query"},{"type":"string","description":"Free-text search","name":"q","in":"query"},{"type":"integer","default":1,"description":"Page number","name":"page","in":"query"},{"maximum":100,"minimum":1,"type":"integer","default":25,"description":"Items per page","name":"page_size","in":"query"}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.PageJob"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"500":{"description":"internal error","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]}],"description":"Create a job immediately or schedule it for the future via ` + "`" + `run_at` + "`" + `.","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"Enqueue a new Archer job (admin)","operationId":"AdminEnqueueArcherJob","parameters":[{"description":"Job parameters","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.EnqueueRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.Job"}},"400":{"description":"invalid json or missing fields","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"500":{"description":"internal error","schema":{"type":"string"}}}}},"/admin/archer/jobs/{id}/cancel":{"post":{"security":[{"BearerAuth":[]}],"description":"Set job status to canceled if cancellable. For running jobs, this only affects future picks; wire to Archer if you need active kill.","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"Cancel an Archer job (admin)","operationId":"AdminCancelArcherJob","parameters":[{"type":"string","description":"Job ID","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.Job"}},"400":{"description":"invalid job or not cancellable","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/admin/archer/jobs/{id}/retry":{"post":{"security":[{"BearerAuth":[]}],"description":"Marks the job retriable (DB flip). Swap this for an Archer admin call if you expose one.","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"Retry a failed/canceled Archer job (admin)","operationId":"AdminRetryArcherJob","parameters":[{"type":"string","description":"Job ID","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.Job"}},"400":{"description":"invalid job or not eligible","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/admin/archer/queues":{"get":{"security":[{"BearerAuth":[]}],"description":"Summary metrics per queue (pending, running, failed, scheduled).","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"List Archer queues (admin)","operationId":"AdminListArcherQueues","responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.QueueInfo"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"500":{"description":"internal error","schema":{"type":"string"}}}}},"/annotations":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns annotations for the organization in X-Org-ID. Filters: ` + "`" + `key` + "`" + `, ` + "`" + `value` + "`" + `, and ` + "`" + `q` + "`" + ` (key contains). Add ` + "`" + `include=node_pools` + "`" + ` to include linked node pools.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"List annotations (org scoped)","operationId":"ListAnnotations","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Exact key","name":"key","in":"query"},{"type":"string","description":"Exact value","name":"value","in":"query"},{"type":"string","description":"key contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.AnnotationResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list annotations","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates an annotation.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"Create annotation (org scoped)","operationId":"CreateAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Annotation payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateAnnotationRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.AnnotationResponse"}},"400":{"description":"invalid json / missing fields","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/annotations/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns one annotation. Add ` + "`" + `include=node_pools` + "`" + ` to include node pools.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"Get annotation by ID (org scoped)","operationId":"GetAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Annotation ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.AnnotationResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the annotation.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"Delete annotation (org scoped)","operationId":"DeleteAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Annotation ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update annotation fields.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"Update annotation (org scoped)","operationId":"UpdateAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Annotation ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateAnnotationRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.AnnotationResponse"}},"400":{"description":"invalid id / invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/auth/logout":{"post":{"consumes":["application/json"],"produces":["application/json"],"tags":["Auth"],"summary":"Revoke refresh token family (logout everywhere)","operationId":"Logout","parameters":[{"description":"Refresh token","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.LogoutRequest"}}],"responses":{"204":{"description":"No Content"}}}},"/auth/refresh":{"post":{"consumes":["application/json"],"produces":["application/json"],"tags":["Auth"],"summary":"Rotate refresh token","operationId":"Refresh","parameters":[{"description":"Refresh token","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.RefreshRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.TokenPair"}}}}},"/auth/{provider}/callback":{"get":{"produces":["application/json"],"tags":["Auth"],"summary":"Handle social login callback","operationId":"AuthCallback","parameters":[{"type":"string","description":"google|github","name":"provider","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.TokenPair"}}}}},"/auth/{provider}/start":{"post":{"description":"Returns provider authorization URL for the frontend to redirect","produces":["application/json"],"tags":["Auth"],"summary":"Begin social login","operationId":"AuthStart","parameters":[{"type":"string","description":"google|github","name":"provider","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.AuthStartResponse"}}}}},"/clusters":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns clusters for the organization in X-Org-ID. Filter by ` + "`" + `q` + "`" + ` (name contains).","produces":["application/json"],"tags":["Clusters"],"summary":"List clusters (org scoped)","operationId":"ListClusters","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Name contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.ClusterResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list clusters","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a cluster. If ` + "`" + `kubeconfig` + "`" + ` is provided, it will be encrypted per-organization and stored securely (never returned).","consumes":["application/json"],"produces":["application/json"],"tags":["Clusters"],"summary":"Create cluster (org scoped)","operationId":"CreateCluster","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateClusterRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.ClusterResponse"}},"400":{"description":"invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/credentials":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns credential metadata for the current org. Secrets are never returned.","consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"List credentials (metadata only)","operationId":"ListCredentials","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Filter by provider (e.g., aws)","name":"provider","in":"query"},{"type":"string","description":"Filter by kind (e.g., aws_access_key)","name":"kind","in":"query"},{"type":"string","description":"Filter by scope kind (provider/service/resource)","name":"scope_kind","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.CredentialOut"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"internal server error","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Create a credential (encrypts secret)","operationId":"CreateCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"description":"Credential payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateCredentialRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.CredentialOut"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"internal server error","schema":{"type":"string"}}}}},"/credentials/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Get credential by ID (metadata only)","operationId":"GetCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Credential ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.CredentialOut"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"internal server error","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Delete credential","operationId":"DeleteCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Credential ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content"},"404":{"description":"not found","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Update credential metadata and/or rotate secret","operationId":"UpdateCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Credential ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateCredentialRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.CredentialOut"}},"403":{"description":"X-Org-ID required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/credentials/{id}/reveal":{"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Reveal decrypted secret (one-time read)","operationId":"RevealCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Credential ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"object","additionalProperties":true}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/healthz":{"get":{"description":"Returns 200 OK when the service is up","consumes":["application/json"],"produces":["application/json"],"tags":["Health"],"summary":"Basic health check","operationId":"HealthCheck // operationId","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.HealthStatus"}}}}},"/labels":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns node labels for the organization in X-Org-ID. Filters: ` + "`" + `key` + "`" + `, ` + "`" + `value` + "`" + `, and ` + "`" + `q` + "`" + ` (key contains). Add ` + "`" + `include=node_pools` + "`" + ` to include linked node groups.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"List node labels (org scoped)","operationId":"ListLabels","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Exact key","name":"key","in":"query"},{"type":"string","description":"Exact value","name":"value","in":"query"},{"type":"string","description":"Key contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.LabelResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list node taints","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a label.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"Create label (org scoped)","operationId":"CreateLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Label payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateLabelRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.LabelResponse"}},"400":{"description":"invalid json / missing fields / invalid node_pool_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/labels/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns one label.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"Get label by ID (org scoped)","operationId":"GetLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Label ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.LabelResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the label.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"Delete label (org scoped)","operationId":"DeleteLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Label ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update label fields.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"Update label (org scoped)","operationId":"UpdateLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Label ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateLabelRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.LabelResponse"}},"400":{"description":"invalid id / invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/me":{"get":{"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"produces":["application/json"],"tags":["Me"],"summary":"Get current user profile","operationId":"GetMe","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.meResponse"}}}},"patch":{"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Me"],"summary":"Update current user profile","operationId":"UpdateMe","parameters":[{"description":"Patch profile","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.updateMeRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/models.User"}}}}},"/me/api-keys":{"get":{"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"produces":["application/json"],"tags":["MeAPIKeys"],"summary":"List my API keys","operationId":"ListUserAPIKeys","responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/handlers.userAPIKeyOut"}}}}},"post":{"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"description":"Returns the plaintext key once. Store it securely on the client side.","consumes":["application/json"],"produces":["application/json"],"tags":["MeAPIKeys"],"summary":"Create a new user API key","operationId":"CreateUserAPIKey","parameters":[{"description":"Key options","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.createUserKeyRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/handlers.userAPIKeyOut"}}}}},"/me/api-keys/{id}":{"delete":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["MeAPIKeys"],"summary":"Delete a user API key","operationId":"DeleteUserAPIKey","parameters":[{"type":"string","description":"Key ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content"}}}},"/node-pools":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns node pools for the organization in X-Org-ID.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List node pools (org scoped)","operationId":"ListNodePools","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Name contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.NodePoolResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list node pools","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a node pool. Optionally attach initial servers.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Create node pool (org scoped)","operationId":"CreateNodePool","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"NodePool payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateNodePoolRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.NodePoolResponse"}},"400":{"description":"invalid json / missing fields / invalid server_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/node-pools/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns one node pool. Add ` + "`" + `include=servers` + "`" + ` to include servers.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Get node pool by ID (org scoped)","operationId":"GetNodePool","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.NodePoolResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the node pool.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Delete node pool (org scoped)","operationId":"DeleteNodePool","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update node pool fields.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Update node pool (org scoped)","operationId":"UpdateNodePool","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateNodePoolRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.NodePoolResponse"}},"400":{"description":"invalid id / invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/node-pools/{id}/annotations":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List annotations attached to a node pool (org scoped)","operationId":"ListNodePoolAnnotations","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.AnnotationResponse"}}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Attach annotation to a node pool (org scoped)","operationId":"AttachNodePoolAnnotations","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Group ID (UUID)","name":"id","in":"path","required":true},{"description":"Annotation IDs to attach","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.AttachAnnotationsRequest"}}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid server_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"attach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/annotations/{annotationId}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Detach one annotation from a node pool (org scoped)","operationId":"DetachNodePoolAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Annotation ID (UUID)","name":"annotationId","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"detach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/labels":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List labels attached to a node pool (org scoped)","operationId":"ListNodePoolLabels","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Label Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.LabelResponse"}}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Attach labels to a node pool (org scoped)","operationId":"AttachNodePoolLabels","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"description":"Label IDs to attach","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.AttachLabelsRequest"}}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid server_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"attach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/labels/{labelId}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Detach one label from a node pool (org scoped)","operationId":"DetachNodePoolLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Label ID (UUID)","name":"labelId","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"detach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/servers":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List servers attached to a node pool (org scoped)","operationId":"ListNodePoolServers","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.ServerResponse"}}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Attach servers to a node pool (org scoped)","operationId":"AttachNodePoolServers","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"description":"Server IDs to attach","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.AttachServersRequest"}}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid server_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"attach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/servers/{serverId}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Detach one server from a node pool (org scoped)","operationId":"DetachNodePoolServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Server ID (UUID)","name":"serverId","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"detach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/taints":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List taints attached to a node pool (org scoped)","operationId":"ListNodePoolTaints","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.TaintResponse"}}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Attach taints to a node pool (org scoped)","operationId":"AttachNodePoolTaints","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"description":"Taint IDs to attach","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.AttachTaintsRequest"}}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid taint_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"attach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/taints/{taintId}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Detach one taint from a node pool (org scoped)","operationId":"DetachNodePoolTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Taint ID (UUID)","name":"taintId","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"detach failed","schema":{"type":"string"}}}}},"/orgs":{"get":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"List organizations I belong to","operationId":"listMyOrgs","responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/models.Organization"}}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"post":{"security":[{"BearerAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Orgs"],"summary":"Create organization","operationId":"createOrg","parameters":[{"description":"Org payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.orgCreateReq"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/models.Organization"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"409":{"description":"Conflict","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}":{"get":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"Get organization","operationId":"getOrg","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/models.Organization"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"delete":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"Delete organization (owner)","operationId":"deleteOrg","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"Deleted"},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"patch":{"security":[{"BearerAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Orgs"],"summary":"Update organization (owner/admin)","operationId":"updateOrg","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"description":"Update payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.orgUpdateReq"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/models.Organization"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}/api-keys":{"get":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"List org-scoped API keys (no secrets)","operationId":"listOrgKeys","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/models.APIKey"}}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"post":{"security":[{"BearerAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Orgs"],"summary":"Create org key/secret pair (owner/admin)","operationId":"createOrgKey","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"description":"Key name + optional expiry","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.orgKeyCreateReq"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/handlers.orgKeyCreateResp"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}/api-keys/{key_id}":{"delete":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"Delete org key (owner/admin)","operationId":"deleteOrgKey","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Key ID (UUID)","name":"key_id","in":"path","required":true}],"responses":{"204":{"description":"Deleted"},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}/members":{"get":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"List members in org","operationId":"listMembers","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/handlers.memberOut"}}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"post":{"security":[{"BearerAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Orgs"],"summary":"Add or update a member (owner/admin)","operationId":"addOrUpdateMember","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"description":"User \u0026 role","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.memberUpsertReq"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.memberOut"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}/members/{user_id}":{"delete":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"Remove a member (owner/admin)","operationId":"removeMember","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"User ID (UUID)","name":"user_id","in":"path","required":true}],"responses":{"204":{"description":"Removed"},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/servers":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns servers for the organization in X-Org-ID. Optional filters: status, role.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"List servers (org scoped)","operationId":"ListServers","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Filter by status (pending|provisioning|ready|failed)","name":"status","in":"query"},{"type":"string","description":"Filter by role","name":"role","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.ServerResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list servers","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a server bound to the org in X-Org-ID. Validates that ssh_key_id belongs to the org.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Create server (org scoped)","operationId":"CreateServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Server payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateServerRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.ServerResponse"}},"400":{"description":"invalid json / missing fields / invalid status / invalid ssh_key_id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/servers/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns one server in the given organization.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Get server by ID (org scoped)","operationId":"GetServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Server ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.ServerResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the server.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Delete server (org scoped)","operationId":"DeleteServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Server ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update fields; changing ssh_key_id validates ownership.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Update server (org scoped)","operationId":"UpdateServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Server ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateServerRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.ServerResponse"}},"400":{"description":"invalid id / invalid json / invalid status / invalid ssh_key_id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/ssh":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns ssh keys for the organization in X-Org-ID.","consumes":["application/json"],"produces":["application/json"],"tags":["Ssh"],"summary":"List ssh keys (org scoped)","operationId":"ListPublicSshKeys","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.SshResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list keys","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Generates an RSA or ED25519 keypair, saves it, and returns metadata. For RSA you may set bits (2048/3072/4096). Default is 4096. ED25519 ignores bits.","consumes":["application/json"],"produces":["application/json"],"tags":["Ssh"],"summary":"Create ssh keypair (org scoped)","operationId":"CreateSSHKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Key generation options","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateSSHRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.SshResponse"}},"400":{"description":"invalid json / invalid bits","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"generation/create failed","schema":{"type":"string"}}}}},"/ssh/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns public key fields. Append ` + "`" + `?reveal=true` + "`" + ` to include the private key PEM.","consumes":["application/json"],"produces":["application/json"],"tags":["Ssh"],"summary":"Get ssh key by ID (org scoped)","operationId":"GetSSHKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"SSH Key ID (UUID)","name":"id","in":"path","required":true},{"type":"boolean","description":"Reveal private key PEM","name":"reveal","in":"query"}],"responses":{"200":{"description":"When reveal=true","schema":{"$ref":"#/definitions/dto.SshRevealResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes a keypair.","consumes":["application/json"],"produces":["application/json"],"tags":["Ssh"],"summary":"Delete ssh keypair (org scoped)","operationId":"DeleteSSHKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"SSH Key ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}}},"/ssh/{id}/download":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Download ` + "`" + `part=public|private|both` + "`" + ` of the keypair. ` + "`" + `both` + "`" + ` returns a zip file.","produces":["application/json"],"tags":["Ssh"],"summary":"Download ssh key files by ID (org scoped)","operationId":"DownloadSSHKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header","required":true},{"type":"string","description":"SSH Key ID (UUID)","name":"id","in":"path","required":true},{"enum":["public","private","both"],"type":"string","description":"Which part to download","name":"part","in":"query","required":true}],"responses":{"200":{"description":"file content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid part","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"download failed","schema":{"type":"string"}}}}},"/taints":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns node taints for the organization in X-Org-ID. Filters: ` + "`" + `key` + "`" + `, ` + "`" + `value` + "`" + `, and ` + "`" + `q` + "`" + ` (key contains). Add ` + "`" + `include=node_pools` + "`" + ` to include linked node pools.","consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"List node pool taints (org scoped)","operationId":"ListTaints","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Exact key","name":"key","in":"query"},{"type":"string","description":"Exact value","name":"value","in":"query"},{"type":"string","description":"key contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.TaintResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list node taints","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a taint.","consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"Create node taint (org scoped)","operationId":"CreateTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Taint payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateTaintRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.TaintResponse"}},"400":{"description":"invalid json / missing fields / invalid node_pool_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/taints/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"Get node taint by ID (org scoped)","operationId":"GetTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Taint ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.TaintResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the taint.","consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"Delete taint (org scoped)","operationId":"DeleteTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Taint ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update taint fields.","consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"Update node taint (org scoped)","operationId":"UpdateTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Taint ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateTaintRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.TaintResponse"}},"400":{"description":"invalid id / invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/version":{"get":{"description":"Returns build/runtime metadata for the running service.","consumes":["application/json"],"produces":["application/json"],"tags":["Meta"],"summary":"Service version information","operationId":"Version // operationId","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.VersionResponse"}}}}}},"definitions":{"dto.AnnotationResponse":{"type":"object","properties":{"created_at":{"type":"string"},"id":{"type":"string"},"key":{"type":"string"},"organization_id":{"type":"string"},"updated_at":{"type":"string"},"value":{"type":"string"}}},"dto.AttachAnnotationsRequest":{"type":"object","properties":{"annotation_ids":{"type":"array","items":{"type":"string"}}}},"dto.AttachLabelsRequest":{"type":"object","properties":{"label_ids":{"type":"array","items":{"type":"string"}}}},"dto.AttachServersRequest":{"type":"object","properties":{"server_ids":{"type":"array","items":{"type":"string"}}}},"dto.AttachTaintsRequest":{"type":"object","properties":{"taint_ids":{"type":"array","items":{"type":"string"}}}},"dto.AuthStartResponse":{"type":"object","properties":{"auth_url":{"type":"string","example":"https://accounts.google.com/o/oauth2/v2/auth?client_id=..."}}},"dto.ClusterResponse":{"type":"object","properties":{"bastion_server":{"$ref":"#/definitions/dto.ServerResponse"},"captain_domain":{"type":"string"},"certificate_key":{"type":"string"},"cluster_load_balancer":{"type":"string"},"control_load_balancer":{"type":"string"},"created_at":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"node_pools":{"type":"array","items":{"$ref":"#/definitions/dto.NodePoolResponse"}},"provider":{"type":"string"},"random_token":{"type":"string"},"region":{"type":"string"},"status":{"type":"string"},"updated_at":{"type":"string"}}},"dto.CreateAnnotationRequest":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"dto.CreateClusterRequest":{"type":"object","properties":{"captain_domain":{"type":"string"},"cluster_load_balancer":{"type":"string"},"control_load_balancer":{"type":"string"},"name":{"type":"string"},"provider":{"type":"string"},"region":{"type":"string"},"status":{"type":"string"}}},"dto.CreateCredentialRequest":{"type":"object","required":["kind","provider","schema_version","scope","scope_kind","scope_version","secret"],"properties":{"account_id":{"type":"string","maxLength":32},"kind":{"description":"aws_access_key, api_token, basic_auth, oauth2","type":"string"},"name":{"description":"human label","type":"string","maxLength":100},"provider":{"type":"string","enum":["aws","cloudflare","hetzner","digitalocean","generic"]},"region":{"type":"string","maxLength":32},"schema_version":{"description":"secret schema version","type":"integer","minimum":1},"scope":{"description":"{\"service\":\"route53\"} or {\"arn\":\"...\"}","type":"object"},"scope_kind":{"type":"string","enum":["provider","service","resource"]},"scope_version":{"description":"scope schema version","type":"integer","minimum":1},"secret":{"description":"encrypted later","type":"object"}}},"dto.CreateLabelRequest":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"dto.CreateNodePoolRequest":{"type":"object","properties":{"name":{"type":"string"},"role":{"type":"string","enum":["master","worker"]}}},"dto.CreateSSHRequest":{"type":"object","properties":{"bits":{"description":"Only for RSA","type":"integer"},"comment":{"type":"string","example":"deploy@autoglue"},"name":{"type":"string"},"type":{"description":"\"rsa\" (default) or \"ed25519\"","type":"string"}}},"dto.CreateServerRequest":{"type":"object","properties":{"hostname":{"type":"string"},"private_ip_address":{"type":"string"},"public_ip_address":{"type":"string"},"role":{"type":"string","enum":["master","worker","bastion"],"example":"master|worker|bastion"},"ssh_key_id":{"type":"string"},"ssh_user":{"type":"string"},"status":{"type":"string","enum":["pending","provisioning","ready","failed"],"example":"pending|provisioning|ready|failed"}}},"dto.CreateTaintRequest":{"type":"object","properties":{"effect":{"type":"string"},"key":{"type":"string"},"value":{"type":"string"}}},"dto.CredentialOut":{"type":"object","properties":{"account_id":{"type":"string"},"created_at":{"type":"string"},"id":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"provider":{"type":"string"},"region":{"type":"string"},"schema_version":{"type":"integer"},"scope":{"type":"object"},"scope_kind":{"type":"string"},"scope_version":{"type":"integer"},"updated_at":{"type":"string"}}},"dto.EnqueueRequest":{"type":"object","properties":{"payload":{"type":"object"},"queue":{"type":"string","example":"default"},"run_at":{"type":"string","example":"2025-11-05T08:00:00Z"},"type":{"type":"string","example":"email.send"}}},"dto.JWK":{"type":"object","properties":{"alg":{"type":"string","example":"RS256"},"e":{"type":"string","example":"AQAB"},"kid":{"type":"string","example":"7c6f1d0a-7a98-4e6a-9dbf-6b1af4b9f345"},"kty":{"type":"string","example":"RSA"},"n":{"type":"string"},"use":{"type":"string","example":"sig"},"x":{"type":"string"}}},"dto.JWKS":{"type":"object","properties":{"keys":{"type":"array","items":{"$ref":"#/definitions/dto.JWK"}}}},"dto.Job":{"type":"object","properties":{"attempts":{"type":"integer","example":0},"created_at":{"type":"string","example":"2025-11-04T09:30:00Z"},"id":{"type":"string","example":"01HF7SZK8Z8WG1M3J7S2Z8M2N6"},"last_error":{"type":"string","example":"error message"},"max_attempts":{"type":"integer","example":3},"payload":{},"queue":{"type":"string","example":"default"},"run_at":{"type":"string","example":"2025-11-04T09:30:00Z"},"status":{"enum":["queued|running|succeeded|failed|canceled|retrying|scheduled"],"allOf":[{"$ref":"#/definitions/dto.JobStatus"}],"example":"queued"},"type":{"type":"string","example":"email.send"},"updated_at":{"type":"string","example":"2025-11-04T09:30:00Z"}}},"dto.JobStatus":{"type":"string","enum":["queued","running","succeeded","failed","canceled","retrying","scheduled"],"x-enum-varnames":["StatusQueued","StatusRunning","StatusSucceeded","StatusFailed","StatusCanceled","StatusRetrying","StatusScheduled"]},"dto.LabelResponse":{"type":"object","properties":{"created_at":{"type":"string"},"id":{"type":"string"},"key":{"type":"string"},"organization_id":{"type":"string"},"updated_at":{"type":"string"},"value":{"type":"string"}}},"dto.LogoutRequest":{"type":"object","properties":{"refresh_token":{"type":"string","example":"m0l9o8rT3t0V8d3eFf..."}}},"dto.NodePoolResponse":{"type":"object","properties":{"annotations":{"type":"array","items":{"$ref":"#/definitions/dto.AnnotationResponse"}},"created_at":{"type":"string"},"id":{"type":"string"},"labels":{"type":"array","items":{"$ref":"#/definitions/dto.LabelResponse"}},"name":{"type":"string"},"organization_id":{"type":"string"},"role":{"type":"string","enum":["master","worker"]},"servers":{"type":"array","items":{"$ref":"#/definitions/dto.ServerResponse"}},"taints":{"type":"array","items":{"$ref":"#/definitions/dto.TaintResponse"}},"updated_at":{"type":"string"}}},"dto.PageJob":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/definitions/dto.Job"}},"page":{"type":"integer","example":1},"page_size":{"type":"integer","example":25},"total":{"type":"integer","example":120}}},"dto.QueueInfo":{"type":"object","properties":{"failed":{"type":"integer","example":5},"name":{"type":"string","example":"default"},"pending":{"type":"integer","example":42},"running":{"type":"integer","example":3},"scheduled":{"type":"integer","example":7}}},"dto.RefreshRequest":{"type":"object","properties":{"refresh_token":{"type":"string","example":"m0l9o8rT3t0V8d3eFf..."}}},"dto.ServerResponse":{"type":"object","properties":{"created_at":{"type":"string"},"hostname":{"type":"string"},"id":{"type":"string"},"organization_id":{"type":"string"},"private_ip_address":{"type":"string"},"public_ip_address":{"type":"string"},"role":{"type":"string","enum":["master","worker","bastion"],"example":"master|worker|bastion"},"ssh_key_id":{"type":"string"},"ssh_user":{"type":"string"},"status":{"type":"string","enum":["pending","provisioning","ready","failed"],"example":"pending|provisioning|ready|failed"},"updated_at":{"type":"string"}}},"dto.SshResponse":{"type":"object","properties":{"created_at":{"type":"string"},"fingerprint":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"organization_id":{"type":"string"},"public_key":{"type":"string"},"updated_at":{"type":"string"}}},"dto.SshRevealResponse":{"type":"object","properties":{"created_at":{"type":"string"},"fingerprint":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"organization_id":{"type":"string"},"private_key":{"type":"string"},"public_key":{"type":"string"},"updated_at":{"type":"string"}}},"dto.TaintResponse":{"type":"object","properties":{"created_at":{"type":"string"},"effect":{"type":"string"},"id":{"type":"string"},"key":{"type":"string"},"organization_id":{"type":"string"},"updated_at":{"type":"string"},"value":{"type":"string"}}},"dto.TokenPair":{"type":"object","properties":{"access_token":{"type":"string","example":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ij..."},"expires_in":{"type":"integer","example":3600},"refresh_token":{"type":"string","example":"m0l9o8rT3t0V8d3eFf...."},"token_type":{"type":"string","example":"Bearer"}}},"dto.UpdateAnnotationRequest":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"dto.UpdateCredentialRequest":{"type":"object","properties":{"account_id":{"type":"string"},"name":{"type":"string"},"region":{"type":"string"},"scope":{"type":"object"},"scope_kind":{"type":"string"},"scope_version":{"type":"integer"},"secret":{"description":"set if rotating","type":"object"}}},"dto.UpdateLabelRequest":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"dto.UpdateNodePoolRequest":{"type":"object","properties":{"name":{"type":"string"},"role":{"type":"string","enum":["master","worker"]}}},"dto.UpdateServerRequest":{"type":"object","properties":{"hostname":{"type":"string"},"private_ip_address":{"type":"string"},"public_ip_address":{"type":"string"},"role":{"type":"string","enum":["master","worker","bastion"],"example":"master|worker|bastion"},"ssh_key_id":{"type":"string"},"ssh_user":{"type":"string"},"status":{"type":"string","enum":["pending","provisioning","ready","failed"],"example":"pending|provisioning|ready|failed"}}},"dto.UpdateTaintRequest":{"type":"object","properties":{"effect":{"type":"string"},"key":{"type":"string"},"value":{"type":"string"}}},"handlers.HealthStatus":{"type":"object","properties":{"status":{"type":"string","example":"ok"}}},"handlers.VersionResponse":{"type":"object","properties":{"built":{"type":"string","example":"2025-11-08T12:34:56Z"},"builtBy":{"type":"string","example":"ci"},"commit":{"type":"string","example":"a1b2c3d"},"commitTime":{"type":"string","example":"2025-11-08T12:31:00Z"},"go":{"type":"string","example":"go1.23.3"},"goArch":{"type":"string","example":"amd64"},"goOS":{"type":"string","example":"linux"},"modified":{"type":"boolean","example":false},"revision":{"type":"string","example":"a1b2c3d4e5f6abcdef"},"vcs":{"type":"string","example":"git"},"version":{"type":"string","example":"1.4.2"}}},"handlers.createUserKeyRequest":{"type":"object","properties":{"expires_in_hours":{"description":"optional TTL","type":"integer"},"name":{"type":"string"}}},"handlers.meResponse":{"type":"object","properties":{"avatar_url":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"display_name":{"type":"string"},"emails":{"type":"array","items":{"$ref":"#/definitions/models.UserEmail"}},"id":{"description":"example: 3fa85f64-5717-4562-b3fc-2c963f66afa6","type":"string","format":"uuid"},"is_admin":{"type":"boolean"},"is_disabled":{"type":"boolean"},"organizations":{"type":"array","items":{"$ref":"#/definitions/models.Organization"}},"primary_email":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"handlers.memberOut":{"type":"object","properties":{"email":{"type":"string"},"role":{"description":"owner/admin/member","type":"string"},"user_id":{"type":"string","format":"uuid"}}},"handlers.memberUpsertReq":{"type":"object","properties":{"role":{"type":"string","example":"member"},"user_id":{"type":"string","format":"uuid"}}},"handlers.orgCreateReq":{"type":"object","properties":{"domain":{"type":"string","example":"acme.com"},"name":{"type":"string","example":"Acme Corp"}}},"handlers.orgKeyCreateReq":{"type":"object","properties":{"expires_in_hours":{"type":"integer","example":720},"name":{"type":"string","example":"automation-bot"}}},"handlers.orgKeyCreateResp":{"type":"object","properties":{"created_at":{"type":"string"},"expires_at":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"org_key":{"description":"shown once:","type":"string"},"org_secret":{"description":"shown once:","type":"string"},"scope":{"description":"\"org\"","type":"string"}}},"handlers.orgUpdateReq":{"type":"object","properties":{"domain":{"type":"string"},"name":{"type":"string"}}},"handlers.updateMeRequest":{"type":"object","properties":{"display_name":{"type":"string"}}},"handlers.userAPIKeyOut":{"type":"object","properties":{"created_at":{"type":"string"},"expires_at":{"type":"string"},"id":{"type":"string","format":"uuid"},"last_used_at":{"type":"string"},"name":{"type":"string"},"plain":{"description":"Shown only on create:","type":"string"},"scope":{"description":"\"user\"","type":"string"}}},"models.APIKey":{"type":"object","properties":{"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"last_used_at":{"type":"string","format":"date-time"},"name":{"type":"string"},"org_id":{"type":"string","format":"uuid"},"prefix":{"type":"string"},"revoked":{"type":"boolean"},"scope":{"type":"string"},"updated_at":{"type":"string","format":"date-time"},"user_id":{"type":"string","format":"uuid"}}},"models.Organization":{"type":"object","properties":{"created_at":{"type":"string","format":"date-time"},"domain":{"type":"string"},"id":{"description":"example: 3fa85f64-5717-4562-b3fc-2c963f66afa6","type":"string","format":"uuid"},"name":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"models.User":{"type":"object","properties":{"avatar_url":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"display_name":{"type":"string"},"id":{"description":"example: 3fa85f64-5717-4562-b3fc-2c963f66afa6","type":"string","format":"uuid"},"is_admin":{"type":"boolean"},"is_disabled":{"type":"boolean"},"primary_email":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"models.UserEmail":{"type":"object","properties":{"created_at":{"type":"string","format":"date-time"},"email":{"type":"string"},"id":{"description":"example: 3fa85f64-5717-4562-b3fc-2c963f66afa6","type":"string","format":"uuid"},"is_primary":{"type":"boolean"},"is_verified":{"type":"boolean"},"updated_at":{"type":"string","format":"date-time"},"user":{"$ref":"#/definitions/models.User"},"user_id":{"type":"string","format":"uuid"}}},"utils.ErrorResponse":{"type":"object","properties":{"code":{"description":"A machine-readable error code, e.g. \"validation_error\"\nexample: validation_error","type":"string"},"message":{"description":"Human-readable message\nexample: slug is required","type":"string"}}}},"securityDefinitions":{"ApiKeyAuth":{"description":"User API key","type":"apiKey","name":"X-API-KEY","in":"header"},"BearerAuth":{"description":"Bearer token authentication","type":"apiKey","name":"Authorization","in":"header"},"OrgKeyAuth":{"description":"Org-level key/secret authentication","type":"apiKey","name":"X-ORG-KEY","in":"header"},"OrgSecretAuth":{"description":"Org-level secret","type":"apiKey","name":"X-ORG-SECRET","in":"header"}}}` + "schemes": {{ marshal .Schemes }},"swagger":"2.0","info":{"description":"{{escape .Description}}","title":"{{.Title}}","contact":{"name":"GlueOps"},"version":"{{.Version}}"},"host":"{{.Host}}","basePath":"{{.BasePath}}","paths":{"/.well-known/jwks.json":{"get":{"description":"Returns the JSON Web Key Set for token verification","produces":["application/json"],"tags":["Auth"],"summary":"Get JWKS","operationId":"getJWKS","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.JWKS"}}}}},"/admin/archer/jobs":{"get":{"security":[{"BearerAuth":[]}],"description":"Paginated background jobs with optional filters. Search ` + "`" + `q` + "`" + ` may match id, type, error, payload (implementation-dependent).","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"List Archer jobs (admin)","operationId":"AdminListArcherJobs","parameters":[{"enum":["queued","running","succeeded","failed","canceled","retrying","scheduled"],"type":"string","description":"Filter by status","name":"status","in":"query"},{"type":"string","description":"Filter by queue name / worker name","name":"queue","in":"query"},{"type":"string","description":"Free-text search","name":"q","in":"query"},{"type":"integer","default":1,"description":"Page number","name":"page","in":"query"},{"maximum":100,"minimum":1,"type":"integer","default":25,"description":"Items per page","name":"page_size","in":"query"}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.PageJob"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"500":{"description":"internal error","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]}],"description":"Create a job immediately or schedule it for the future via ` + "`" + `run_at` + "`" + `.","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"Enqueue a new Archer job (admin)","operationId":"AdminEnqueueArcherJob","parameters":[{"description":"Job parameters","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.EnqueueRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.Job"}},"400":{"description":"invalid json or missing fields","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"500":{"description":"internal error","schema":{"type":"string"}}}}},"/admin/archer/jobs/{id}/cancel":{"post":{"security":[{"BearerAuth":[]}],"description":"Set job status to canceled if cancellable. For running jobs, this only affects future picks; wire to Archer if you need active kill.","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"Cancel an Archer job (admin)","operationId":"AdminCancelArcherJob","parameters":[{"type":"string","description":"Job ID","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.Job"}},"400":{"description":"invalid job or not cancellable","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/admin/archer/jobs/{id}/retry":{"post":{"security":[{"BearerAuth":[]}],"description":"Marks the job retriable (DB flip). Swap this for an Archer admin call if you expose one.","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"Retry a failed/canceled Archer job (admin)","operationId":"AdminRetryArcherJob","parameters":[{"type":"string","description":"Job ID","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.Job"}},"400":{"description":"invalid job or not eligible","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/admin/archer/queues":{"get":{"security":[{"BearerAuth":[]}],"description":"Summary metrics per queue (pending, running, failed, scheduled).","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"List Archer queues (admin)","operationId":"AdminListArcherQueues","responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.QueueInfo"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"500":{"description":"internal error","schema":{"type":"string"}}}}},"/annotations":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns annotations for the organization in X-Org-ID. Filters: ` + "`" + `key` + "`" + `, ` + "`" + `value` + "`" + `, and ` + "`" + `q` + "`" + ` (key contains). Add ` + "`" + `include=node_pools` + "`" + ` to include linked node pools.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"List annotations (org scoped)","operationId":"ListAnnotations","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Exact key","name":"key","in":"query"},{"type":"string","description":"Exact value","name":"value","in":"query"},{"type":"string","description":"key contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.AnnotationResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list annotations","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates an annotation.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"Create annotation (org scoped)","operationId":"CreateAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Annotation payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateAnnotationRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.AnnotationResponse"}},"400":{"description":"invalid json / missing fields","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/annotations/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns one annotation. Add ` + "`" + `include=node_pools` + "`" + ` to include node pools.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"Get annotation by ID (org scoped)","operationId":"GetAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Annotation ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.AnnotationResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the annotation.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"Delete annotation (org scoped)","operationId":"DeleteAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Annotation ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update annotation fields.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"Update annotation (org scoped)","operationId":"UpdateAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Annotation ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateAnnotationRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.AnnotationResponse"}},"400":{"description":"invalid id / invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/auth/logout":{"post":{"consumes":["application/json"],"produces":["application/json"],"tags":["Auth"],"summary":"Revoke refresh token family (logout everywhere)","operationId":"Logout","parameters":[{"description":"Refresh token","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.LogoutRequest"}}],"responses":{"204":{"description":"No Content"}}}},"/auth/refresh":{"post":{"consumes":["application/json"],"produces":["application/json"],"tags":["Auth"],"summary":"Rotate refresh token","operationId":"Refresh","parameters":[{"description":"Refresh token","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.RefreshRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.TokenPair"}}}}},"/auth/{provider}/callback":{"get":{"produces":["application/json"],"tags":["Auth"],"summary":"Handle social login callback","operationId":"AuthCallback","parameters":[{"type":"string","description":"google|github","name":"provider","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.TokenPair"}}}}},"/auth/{provider}/start":{"post":{"description":"Returns provider authorization URL for the frontend to redirect","produces":["application/json"],"tags":["Auth"],"summary":"Begin social login","operationId":"AuthStart","parameters":[{"type":"string","description":"google|github","name":"provider","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.AuthStartResponse"}}}}},"/clusters":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns clusters for the organization in X-Org-ID. Filter by ` + "`" + `q` + "`" + ` (name contains).","produces":["application/json"],"tags":["Clusters"],"summary":"List clusters (org scoped)","operationId":"ListClusters","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Name contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.ClusterResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list clusters","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a cluster. If ` + "`" + `kubeconfig` + "`" + ` is provided, it will be encrypted per-organization and stored securely (never returned).","consumes":["application/json"],"produces":["application/json"],"tags":["Clusters"],"summary":"Create cluster (org scoped)","operationId":"CreateCluster","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateClusterRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.ClusterResponse"}},"400":{"description":"invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/credentials":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns credential metadata for the current org. Secrets are never returned.","consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"List credentials (metadata only)","operationId":"ListCredentials","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Filter by provider (e.g., aws)","name":"provider","in":"query"},{"type":"string","description":"Filter by kind (e.g., aws_access_key)","name":"kind","in":"query"},{"type":"string","description":"Filter by scope kind (provider/service/resource)","name":"scope_kind","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.CredentialOut"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"internal server error","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Create a credential (encrypts secret)","operationId":"CreateCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"description":"Credential payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateCredentialRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.CredentialOut"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"internal server error","schema":{"type":"string"}}}}},"/credentials/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Get credential by ID (metadata only)","operationId":"GetCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Credential ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.CredentialOut"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"internal server error","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Delete credential","operationId":"DeleteCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Credential ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content"},"404":{"description":"not found","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Update credential metadata and/or rotate secret","operationId":"UpdateCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Credential ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateCredentialRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.CredentialOut"}},"403":{"description":"X-Org-ID required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/credentials/{id}/reveal":{"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Reveal decrypted secret (one-time read)","operationId":"RevealCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Credential ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"object","additionalProperties":true}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/dns/domains":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns domains for X-Org-ID. Filters: ` + "`" + `domain_name` + "`" + `, ` + "`" + `status` + "`" + `, ` + "`" + `q` + "`" + ` (contains).","consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"List domains (org scoped)","operationId":"ListDomains","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Exact domain name (lowercase, no trailing dot)","name":"domain_name","in":"query"},{"type":"string","description":"pending|provisioning|ready|failed","name":"status","in":"query"},{"type":"string","description":"Domain contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.DomainResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"db error","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a domain bound to a Route 53 scoped credential. Archer will backfill ZoneID if omitted.","consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"Create a domain (org scoped)","operationId":"CreateDomain","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Domain payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateDomainRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.DomainResponse"}},"400":{"description":"validation error","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"db error","schema":{"type":"string"}}}}},"/dns/domains/{domain_id}/records":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Filters: ` + "`" + `name` + "`" + `, ` + "`" + `type` + "`" + `, ` + "`" + `status` + "`" + `.","consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"List record sets for a domain","operationId":"ListRecordSets","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Domain ID (UUID)","name":"domain_id","in":"path","required":true},{"type":"string","description":"Exact relative name or FQDN (server normalizes)","name":"name","in":"query"},{"type":"string","description":"RR type (A, AAAA, CNAME, TXT, MX, NS, SRV, CAA)","name":"type","in":"query"},{"type":"string","description":"pending|provisioning|ready|failed","name":"status","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.RecordSetResponse"}}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"domain not found","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"Create a record set (pending; Archer will UPSERT to Route 53)","operationId":"CreateRecordSet","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Domain ID (UUID)","name":"domain_id","in":"path","required":true},{"description":"Record set payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateRecordSetRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.RecordSetResponse"}},"400":{"description":"validation error","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"domain not found","schema":{"type":"string"}}}}},"/dns/domains/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"Get a domain (org scoped)","operationId":"GetDomain","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Domain ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.DomainResponse"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"Delete a domain","operationId":"DeleteDomain","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Domain ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content"},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"Update a domain (org scoped)","operationId":"UpdateDomain","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Domain ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateDomainRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.DomainResponse"}},"400":{"description":"validation error","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/dns/records/{id}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"Delete a record set (API removes row; worker can optionally handle external deletion policy)","operationId":"DeleteRecordSet","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Record Set ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content"},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"Update a record set (flips to pending for reconciliation)","operationId":"UpdateRecordSet","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Record Set ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateRecordSetRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.RecordSetResponse"}},"400":{"description":"validation error","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/healthz":{"get":{"description":"Returns 200 OK when the service is up","consumes":["application/json"],"produces":["application/json"],"tags":["Health"],"summary":"Basic health check","operationId":"HealthCheck // operationId","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.HealthStatus"}}}}},"/labels":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns node labels for the organization in X-Org-ID. Filters: ` + "`" + `key` + "`" + `, ` + "`" + `value` + "`" + `, and ` + "`" + `q` + "`" + ` (key contains). Add ` + "`" + `include=node_pools` + "`" + ` to include linked node groups.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"List node labels (org scoped)","operationId":"ListLabels","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Exact key","name":"key","in":"query"},{"type":"string","description":"Exact value","name":"value","in":"query"},{"type":"string","description":"Key contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.LabelResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list node taints","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a label.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"Create label (org scoped)","operationId":"CreateLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Label payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateLabelRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.LabelResponse"}},"400":{"description":"invalid json / missing fields / invalid node_pool_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/labels/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns one label.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"Get label by ID (org scoped)","operationId":"GetLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Label ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.LabelResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the label.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"Delete label (org scoped)","operationId":"DeleteLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Label ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update label fields.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"Update label (org scoped)","operationId":"UpdateLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Label ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateLabelRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.LabelResponse"}},"400":{"description":"invalid id / invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/me":{"get":{"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"produces":["application/json"],"tags":["Me"],"summary":"Get current user profile","operationId":"GetMe","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.meResponse"}}}},"patch":{"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Me"],"summary":"Update current user profile","operationId":"UpdateMe","parameters":[{"description":"Patch profile","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.updateMeRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/models.User"}}}}},"/me/api-keys":{"get":{"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"produces":["application/json"],"tags":["MeAPIKeys"],"summary":"List my API keys","operationId":"ListUserAPIKeys","responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/handlers.userAPIKeyOut"}}}}},"post":{"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"description":"Returns the plaintext key once. Store it securely on the client side.","consumes":["application/json"],"produces":["application/json"],"tags":["MeAPIKeys"],"summary":"Create a new user API key","operationId":"CreateUserAPIKey","parameters":[{"description":"Key options","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.createUserKeyRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/handlers.userAPIKeyOut"}}}}},"/me/api-keys/{id}":{"delete":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["MeAPIKeys"],"summary":"Delete a user API key","operationId":"DeleteUserAPIKey","parameters":[{"type":"string","description":"Key ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content"}}}},"/node-pools":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns node pools for the organization in X-Org-ID.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List node pools (org scoped)","operationId":"ListNodePools","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Name contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.NodePoolResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list node pools","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a node pool. Optionally attach initial servers.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Create node pool (org scoped)","operationId":"CreateNodePool","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"NodePool payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateNodePoolRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.NodePoolResponse"}},"400":{"description":"invalid json / missing fields / invalid server_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/node-pools/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns one node pool. Add ` + "`" + `include=servers` + "`" + ` to include servers.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Get node pool by ID (org scoped)","operationId":"GetNodePool","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.NodePoolResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the node pool.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Delete node pool (org scoped)","operationId":"DeleteNodePool","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update node pool fields.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Update node pool (org scoped)","operationId":"UpdateNodePool","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateNodePoolRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.NodePoolResponse"}},"400":{"description":"invalid id / invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/node-pools/{id}/annotations":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List annotations attached to a node pool (org scoped)","operationId":"ListNodePoolAnnotations","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.AnnotationResponse"}}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Attach annotation to a node pool (org scoped)","operationId":"AttachNodePoolAnnotations","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Group ID (UUID)","name":"id","in":"path","required":true},{"description":"Annotation IDs to attach","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.AttachAnnotationsRequest"}}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid server_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"attach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/annotations/{annotationId}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Detach one annotation from a node pool (org scoped)","operationId":"DetachNodePoolAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Annotation ID (UUID)","name":"annotationId","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"detach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/labels":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List labels attached to a node pool (org scoped)","operationId":"ListNodePoolLabels","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Label Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.LabelResponse"}}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Attach labels to a node pool (org scoped)","operationId":"AttachNodePoolLabels","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"description":"Label IDs to attach","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.AttachLabelsRequest"}}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid server_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"attach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/labels/{labelId}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Detach one label from a node pool (org scoped)","operationId":"DetachNodePoolLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Label ID (UUID)","name":"labelId","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"detach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/servers":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List servers attached to a node pool (org scoped)","operationId":"ListNodePoolServers","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.ServerResponse"}}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Attach servers to a node pool (org scoped)","operationId":"AttachNodePoolServers","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"description":"Server IDs to attach","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.AttachServersRequest"}}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid server_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"attach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/servers/{serverId}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Detach one server from a node pool (org scoped)","operationId":"DetachNodePoolServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Server ID (UUID)","name":"serverId","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"detach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/taints":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List taints attached to a node pool (org scoped)","operationId":"ListNodePoolTaints","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.TaintResponse"}}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Attach taints to a node pool (org scoped)","operationId":"AttachNodePoolTaints","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"description":"Taint IDs to attach","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.AttachTaintsRequest"}}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid taint_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"attach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/taints/{taintId}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Detach one taint from a node pool (org scoped)","operationId":"DetachNodePoolTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Taint ID (UUID)","name":"taintId","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"detach failed","schema":{"type":"string"}}}}},"/orgs":{"get":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"List organizations I belong to","operationId":"listMyOrgs","responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/models.Organization"}}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"post":{"security":[{"BearerAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Orgs"],"summary":"Create organization","operationId":"createOrg","parameters":[{"description":"Org payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.orgCreateReq"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/models.Organization"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"409":{"description":"Conflict","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}":{"get":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"Get organization","operationId":"getOrg","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/models.Organization"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"delete":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"Delete organization (owner)","operationId":"deleteOrg","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"Deleted"},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"patch":{"security":[{"BearerAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Orgs"],"summary":"Update organization (owner/admin)","operationId":"updateOrg","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"description":"Update payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.orgUpdateReq"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/models.Organization"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}/api-keys":{"get":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"List org-scoped API keys (no secrets)","operationId":"listOrgKeys","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/models.APIKey"}}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"post":{"security":[{"BearerAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Orgs"],"summary":"Create org key/secret pair (owner/admin)","operationId":"createOrgKey","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"description":"Key name + optional expiry","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.orgKeyCreateReq"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/handlers.orgKeyCreateResp"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}/api-keys/{key_id}":{"delete":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"Delete org key (owner/admin)","operationId":"deleteOrgKey","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Key ID (UUID)","name":"key_id","in":"path","required":true}],"responses":{"204":{"description":"Deleted"},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}/members":{"get":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"List members in org","operationId":"listMembers","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/handlers.memberOut"}}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"post":{"security":[{"BearerAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Orgs"],"summary":"Add or update a member (owner/admin)","operationId":"addOrUpdateMember","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"description":"User \u0026 role","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.memberUpsertReq"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.memberOut"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}/members/{user_id}":{"delete":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"Remove a member (owner/admin)","operationId":"removeMember","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"User ID (UUID)","name":"user_id","in":"path","required":true}],"responses":{"204":{"description":"Removed"},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/servers":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns servers for the organization in X-Org-ID. Optional filters: status, role.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"List servers (org scoped)","operationId":"ListServers","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Filter by status (pending|provisioning|ready|failed)","name":"status","in":"query"},{"type":"string","description":"Filter by role","name":"role","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.ServerResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list servers","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a server bound to the org in X-Org-ID. Validates that ssh_key_id belongs to the org.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Create server (org scoped)","operationId":"CreateServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Server payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateServerRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.ServerResponse"}},"400":{"description":"invalid json / missing fields / invalid status / invalid ssh_key_id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/servers/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns one server in the given organization.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Get server by ID (org scoped)","operationId":"GetServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Server ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.ServerResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the server.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Delete server (org scoped)","operationId":"DeleteServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Server ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update fields; changing ssh_key_id validates ownership.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Update server (org scoped)","operationId":"UpdateServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Server ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateServerRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.ServerResponse"}},"400":{"description":"invalid id / invalid json / invalid status / invalid ssh_key_id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/servers/{id}/reset-hostkey":{"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Clears the stored SSH host key for this server. The next SSH connection will re-learn the host key (trust-on-first-use).","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Reset SSH host key (org scoped)","operationId":"ResetServerHostKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Server ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.ServerResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"reset failed","schema":{"type":"string"}}}}},"/ssh":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns ssh keys for the organization in X-Org-ID.","consumes":["application/json"],"produces":["application/json"],"tags":["Ssh"],"summary":"List ssh keys (org scoped)","operationId":"ListPublicSshKeys","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.SshResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list keys","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Generates an RSA or ED25519 keypair, saves it, and returns metadata. For RSA you may set bits (2048/3072/4096). Default is 4096. ED25519 ignores bits.","consumes":["application/json"],"produces":["application/json"],"tags":["Ssh"],"summary":"Create ssh keypair (org scoped)","operationId":"CreateSSHKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Key generation options","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateSSHRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.SshResponse"}},"400":{"description":"invalid json / invalid bits","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"generation/create failed","schema":{"type":"string"}}}}},"/ssh/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns public key fields. Append ` + "`" + `?reveal=true` + "`" + ` to include the private key PEM.","consumes":["application/json"],"produces":["application/json"],"tags":["Ssh"],"summary":"Get ssh key by ID (org scoped)","operationId":"GetSSHKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"SSH Key ID (UUID)","name":"id","in":"path","required":true},{"type":"boolean","description":"Reveal private key PEM","name":"reveal","in":"query"}],"responses":{"200":{"description":"When reveal=true","schema":{"$ref":"#/definitions/dto.SshRevealResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes a keypair.","consumes":["application/json"],"produces":["application/json"],"tags":["Ssh"],"summary":"Delete ssh keypair (org scoped)","operationId":"DeleteSSHKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"SSH Key ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}}},"/ssh/{id}/download":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Download ` + "`" + `part=public|private|both` + "`" + ` of the keypair. ` + "`" + `both` + "`" + ` returns a zip file.","produces":["application/json"],"tags":["Ssh"],"summary":"Download ssh key files by ID (org scoped)","operationId":"DownloadSSHKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header","required":true},{"type":"string","description":"SSH Key ID (UUID)","name":"id","in":"path","required":true},{"enum":["public","private","both"],"type":"string","description":"Which part to download","name":"part","in":"query","required":true}],"responses":{"200":{"description":"file content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid part","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"download failed","schema":{"type":"string"}}}}},"/taints":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns node taints for the organization in X-Org-ID. Filters: ` + "`" + `key` + "`" + `, ` + "`" + `value` + "`" + `, and ` + "`" + `q` + "`" + ` (key contains). Add ` + "`" + `include=node_pools` + "`" + ` to include linked node pools.","consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"List node pool taints (org scoped)","operationId":"ListTaints","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Exact key","name":"key","in":"query"},{"type":"string","description":"Exact value","name":"value","in":"query"},{"type":"string","description":"key contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.TaintResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list node taints","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a taint.","consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"Create node taint (org scoped)","operationId":"CreateTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Taint payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateTaintRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.TaintResponse"}},"400":{"description":"invalid json / missing fields / invalid node_pool_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/taints/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"Get node taint by ID (org scoped)","operationId":"GetTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Taint ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.TaintResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the taint.","consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"Delete taint (org scoped)","operationId":"DeleteTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Taint ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update taint fields.","consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"Update node taint (org scoped)","operationId":"UpdateTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Taint ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateTaintRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.TaintResponse"}},"400":{"description":"invalid id / invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/version":{"get":{"description":"Returns build/runtime metadata for the running service.","consumes":["application/json"],"produces":["application/json"],"tags":["Meta"],"summary":"Service version information","operationId":"Version // operationId","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.VersionResponse"}}}}}},"definitions":{"dto.AnnotationResponse":{"type":"object","properties":{"created_at":{"type":"string"},"id":{"type":"string"},"key":{"type":"string"},"organization_id":{"type":"string"},"updated_at":{"type":"string"},"value":{"type":"string"}}},"dto.AttachAnnotationsRequest":{"type":"object","properties":{"annotation_ids":{"type":"array","items":{"type":"string"}}}},"dto.AttachLabelsRequest":{"type":"object","properties":{"label_ids":{"type":"array","items":{"type":"string"}}}},"dto.AttachServersRequest":{"type":"object","properties":{"server_ids":{"type":"array","items":{"type":"string"}}}},"dto.AttachTaintsRequest":{"type":"object","properties":{"taint_ids":{"type":"array","items":{"type":"string"}}}},"dto.AuthStartResponse":{"type":"object","properties":{"auth_url":{"type":"string","example":"https://accounts.google.com/o/oauth2/v2/auth?client_id=..."}}},"dto.ClusterResponse":{"type":"object","properties":{"bastion_server":{"$ref":"#/definitions/dto.ServerResponse"},"captain_domain":{"type":"string"},"certificate_key":{"type":"string"},"cluster_load_balancer":{"type":"string"},"control_load_balancer":{"type":"string"},"created_at":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"node_pools":{"type":"array","items":{"$ref":"#/definitions/dto.NodePoolResponse"}},"provider":{"type":"string"},"random_token":{"type":"string"},"region":{"type":"string"},"status":{"type":"string"},"updated_at":{"type":"string"}}},"dto.CreateAnnotationRequest":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"dto.CreateClusterRequest":{"type":"object","properties":{"captain_domain":{"type":"string"},"cluster_load_balancer":{"type":"string"},"control_load_balancer":{"type":"string"},"name":{"type":"string"},"provider":{"type":"string"},"region":{"type":"string"},"status":{"type":"string"}}},"dto.CreateCredentialRequest":{"type":"object","required":["kind","provider","schema_version","scope","scope_kind","scope_version","secret"],"properties":{"account_id":{"type":"string","maxLength":32},"kind":{"description":"aws_access_key, api_token, basic_auth, oauth2","type":"string"},"name":{"description":"human label","type":"string","maxLength":100},"provider":{"type":"string","enum":["aws","cloudflare","hetzner","digitalocean","generic"]},"region":{"type":"string","maxLength":32},"schema_version":{"description":"secret schema version","type":"integer","minimum":1},"scope":{"description":"{\"service\":\"route53\"} or {\"arn\":\"...\"}","type":"object"},"scope_kind":{"type":"string","enum":["provider","service","resource"]},"scope_version":{"description":"scope schema version","type":"integer","minimum":1},"secret":{"description":"encrypted later","type":"object"}}},"dto.CreateDomainRequest":{"type":"object","required":["credential_id","domain_name"],"properties":{"credential_id":{"type":"string"},"domain_name":{"type":"string"},"zone_id":{"type":"string","maxLength":128}}},"dto.CreateLabelRequest":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"dto.CreateNodePoolRequest":{"type":"object","properties":{"name":{"type":"string"},"role":{"type":"string","enum":["master","worker"]}}},"dto.CreateRecordSetRequest":{"type":"object","required":["name","type"],"properties":{"name":{"description":"Name relative to domain (\"endpoint\") OR FQDN (\"endpoint.example.com\").\nServer normalizes to relative.","type":"string","maxLength":253},"ttl":{"type":"integer","maximum":86400,"minimum":1},"type":{"type":"string"},"values":{"type":"array","items":{"type":"string"}}}},"dto.CreateSSHRequest":{"type":"object","properties":{"bits":{"description":"Only for RSA","type":"integer"},"comment":{"type":"string","example":"deploy@autoglue"},"name":{"type":"string"},"type":{"description":"\"rsa\" (default) or \"ed25519\"","type":"string"}}},"dto.CreateServerRequest":{"type":"object","properties":{"hostname":{"type":"string"},"private_ip_address":{"type":"string"},"public_ip_address":{"type":"string"},"role":{"type":"string","enum":["master","worker","bastion"],"example":"master|worker|bastion"},"ssh_key_id":{"type":"string"},"ssh_user":{"type":"string"},"status":{"type":"string","enum":["pending","provisioning","ready","failed"],"example":"pending|provisioning|ready|failed"}}},"dto.CreateTaintRequest":{"type":"object","properties":{"effect":{"type":"string"},"key":{"type":"string"},"value":{"type":"string"}}},"dto.CredentialOut":{"type":"object","properties":{"account_id":{"type":"string"},"created_at":{"type":"string"},"id":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"provider":{"type":"string"},"region":{"type":"string"},"schema_version":{"type":"integer"},"scope":{"type":"object"},"scope_kind":{"type":"string"},"scope_version":{"type":"integer"},"updated_at":{"type":"string"}}},"dto.DomainResponse":{"type":"object","properties":{"created_at":{"type":"string"},"credential_id":{"type":"string"},"domain_name":{"type":"string"},"id":{"type":"string"},"last_error":{"type":"string"},"organization_id":{"type":"string"},"status":{"type":"string"},"updated_at":{"type":"string"},"zone_id":{"type":"string"}}},"dto.EnqueueRequest":{"type":"object","properties":{"payload":{"type":"object"},"queue":{"type":"string","example":"default"},"run_at":{"type":"string","example":"2025-11-05T08:00:00Z"},"type":{"type":"string","example":"email.send"}}},"dto.JWK":{"type":"object","properties":{"alg":{"type":"string","example":"RS256"},"e":{"type":"string","example":"AQAB"},"kid":{"type":"string","example":"7c6f1d0a-7a98-4e6a-9dbf-6b1af4b9f345"},"kty":{"type":"string","example":"RSA"},"n":{"type":"string"},"use":{"type":"string","example":"sig"},"x":{"type":"string"}}},"dto.JWKS":{"type":"object","properties":{"keys":{"type":"array","items":{"$ref":"#/definitions/dto.JWK"}}}},"dto.Job":{"type":"object","properties":{"attempts":{"type":"integer","example":0},"created_at":{"type":"string","example":"2025-11-04T09:30:00Z"},"id":{"type":"string","example":"01HF7SZK8Z8WG1M3J7S2Z8M2N6"},"last_error":{"type":"string","example":"error message"},"max_attempts":{"type":"integer","example":3},"payload":{},"queue":{"type":"string","example":"default"},"run_at":{"type":"string","example":"2025-11-04T09:30:00Z"},"status":{"enum":["queued|running|succeeded|failed|canceled|retrying|scheduled"],"allOf":[{"$ref":"#/definitions/dto.JobStatus"}],"example":"queued"},"type":{"type":"string","example":"email.send"},"updated_at":{"type":"string","example":"2025-11-04T09:30:00Z"}}},"dto.JobStatus":{"type":"string","enum":["queued","running","succeeded","failed","canceled","retrying","scheduled"],"x-enum-varnames":["StatusQueued","StatusRunning","StatusSucceeded","StatusFailed","StatusCanceled","StatusRetrying","StatusScheduled"]},"dto.LabelResponse":{"type":"object","properties":{"created_at":{"type":"string"},"id":{"type":"string"},"key":{"type":"string"},"organization_id":{"type":"string"},"updated_at":{"type":"string"},"value":{"type":"string"}}},"dto.LogoutRequest":{"type":"object","properties":{"refresh_token":{"type":"string","example":"m0l9o8rT3t0V8d3eFf..."}}},"dto.NodePoolResponse":{"type":"object","properties":{"annotations":{"type":"array","items":{"$ref":"#/definitions/dto.AnnotationResponse"}},"created_at":{"type":"string"},"id":{"type":"string"},"labels":{"type":"array","items":{"$ref":"#/definitions/dto.LabelResponse"}},"name":{"type":"string"},"organization_id":{"type":"string"},"role":{"type":"string","enum":["master","worker"]},"servers":{"type":"array","items":{"$ref":"#/definitions/dto.ServerResponse"}},"taints":{"type":"array","items":{"$ref":"#/definitions/dto.TaintResponse"}},"updated_at":{"type":"string"}}},"dto.PageJob":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/definitions/dto.Job"}},"page":{"type":"integer","example":1},"page_size":{"type":"integer","example":25},"total":{"type":"integer","example":120}}},"dto.QueueInfo":{"type":"object","properties":{"failed":{"type":"integer","example":5},"name":{"type":"string","example":"default"},"pending":{"type":"integer","example":42},"running":{"type":"integer","example":3},"scheduled":{"type":"integer","example":7}}},"dto.RecordSetResponse":{"type":"object","properties":{"created_at":{"type":"string"},"domain_id":{"type":"string"},"fingerprint":{"type":"string"},"id":{"type":"string"},"last_error":{"type":"string"},"name":{"type":"string"},"owner":{"type":"string"},"status":{"type":"string"},"ttl":{"type":"integer"},"type":{"type":"string"},"updated_at":{"type":"string"},"values":{"description":"[]string JSON","type":"object"}}},"dto.RefreshRequest":{"type":"object","properties":{"refresh_token":{"type":"string","example":"m0l9o8rT3t0V8d3eFf..."}}},"dto.ServerResponse":{"type":"object","properties":{"created_at":{"type":"string"},"hostname":{"type":"string"},"id":{"type":"string"},"organization_id":{"type":"string"},"private_ip_address":{"type":"string"},"public_ip_address":{"type":"string"},"role":{"type":"string","enum":["master","worker","bastion"],"example":"master|worker|bastion"},"ssh_key_id":{"type":"string"},"ssh_user":{"type":"string"},"status":{"type":"string","enum":["pending","provisioning","ready","failed"],"example":"pending|provisioning|ready|failed"},"updated_at":{"type":"string"}}},"dto.SshResponse":{"type":"object","properties":{"created_at":{"type":"string"},"fingerprint":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"organization_id":{"type":"string"},"public_key":{"type":"string"},"updated_at":{"type":"string"}}},"dto.SshRevealResponse":{"type":"object","properties":{"created_at":{"type":"string"},"fingerprint":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"organization_id":{"type":"string"},"private_key":{"type":"string"},"public_key":{"type":"string"},"updated_at":{"type":"string"}}},"dto.TaintResponse":{"type":"object","properties":{"created_at":{"type":"string"},"effect":{"type":"string"},"id":{"type":"string"},"key":{"type":"string"},"organization_id":{"type":"string"},"updated_at":{"type":"string"},"value":{"type":"string"}}},"dto.TokenPair":{"type":"object","properties":{"access_token":{"type":"string","example":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ij..."},"expires_in":{"type":"integer","example":3600},"refresh_token":{"type":"string","example":"m0l9o8rT3t0V8d3eFf...."},"token_type":{"type":"string","example":"Bearer"}}},"dto.UpdateAnnotationRequest":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"dto.UpdateCredentialRequest":{"type":"object","properties":{"account_id":{"type":"string"},"name":{"type":"string"},"region":{"type":"string"},"scope":{"type":"object"},"scope_kind":{"type":"string"},"scope_version":{"type":"integer"},"secret":{"description":"set if rotating","type":"object"}}},"dto.UpdateDomainRequest":{"type":"object","properties":{"credential_id":{"type":"string"},"domain_name":{"type":"string"},"status":{"type":"string","enum":["pending","provisioning","ready","failed"]},"zone_id":{"type":"string","maxLength":128}}},"dto.UpdateLabelRequest":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"dto.UpdateNodePoolRequest":{"type":"object","properties":{"name":{"type":"string"},"role":{"type":"string","enum":["master","worker"]}}},"dto.UpdateRecordSetRequest":{"type":"object","properties":{"name":{"description":"Any change flips status back to pending (worker will UPSERT)","type":"string","maxLength":253},"status":{"type":"string","enum":["pending","provisioning","ready","failed"]},"ttl":{"type":"integer","maximum":86400,"minimum":1},"type":{"type":"string"},"values":{"type":"array","items":{"type":"string"}}}},"dto.UpdateServerRequest":{"type":"object","properties":{"hostname":{"type":"string"},"private_ip_address":{"type":"string"},"public_ip_address":{"type":"string"},"role":{"type":"string","enum":["master","worker","bastion"],"example":"master|worker|bastion"},"ssh_key_id":{"type":"string"},"ssh_user":{"type":"string"},"status":{"type":"string","enum":["pending","provisioning","ready","failed"],"example":"pending|provisioning|ready|failed"}}},"dto.UpdateTaintRequest":{"type":"object","properties":{"effect":{"type":"string"},"key":{"type":"string"},"value":{"type":"string"}}},"handlers.HealthStatus":{"type":"object","properties":{"status":{"type":"string","example":"ok"}}},"handlers.VersionResponse":{"type":"object","properties":{"built":{"type":"string","example":"2025-11-08T12:34:56Z"},"builtBy":{"type":"string","example":"ci"},"commit":{"type":"string","example":"a1b2c3d"},"commitTime":{"type":"string","example":"2025-11-08T12:31:00Z"},"go":{"type":"string","example":"go1.23.3"},"goArch":{"type":"string","example":"amd64"},"goOS":{"type":"string","example":"linux"},"modified":{"type":"boolean","example":false},"revision":{"type":"string","example":"a1b2c3d4e5f6abcdef"},"vcs":{"type":"string","example":"git"},"version":{"type":"string","example":"1.4.2"}}},"handlers.createUserKeyRequest":{"type":"object","properties":{"expires_in_hours":{"description":"optional TTL","type":"integer"},"name":{"type":"string"}}},"handlers.meResponse":{"type":"object","properties":{"avatar_url":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"display_name":{"type":"string"},"emails":{"type":"array","items":{"$ref":"#/definitions/models.UserEmail"}},"id":{"description":"example: 3fa85f64-5717-4562-b3fc-2c963f66afa6","type":"string","format":"uuid"},"is_admin":{"type":"boolean"},"is_disabled":{"type":"boolean"},"organizations":{"type":"array","items":{"$ref":"#/definitions/models.Organization"}},"primary_email":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"handlers.memberOut":{"type":"object","properties":{"email":{"type":"string"},"role":{"description":"owner/admin/member","type":"string"},"user_id":{"type":"string","format":"uuid"}}},"handlers.memberUpsertReq":{"type":"object","properties":{"role":{"type":"string","example":"member"},"user_id":{"type":"string","format":"uuid"}}},"handlers.orgCreateReq":{"type":"object","properties":{"domain":{"type":"string","example":"acme.com"},"name":{"type":"string","example":"Acme Corp"}}},"handlers.orgKeyCreateReq":{"type":"object","properties":{"expires_in_hours":{"type":"integer","example":720},"name":{"type":"string","example":"automation-bot"}}},"handlers.orgKeyCreateResp":{"type":"object","properties":{"created_at":{"type":"string"},"expires_at":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"org_key":{"description":"shown once:","type":"string"},"org_secret":{"description":"shown once:","type":"string"},"scope":{"description":"\"org\"","type":"string"}}},"handlers.orgUpdateReq":{"type":"object","properties":{"domain":{"type":"string"},"name":{"type":"string"}}},"handlers.updateMeRequest":{"type":"object","properties":{"display_name":{"type":"string"}}},"handlers.userAPIKeyOut":{"type":"object","properties":{"created_at":{"type":"string"},"expires_at":{"type":"string"},"id":{"type":"string","format":"uuid"},"last_used_at":{"type":"string"},"name":{"type":"string"},"plain":{"description":"Shown only on create:","type":"string"},"scope":{"description":"\"user\"","type":"string"}}},"models.APIKey":{"type":"object","properties":{"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"last_used_at":{"type":"string","format":"date-time"},"name":{"type":"string"},"org_id":{"type":"string","format":"uuid"},"prefix":{"type":"string"},"revoked":{"type":"boolean"},"scope":{"type":"string"},"updated_at":{"type":"string","format":"date-time"},"user_id":{"type":"string","format":"uuid"}}},"models.Organization":{"type":"object","properties":{"created_at":{"type":"string","format":"date-time"},"domain":{"type":"string"},"id":{"description":"example: 3fa85f64-5717-4562-b3fc-2c963f66afa6","type":"string","format":"uuid"},"name":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"models.User":{"type":"object","properties":{"avatar_url":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"display_name":{"type":"string"},"id":{"description":"example: 3fa85f64-5717-4562-b3fc-2c963f66afa6","type":"string","format":"uuid"},"is_admin":{"type":"boolean"},"is_disabled":{"type":"boolean"},"primary_email":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"models.UserEmail":{"type":"object","properties":{"created_at":{"type":"string","format":"date-time"},"email":{"type":"string"},"id":{"description":"example: 3fa85f64-5717-4562-b3fc-2c963f66afa6","type":"string","format":"uuid"},"is_primary":{"type":"boolean"},"is_verified":{"type":"boolean"},"updated_at":{"type":"string","format":"date-time"},"user":{"$ref":"#/definitions/models.User"},"user_id":{"type":"string","format":"uuid"}}},"utils.ErrorResponse":{"type":"object","properties":{"code":{"description":"A machine-readable error code, e.g. \"validation_error\"\nexample: validation_error","type":"string"},"message":{"description":"Human-readable message\nexample: slug is required","type":"string"}}}},"securityDefinitions":{"ApiKeyAuth":{"description":"User API key","type":"apiKey","name":"X-API-KEY","in":"header"},"BearerAuth":{"description":"Bearer token authentication","type":"apiKey","name":"Authorization","in":"header"},"OrgKeyAuth":{"description":"Org-level key/secret authentication","type":"apiKey","name":"X-ORG-KEY","in":"header"},"OrgSecretAuth":{"description":"Org-level secret","type":"apiKey","name":"X-ORG-SECRET","in":"header"}}}` // SwaggerInfo holds exported Swagger Info so clients can modify it var SwaggerInfo = &swag.Spec{ diff --git a/docs/swagger.json b/docs/swagger.json index 3e53bef..bce8979 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1 +1 @@ -{"schemes":["http","https"],"swagger":"2.0","info":{"description":"API for managing K3s clusters across cloud providers","title":"AutoGlue API","contact":{"name":"GlueOps"},"version":"1.0"},"basePath":"/api/v1","paths":{"/.well-known/jwks.json":{"get":{"description":"Returns the JSON Web Key Set for token verification","produces":["application/json"],"tags":["Auth"],"summary":"Get JWKS","operationId":"getJWKS","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.JWKS"}}}}},"/admin/archer/jobs":{"get":{"security":[{"BearerAuth":[]}],"description":"Paginated background jobs with optional filters. Search `q` may match id, type, error, payload (implementation-dependent).","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"List Archer jobs (admin)","operationId":"AdminListArcherJobs","parameters":[{"enum":["queued","running","succeeded","failed","canceled","retrying","scheduled"],"type":"string","description":"Filter by status","name":"status","in":"query"},{"type":"string","description":"Filter by queue name / worker name","name":"queue","in":"query"},{"type":"string","description":"Free-text search","name":"q","in":"query"},{"type":"integer","default":1,"description":"Page number","name":"page","in":"query"},{"maximum":100,"minimum":1,"type":"integer","default":25,"description":"Items per page","name":"page_size","in":"query"}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.PageJob"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"500":{"description":"internal error","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]}],"description":"Create a job immediately or schedule it for the future via `run_at`.","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"Enqueue a new Archer job (admin)","operationId":"AdminEnqueueArcherJob","parameters":[{"description":"Job parameters","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.EnqueueRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.Job"}},"400":{"description":"invalid json or missing fields","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"500":{"description":"internal error","schema":{"type":"string"}}}}},"/admin/archer/jobs/{id}/cancel":{"post":{"security":[{"BearerAuth":[]}],"description":"Set job status to canceled if cancellable. For running jobs, this only affects future picks; wire to Archer if you need active kill.","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"Cancel an Archer job (admin)","operationId":"AdminCancelArcherJob","parameters":[{"type":"string","description":"Job ID","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.Job"}},"400":{"description":"invalid job or not cancellable","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/admin/archer/jobs/{id}/retry":{"post":{"security":[{"BearerAuth":[]}],"description":"Marks the job retriable (DB flip). Swap this for an Archer admin call if you expose one.","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"Retry a failed/canceled Archer job (admin)","operationId":"AdminRetryArcherJob","parameters":[{"type":"string","description":"Job ID","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.Job"}},"400":{"description":"invalid job or not eligible","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/admin/archer/queues":{"get":{"security":[{"BearerAuth":[]}],"description":"Summary metrics per queue (pending, running, failed, scheduled).","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"List Archer queues (admin)","operationId":"AdminListArcherQueues","responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.QueueInfo"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"500":{"description":"internal error","schema":{"type":"string"}}}}},"/annotations":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns annotations for the organization in X-Org-ID. Filters: `key`, `value`, and `q` (key contains). Add `include=node_pools` to include linked node pools.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"List annotations (org scoped)","operationId":"ListAnnotations","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Exact key","name":"key","in":"query"},{"type":"string","description":"Exact value","name":"value","in":"query"},{"type":"string","description":"key contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.AnnotationResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list annotations","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates an annotation.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"Create annotation (org scoped)","operationId":"CreateAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Annotation payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateAnnotationRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.AnnotationResponse"}},"400":{"description":"invalid json / missing fields","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/annotations/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns one annotation. Add `include=node_pools` to include node pools.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"Get annotation by ID (org scoped)","operationId":"GetAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Annotation ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.AnnotationResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the annotation.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"Delete annotation (org scoped)","operationId":"DeleteAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Annotation ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update annotation fields.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"Update annotation (org scoped)","operationId":"UpdateAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Annotation ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateAnnotationRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.AnnotationResponse"}},"400":{"description":"invalid id / invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/auth/logout":{"post":{"consumes":["application/json"],"produces":["application/json"],"tags":["Auth"],"summary":"Revoke refresh token family (logout everywhere)","operationId":"Logout","parameters":[{"description":"Refresh token","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.LogoutRequest"}}],"responses":{"204":{"description":"No Content"}}}},"/auth/refresh":{"post":{"consumes":["application/json"],"produces":["application/json"],"tags":["Auth"],"summary":"Rotate refresh token","operationId":"Refresh","parameters":[{"description":"Refresh token","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.RefreshRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.TokenPair"}}}}},"/auth/{provider}/callback":{"get":{"produces":["application/json"],"tags":["Auth"],"summary":"Handle social login callback","operationId":"AuthCallback","parameters":[{"type":"string","description":"google|github","name":"provider","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.TokenPair"}}}}},"/auth/{provider}/start":{"post":{"description":"Returns provider authorization URL for the frontend to redirect","produces":["application/json"],"tags":["Auth"],"summary":"Begin social login","operationId":"AuthStart","parameters":[{"type":"string","description":"google|github","name":"provider","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.AuthStartResponse"}}}}},"/clusters":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns clusters for the organization in X-Org-ID. Filter by `q` (name contains).","produces":["application/json"],"tags":["Clusters"],"summary":"List clusters (org scoped)","operationId":"ListClusters","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Name contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.ClusterResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list clusters","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a cluster. If `kubeconfig` is provided, it will be encrypted per-organization and stored securely (never returned).","consumes":["application/json"],"produces":["application/json"],"tags":["Clusters"],"summary":"Create cluster (org scoped)","operationId":"CreateCluster","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateClusterRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.ClusterResponse"}},"400":{"description":"invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/credentials":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns credential metadata for the current org. Secrets are never returned.","consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"List credentials (metadata only)","operationId":"ListCredentials","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Filter by provider (e.g., aws)","name":"provider","in":"query"},{"type":"string","description":"Filter by kind (e.g., aws_access_key)","name":"kind","in":"query"},{"type":"string","description":"Filter by scope kind (provider/service/resource)","name":"scope_kind","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.CredentialOut"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"internal server error","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Create a credential (encrypts secret)","operationId":"CreateCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"description":"Credential payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateCredentialRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.CredentialOut"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"internal server error","schema":{"type":"string"}}}}},"/credentials/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Get credential by ID (metadata only)","operationId":"GetCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Credential ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.CredentialOut"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"internal server error","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Delete credential","operationId":"DeleteCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Credential ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content"},"404":{"description":"not found","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Update credential metadata and/or rotate secret","operationId":"UpdateCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Credential ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateCredentialRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.CredentialOut"}},"403":{"description":"X-Org-ID required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/credentials/{id}/reveal":{"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Reveal decrypted secret (one-time read)","operationId":"RevealCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Credential ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"object","additionalProperties":true}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/healthz":{"get":{"description":"Returns 200 OK when the service is up","consumes":["application/json"],"produces":["application/json"],"tags":["Health"],"summary":"Basic health check","operationId":"HealthCheck // operationId","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.HealthStatus"}}}}},"/labels":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns node labels for the organization in X-Org-ID. Filters: `key`, `value`, and `q` (key contains). Add `include=node_pools` to include linked node groups.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"List node labels (org scoped)","operationId":"ListLabels","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Exact key","name":"key","in":"query"},{"type":"string","description":"Exact value","name":"value","in":"query"},{"type":"string","description":"Key contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.LabelResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list node taints","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a label.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"Create label (org scoped)","operationId":"CreateLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Label payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateLabelRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.LabelResponse"}},"400":{"description":"invalid json / missing fields / invalid node_pool_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/labels/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns one label.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"Get label by ID (org scoped)","operationId":"GetLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Label ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.LabelResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the label.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"Delete label (org scoped)","operationId":"DeleteLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Label ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update label fields.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"Update label (org scoped)","operationId":"UpdateLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Label ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateLabelRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.LabelResponse"}},"400":{"description":"invalid id / invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/me":{"get":{"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"produces":["application/json"],"tags":["Me"],"summary":"Get current user profile","operationId":"GetMe","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.meResponse"}}}},"patch":{"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Me"],"summary":"Update current user profile","operationId":"UpdateMe","parameters":[{"description":"Patch profile","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.updateMeRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/models.User"}}}}},"/me/api-keys":{"get":{"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"produces":["application/json"],"tags":["MeAPIKeys"],"summary":"List my API keys","operationId":"ListUserAPIKeys","responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/handlers.userAPIKeyOut"}}}}},"post":{"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"description":"Returns the plaintext key once. Store it securely on the client side.","consumes":["application/json"],"produces":["application/json"],"tags":["MeAPIKeys"],"summary":"Create a new user API key","operationId":"CreateUserAPIKey","parameters":[{"description":"Key options","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.createUserKeyRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/handlers.userAPIKeyOut"}}}}},"/me/api-keys/{id}":{"delete":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["MeAPIKeys"],"summary":"Delete a user API key","operationId":"DeleteUserAPIKey","parameters":[{"type":"string","description":"Key ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content"}}}},"/node-pools":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns node pools for the organization in X-Org-ID.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List node pools (org scoped)","operationId":"ListNodePools","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Name contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.NodePoolResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list node pools","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a node pool. Optionally attach initial servers.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Create node pool (org scoped)","operationId":"CreateNodePool","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"NodePool payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateNodePoolRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.NodePoolResponse"}},"400":{"description":"invalid json / missing fields / invalid server_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/node-pools/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns one node pool. Add `include=servers` to include servers.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Get node pool by ID (org scoped)","operationId":"GetNodePool","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.NodePoolResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the node pool.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Delete node pool (org scoped)","operationId":"DeleteNodePool","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update node pool fields.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Update node pool (org scoped)","operationId":"UpdateNodePool","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateNodePoolRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.NodePoolResponse"}},"400":{"description":"invalid id / invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/node-pools/{id}/annotations":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List annotations attached to a node pool (org scoped)","operationId":"ListNodePoolAnnotations","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.AnnotationResponse"}}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Attach annotation to a node pool (org scoped)","operationId":"AttachNodePoolAnnotations","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Group ID (UUID)","name":"id","in":"path","required":true},{"description":"Annotation IDs to attach","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.AttachAnnotationsRequest"}}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid server_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"attach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/annotations/{annotationId}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Detach one annotation from a node pool (org scoped)","operationId":"DetachNodePoolAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Annotation ID (UUID)","name":"annotationId","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"detach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/labels":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List labels attached to a node pool (org scoped)","operationId":"ListNodePoolLabels","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Label Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.LabelResponse"}}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Attach labels to a node pool (org scoped)","operationId":"AttachNodePoolLabels","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"description":"Label IDs to attach","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.AttachLabelsRequest"}}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid server_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"attach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/labels/{labelId}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Detach one label from a node pool (org scoped)","operationId":"DetachNodePoolLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Label ID (UUID)","name":"labelId","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"detach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/servers":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List servers attached to a node pool (org scoped)","operationId":"ListNodePoolServers","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.ServerResponse"}}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Attach servers to a node pool (org scoped)","operationId":"AttachNodePoolServers","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"description":"Server IDs to attach","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.AttachServersRequest"}}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid server_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"attach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/servers/{serverId}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Detach one server from a node pool (org scoped)","operationId":"DetachNodePoolServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Server ID (UUID)","name":"serverId","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"detach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/taints":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List taints attached to a node pool (org scoped)","operationId":"ListNodePoolTaints","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.TaintResponse"}}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Attach taints to a node pool (org scoped)","operationId":"AttachNodePoolTaints","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"description":"Taint IDs to attach","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.AttachTaintsRequest"}}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid taint_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"attach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/taints/{taintId}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Detach one taint from a node pool (org scoped)","operationId":"DetachNodePoolTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Taint ID (UUID)","name":"taintId","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"detach failed","schema":{"type":"string"}}}}},"/orgs":{"get":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"List organizations I belong to","operationId":"listMyOrgs","responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/models.Organization"}}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"post":{"security":[{"BearerAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Orgs"],"summary":"Create organization","operationId":"createOrg","parameters":[{"description":"Org payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.orgCreateReq"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/models.Organization"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"409":{"description":"Conflict","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}":{"get":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"Get organization","operationId":"getOrg","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/models.Organization"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"delete":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"Delete organization (owner)","operationId":"deleteOrg","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"Deleted"},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"patch":{"security":[{"BearerAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Orgs"],"summary":"Update organization (owner/admin)","operationId":"updateOrg","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"description":"Update payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.orgUpdateReq"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/models.Organization"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}/api-keys":{"get":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"List org-scoped API keys (no secrets)","operationId":"listOrgKeys","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/models.APIKey"}}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"post":{"security":[{"BearerAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Orgs"],"summary":"Create org key/secret pair (owner/admin)","operationId":"createOrgKey","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"description":"Key name + optional expiry","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.orgKeyCreateReq"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/handlers.orgKeyCreateResp"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}/api-keys/{key_id}":{"delete":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"Delete org key (owner/admin)","operationId":"deleteOrgKey","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Key ID (UUID)","name":"key_id","in":"path","required":true}],"responses":{"204":{"description":"Deleted"},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}/members":{"get":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"List members in org","operationId":"listMembers","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/handlers.memberOut"}}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"post":{"security":[{"BearerAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Orgs"],"summary":"Add or update a member (owner/admin)","operationId":"addOrUpdateMember","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"description":"User \u0026 role","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.memberUpsertReq"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.memberOut"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}/members/{user_id}":{"delete":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"Remove a member (owner/admin)","operationId":"removeMember","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"User ID (UUID)","name":"user_id","in":"path","required":true}],"responses":{"204":{"description":"Removed"},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/servers":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns servers for the organization in X-Org-ID. Optional filters: status, role.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"List servers (org scoped)","operationId":"ListServers","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Filter by status (pending|provisioning|ready|failed)","name":"status","in":"query"},{"type":"string","description":"Filter by role","name":"role","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.ServerResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list servers","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a server bound to the org in X-Org-ID. Validates that ssh_key_id belongs to the org.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Create server (org scoped)","operationId":"CreateServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Server payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateServerRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.ServerResponse"}},"400":{"description":"invalid json / missing fields / invalid status / invalid ssh_key_id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/servers/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns one server in the given organization.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Get server by ID (org scoped)","operationId":"GetServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Server ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.ServerResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the server.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Delete server (org scoped)","operationId":"DeleteServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Server ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update fields; changing ssh_key_id validates ownership.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Update server (org scoped)","operationId":"UpdateServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Server ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateServerRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.ServerResponse"}},"400":{"description":"invalid id / invalid json / invalid status / invalid ssh_key_id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/ssh":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns ssh keys for the organization in X-Org-ID.","consumes":["application/json"],"produces":["application/json"],"tags":["Ssh"],"summary":"List ssh keys (org scoped)","operationId":"ListPublicSshKeys","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.SshResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list keys","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Generates an RSA or ED25519 keypair, saves it, and returns metadata. For RSA you may set bits (2048/3072/4096). Default is 4096. ED25519 ignores bits.","consumes":["application/json"],"produces":["application/json"],"tags":["Ssh"],"summary":"Create ssh keypair (org scoped)","operationId":"CreateSSHKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Key generation options","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateSSHRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.SshResponse"}},"400":{"description":"invalid json / invalid bits","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"generation/create failed","schema":{"type":"string"}}}}},"/ssh/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns public key fields. Append `?reveal=true` to include the private key PEM.","consumes":["application/json"],"produces":["application/json"],"tags":["Ssh"],"summary":"Get ssh key by ID (org scoped)","operationId":"GetSSHKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"SSH Key ID (UUID)","name":"id","in":"path","required":true},{"type":"boolean","description":"Reveal private key PEM","name":"reveal","in":"query"}],"responses":{"200":{"description":"When reveal=true","schema":{"$ref":"#/definitions/dto.SshRevealResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes a keypair.","consumes":["application/json"],"produces":["application/json"],"tags":["Ssh"],"summary":"Delete ssh keypair (org scoped)","operationId":"DeleteSSHKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"SSH Key ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}}},"/ssh/{id}/download":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Download `part=public|private|both` of the keypair. `both` returns a zip file.","produces":["application/json"],"tags":["Ssh"],"summary":"Download ssh key files by ID (org scoped)","operationId":"DownloadSSHKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header","required":true},{"type":"string","description":"SSH Key ID (UUID)","name":"id","in":"path","required":true},{"enum":["public","private","both"],"type":"string","description":"Which part to download","name":"part","in":"query","required":true}],"responses":{"200":{"description":"file content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid part","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"download failed","schema":{"type":"string"}}}}},"/taints":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns node taints for the organization in X-Org-ID. Filters: `key`, `value`, and `q` (key contains). Add `include=node_pools` to include linked node pools.","consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"List node pool taints (org scoped)","operationId":"ListTaints","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Exact key","name":"key","in":"query"},{"type":"string","description":"Exact value","name":"value","in":"query"},{"type":"string","description":"key contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.TaintResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list node taints","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a taint.","consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"Create node taint (org scoped)","operationId":"CreateTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Taint payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateTaintRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.TaintResponse"}},"400":{"description":"invalid json / missing fields / invalid node_pool_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/taints/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"Get node taint by ID (org scoped)","operationId":"GetTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Taint ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.TaintResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the taint.","consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"Delete taint (org scoped)","operationId":"DeleteTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Taint ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update taint fields.","consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"Update node taint (org scoped)","operationId":"UpdateTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Taint ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateTaintRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.TaintResponse"}},"400":{"description":"invalid id / invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/version":{"get":{"description":"Returns build/runtime metadata for the running service.","consumes":["application/json"],"produces":["application/json"],"tags":["Meta"],"summary":"Service version information","operationId":"Version // operationId","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.VersionResponse"}}}}}},"definitions":{"dto.AnnotationResponse":{"type":"object","properties":{"created_at":{"type":"string"},"id":{"type":"string"},"key":{"type":"string"},"organization_id":{"type":"string"},"updated_at":{"type":"string"},"value":{"type":"string"}}},"dto.AttachAnnotationsRequest":{"type":"object","properties":{"annotation_ids":{"type":"array","items":{"type":"string"}}}},"dto.AttachLabelsRequest":{"type":"object","properties":{"label_ids":{"type":"array","items":{"type":"string"}}}},"dto.AttachServersRequest":{"type":"object","properties":{"server_ids":{"type":"array","items":{"type":"string"}}}},"dto.AttachTaintsRequest":{"type":"object","properties":{"taint_ids":{"type":"array","items":{"type":"string"}}}},"dto.AuthStartResponse":{"type":"object","properties":{"auth_url":{"type":"string","example":"https://accounts.google.com/o/oauth2/v2/auth?client_id=..."}}},"dto.ClusterResponse":{"type":"object","properties":{"bastion_server":{"$ref":"#/definitions/dto.ServerResponse"},"captain_domain":{"type":"string"},"certificate_key":{"type":"string"},"cluster_load_balancer":{"type":"string"},"control_load_balancer":{"type":"string"},"created_at":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"node_pools":{"type":"array","items":{"$ref":"#/definitions/dto.NodePoolResponse"}},"provider":{"type":"string"},"random_token":{"type":"string"},"region":{"type":"string"},"status":{"type":"string"},"updated_at":{"type":"string"}}},"dto.CreateAnnotationRequest":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"dto.CreateClusterRequest":{"type":"object","properties":{"captain_domain":{"type":"string"},"cluster_load_balancer":{"type":"string"},"control_load_balancer":{"type":"string"},"name":{"type":"string"},"provider":{"type":"string"},"region":{"type":"string"},"status":{"type":"string"}}},"dto.CreateCredentialRequest":{"type":"object","required":["kind","provider","schema_version","scope","scope_kind","scope_version","secret"],"properties":{"account_id":{"type":"string","maxLength":32},"kind":{"description":"aws_access_key, api_token, basic_auth, oauth2","type":"string"},"name":{"description":"human label","type":"string","maxLength":100},"provider":{"type":"string","enum":["aws","cloudflare","hetzner","digitalocean","generic"]},"region":{"type":"string","maxLength":32},"schema_version":{"description":"secret schema version","type":"integer","minimum":1},"scope":{"description":"{\"service\":\"route53\"} or {\"arn\":\"...\"}","type":"object"},"scope_kind":{"type":"string","enum":["provider","service","resource"]},"scope_version":{"description":"scope schema version","type":"integer","minimum":1},"secret":{"description":"encrypted later","type":"object"}}},"dto.CreateLabelRequest":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"dto.CreateNodePoolRequest":{"type":"object","properties":{"name":{"type":"string"},"role":{"type":"string","enum":["master","worker"]}}},"dto.CreateSSHRequest":{"type":"object","properties":{"bits":{"description":"Only for RSA","type":"integer"},"comment":{"type":"string","example":"deploy@autoglue"},"name":{"type":"string"},"type":{"description":"\"rsa\" (default) or \"ed25519\"","type":"string"}}},"dto.CreateServerRequest":{"type":"object","properties":{"hostname":{"type":"string"},"private_ip_address":{"type":"string"},"public_ip_address":{"type":"string"},"role":{"type":"string","enum":["master","worker","bastion"],"example":"master|worker|bastion"},"ssh_key_id":{"type":"string"},"ssh_user":{"type":"string"},"status":{"type":"string","enum":["pending","provisioning","ready","failed"],"example":"pending|provisioning|ready|failed"}}},"dto.CreateTaintRequest":{"type":"object","properties":{"effect":{"type":"string"},"key":{"type":"string"},"value":{"type":"string"}}},"dto.CredentialOut":{"type":"object","properties":{"account_id":{"type":"string"},"created_at":{"type":"string"},"id":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"provider":{"type":"string"},"region":{"type":"string"},"schema_version":{"type":"integer"},"scope":{"type":"object"},"scope_kind":{"type":"string"},"scope_version":{"type":"integer"},"updated_at":{"type":"string"}}},"dto.EnqueueRequest":{"type":"object","properties":{"payload":{"type":"object"},"queue":{"type":"string","example":"default"},"run_at":{"type":"string","example":"2025-11-05T08:00:00Z"},"type":{"type":"string","example":"email.send"}}},"dto.JWK":{"type":"object","properties":{"alg":{"type":"string","example":"RS256"},"e":{"type":"string","example":"AQAB"},"kid":{"type":"string","example":"7c6f1d0a-7a98-4e6a-9dbf-6b1af4b9f345"},"kty":{"type":"string","example":"RSA"},"n":{"type":"string"},"use":{"type":"string","example":"sig"},"x":{"type":"string"}}},"dto.JWKS":{"type":"object","properties":{"keys":{"type":"array","items":{"$ref":"#/definitions/dto.JWK"}}}},"dto.Job":{"type":"object","properties":{"attempts":{"type":"integer","example":0},"created_at":{"type":"string","example":"2025-11-04T09:30:00Z"},"id":{"type":"string","example":"01HF7SZK8Z8WG1M3J7S2Z8M2N6"},"last_error":{"type":"string","example":"error message"},"max_attempts":{"type":"integer","example":3},"payload":{},"queue":{"type":"string","example":"default"},"run_at":{"type":"string","example":"2025-11-04T09:30:00Z"},"status":{"enum":["queued|running|succeeded|failed|canceled|retrying|scheduled"],"allOf":[{"$ref":"#/definitions/dto.JobStatus"}],"example":"queued"},"type":{"type":"string","example":"email.send"},"updated_at":{"type":"string","example":"2025-11-04T09:30:00Z"}}},"dto.JobStatus":{"type":"string","enum":["queued","running","succeeded","failed","canceled","retrying","scheduled"],"x-enum-varnames":["StatusQueued","StatusRunning","StatusSucceeded","StatusFailed","StatusCanceled","StatusRetrying","StatusScheduled"]},"dto.LabelResponse":{"type":"object","properties":{"created_at":{"type":"string"},"id":{"type":"string"},"key":{"type":"string"},"organization_id":{"type":"string"},"updated_at":{"type":"string"},"value":{"type":"string"}}},"dto.LogoutRequest":{"type":"object","properties":{"refresh_token":{"type":"string","example":"m0l9o8rT3t0V8d3eFf..."}}},"dto.NodePoolResponse":{"type":"object","properties":{"annotations":{"type":"array","items":{"$ref":"#/definitions/dto.AnnotationResponse"}},"created_at":{"type":"string"},"id":{"type":"string"},"labels":{"type":"array","items":{"$ref":"#/definitions/dto.LabelResponse"}},"name":{"type":"string"},"organization_id":{"type":"string"},"role":{"type":"string","enum":["master","worker"]},"servers":{"type":"array","items":{"$ref":"#/definitions/dto.ServerResponse"}},"taints":{"type":"array","items":{"$ref":"#/definitions/dto.TaintResponse"}},"updated_at":{"type":"string"}}},"dto.PageJob":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/definitions/dto.Job"}},"page":{"type":"integer","example":1},"page_size":{"type":"integer","example":25},"total":{"type":"integer","example":120}}},"dto.QueueInfo":{"type":"object","properties":{"failed":{"type":"integer","example":5},"name":{"type":"string","example":"default"},"pending":{"type":"integer","example":42},"running":{"type":"integer","example":3},"scheduled":{"type":"integer","example":7}}},"dto.RefreshRequest":{"type":"object","properties":{"refresh_token":{"type":"string","example":"m0l9o8rT3t0V8d3eFf..."}}},"dto.ServerResponse":{"type":"object","properties":{"created_at":{"type":"string"},"hostname":{"type":"string"},"id":{"type":"string"},"organization_id":{"type":"string"},"private_ip_address":{"type":"string"},"public_ip_address":{"type":"string"},"role":{"type":"string","enum":["master","worker","bastion"],"example":"master|worker|bastion"},"ssh_key_id":{"type":"string"},"ssh_user":{"type":"string"},"status":{"type":"string","enum":["pending","provisioning","ready","failed"],"example":"pending|provisioning|ready|failed"},"updated_at":{"type":"string"}}},"dto.SshResponse":{"type":"object","properties":{"created_at":{"type":"string"},"fingerprint":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"organization_id":{"type":"string"},"public_key":{"type":"string"},"updated_at":{"type":"string"}}},"dto.SshRevealResponse":{"type":"object","properties":{"created_at":{"type":"string"},"fingerprint":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"organization_id":{"type":"string"},"private_key":{"type":"string"},"public_key":{"type":"string"},"updated_at":{"type":"string"}}},"dto.TaintResponse":{"type":"object","properties":{"created_at":{"type":"string"},"effect":{"type":"string"},"id":{"type":"string"},"key":{"type":"string"},"organization_id":{"type":"string"},"updated_at":{"type":"string"},"value":{"type":"string"}}},"dto.TokenPair":{"type":"object","properties":{"access_token":{"type":"string","example":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ij..."},"expires_in":{"type":"integer","example":3600},"refresh_token":{"type":"string","example":"m0l9o8rT3t0V8d3eFf...."},"token_type":{"type":"string","example":"Bearer"}}},"dto.UpdateAnnotationRequest":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"dto.UpdateCredentialRequest":{"type":"object","properties":{"account_id":{"type":"string"},"name":{"type":"string"},"region":{"type":"string"},"scope":{"type":"object"},"scope_kind":{"type":"string"},"scope_version":{"type":"integer"},"secret":{"description":"set if rotating","type":"object"}}},"dto.UpdateLabelRequest":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"dto.UpdateNodePoolRequest":{"type":"object","properties":{"name":{"type":"string"},"role":{"type":"string","enum":["master","worker"]}}},"dto.UpdateServerRequest":{"type":"object","properties":{"hostname":{"type":"string"},"private_ip_address":{"type":"string"},"public_ip_address":{"type":"string"},"role":{"type":"string","enum":["master","worker","bastion"],"example":"master|worker|bastion"},"ssh_key_id":{"type":"string"},"ssh_user":{"type":"string"},"status":{"type":"string","enum":["pending","provisioning","ready","failed"],"example":"pending|provisioning|ready|failed"}}},"dto.UpdateTaintRequest":{"type":"object","properties":{"effect":{"type":"string"},"key":{"type":"string"},"value":{"type":"string"}}},"handlers.HealthStatus":{"type":"object","properties":{"status":{"type":"string","example":"ok"}}},"handlers.VersionResponse":{"type":"object","properties":{"built":{"type":"string","example":"2025-11-08T12:34:56Z"},"builtBy":{"type":"string","example":"ci"},"commit":{"type":"string","example":"a1b2c3d"},"commitTime":{"type":"string","example":"2025-11-08T12:31:00Z"},"go":{"type":"string","example":"go1.23.3"},"goArch":{"type":"string","example":"amd64"},"goOS":{"type":"string","example":"linux"},"modified":{"type":"boolean","example":false},"revision":{"type":"string","example":"a1b2c3d4e5f6abcdef"},"vcs":{"type":"string","example":"git"},"version":{"type":"string","example":"1.4.2"}}},"handlers.createUserKeyRequest":{"type":"object","properties":{"expires_in_hours":{"description":"optional TTL","type":"integer"},"name":{"type":"string"}}},"handlers.meResponse":{"type":"object","properties":{"avatar_url":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"display_name":{"type":"string"},"emails":{"type":"array","items":{"$ref":"#/definitions/models.UserEmail"}},"id":{"description":"example: 3fa85f64-5717-4562-b3fc-2c963f66afa6","type":"string","format":"uuid"},"is_admin":{"type":"boolean"},"is_disabled":{"type":"boolean"},"organizations":{"type":"array","items":{"$ref":"#/definitions/models.Organization"}},"primary_email":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"handlers.memberOut":{"type":"object","properties":{"email":{"type":"string"},"role":{"description":"owner/admin/member","type":"string"},"user_id":{"type":"string","format":"uuid"}}},"handlers.memberUpsertReq":{"type":"object","properties":{"role":{"type":"string","example":"member"},"user_id":{"type":"string","format":"uuid"}}},"handlers.orgCreateReq":{"type":"object","properties":{"domain":{"type":"string","example":"acme.com"},"name":{"type":"string","example":"Acme Corp"}}},"handlers.orgKeyCreateReq":{"type":"object","properties":{"expires_in_hours":{"type":"integer","example":720},"name":{"type":"string","example":"automation-bot"}}},"handlers.orgKeyCreateResp":{"type":"object","properties":{"created_at":{"type":"string"},"expires_at":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"org_key":{"description":"shown once:","type":"string"},"org_secret":{"description":"shown once:","type":"string"},"scope":{"description":"\"org\"","type":"string"}}},"handlers.orgUpdateReq":{"type":"object","properties":{"domain":{"type":"string"},"name":{"type":"string"}}},"handlers.updateMeRequest":{"type":"object","properties":{"display_name":{"type":"string"}}},"handlers.userAPIKeyOut":{"type":"object","properties":{"created_at":{"type":"string"},"expires_at":{"type":"string"},"id":{"type":"string","format":"uuid"},"last_used_at":{"type":"string"},"name":{"type":"string"},"plain":{"description":"Shown only on create:","type":"string"},"scope":{"description":"\"user\"","type":"string"}}},"models.APIKey":{"type":"object","properties":{"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"last_used_at":{"type":"string","format":"date-time"},"name":{"type":"string"},"org_id":{"type":"string","format":"uuid"},"prefix":{"type":"string"},"revoked":{"type":"boolean"},"scope":{"type":"string"},"updated_at":{"type":"string","format":"date-time"},"user_id":{"type":"string","format":"uuid"}}},"models.Organization":{"type":"object","properties":{"created_at":{"type":"string","format":"date-time"},"domain":{"type":"string"},"id":{"description":"example: 3fa85f64-5717-4562-b3fc-2c963f66afa6","type":"string","format":"uuid"},"name":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"models.User":{"type":"object","properties":{"avatar_url":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"display_name":{"type":"string"},"id":{"description":"example: 3fa85f64-5717-4562-b3fc-2c963f66afa6","type":"string","format":"uuid"},"is_admin":{"type":"boolean"},"is_disabled":{"type":"boolean"},"primary_email":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"models.UserEmail":{"type":"object","properties":{"created_at":{"type":"string","format":"date-time"},"email":{"type":"string"},"id":{"description":"example: 3fa85f64-5717-4562-b3fc-2c963f66afa6","type":"string","format":"uuid"},"is_primary":{"type":"boolean"},"is_verified":{"type":"boolean"},"updated_at":{"type":"string","format":"date-time"},"user":{"$ref":"#/definitions/models.User"},"user_id":{"type":"string","format":"uuid"}}},"utils.ErrorResponse":{"type":"object","properties":{"code":{"description":"A machine-readable error code, e.g. \"validation_error\"\nexample: validation_error","type":"string"},"message":{"description":"Human-readable message\nexample: slug is required","type":"string"}}}},"securityDefinitions":{"ApiKeyAuth":{"description":"User API key","type":"apiKey","name":"X-API-KEY","in":"header"},"BearerAuth":{"description":"Bearer token authentication","type":"apiKey","name":"Authorization","in":"header"},"OrgKeyAuth":{"description":"Org-level key/secret authentication","type":"apiKey","name":"X-ORG-KEY","in":"header"},"OrgSecretAuth":{"description":"Org-level secret","type":"apiKey","name":"X-ORG-SECRET","in":"header"}}} \ No newline at end of file +{"schemes":["http","https"],"swagger":"2.0","info":{"description":"API for managing K3s clusters across cloud providers","title":"AutoGlue API","contact":{"name":"GlueOps"},"version":"1.0"},"basePath":"/api/v1","paths":{"/.well-known/jwks.json":{"get":{"description":"Returns the JSON Web Key Set for token verification","produces":["application/json"],"tags":["Auth"],"summary":"Get JWKS","operationId":"getJWKS","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.JWKS"}}}}},"/admin/archer/jobs":{"get":{"security":[{"BearerAuth":[]}],"description":"Paginated background jobs with optional filters. Search `q` may match id, type, error, payload (implementation-dependent).","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"List Archer jobs (admin)","operationId":"AdminListArcherJobs","parameters":[{"enum":["queued","running","succeeded","failed","canceled","retrying","scheduled"],"type":"string","description":"Filter by status","name":"status","in":"query"},{"type":"string","description":"Filter by queue name / worker name","name":"queue","in":"query"},{"type":"string","description":"Free-text search","name":"q","in":"query"},{"type":"integer","default":1,"description":"Page number","name":"page","in":"query"},{"maximum":100,"minimum":1,"type":"integer","default":25,"description":"Items per page","name":"page_size","in":"query"}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.PageJob"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"500":{"description":"internal error","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]}],"description":"Create a job immediately or schedule it for the future via `run_at`.","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"Enqueue a new Archer job (admin)","operationId":"AdminEnqueueArcherJob","parameters":[{"description":"Job parameters","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.EnqueueRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.Job"}},"400":{"description":"invalid json or missing fields","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"500":{"description":"internal error","schema":{"type":"string"}}}}},"/admin/archer/jobs/{id}/cancel":{"post":{"security":[{"BearerAuth":[]}],"description":"Set job status to canceled if cancellable. For running jobs, this only affects future picks; wire to Archer if you need active kill.","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"Cancel an Archer job (admin)","operationId":"AdminCancelArcherJob","parameters":[{"type":"string","description":"Job ID","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.Job"}},"400":{"description":"invalid job or not cancellable","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/admin/archer/jobs/{id}/retry":{"post":{"security":[{"BearerAuth":[]}],"description":"Marks the job retriable (DB flip). Swap this for an Archer admin call if you expose one.","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"Retry a failed/canceled Archer job (admin)","operationId":"AdminRetryArcherJob","parameters":[{"type":"string","description":"Job ID","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.Job"}},"400":{"description":"invalid job or not eligible","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/admin/archer/queues":{"get":{"security":[{"BearerAuth":[]}],"description":"Summary metrics per queue (pending, running, failed, scheduled).","consumes":["application/json"],"produces":["application/json"],"tags":["ArcherAdmin"],"summary":"List Archer queues (admin)","operationId":"AdminListArcherQueues","responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.QueueInfo"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"forbidden","schema":{"type":"string"}},"500":{"description":"internal error","schema":{"type":"string"}}}}},"/annotations":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns annotations for the organization in X-Org-ID. Filters: `key`, `value`, and `q` (key contains). Add `include=node_pools` to include linked node pools.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"List annotations (org scoped)","operationId":"ListAnnotations","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Exact key","name":"key","in":"query"},{"type":"string","description":"Exact value","name":"value","in":"query"},{"type":"string","description":"key contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.AnnotationResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list annotations","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates an annotation.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"Create annotation (org scoped)","operationId":"CreateAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Annotation payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateAnnotationRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.AnnotationResponse"}},"400":{"description":"invalid json / missing fields","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/annotations/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns one annotation. Add `include=node_pools` to include node pools.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"Get annotation by ID (org scoped)","operationId":"GetAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Annotation ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.AnnotationResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the annotation.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"Delete annotation (org scoped)","operationId":"DeleteAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Annotation ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update annotation fields.","consumes":["application/json"],"produces":["application/json"],"tags":["Annotations"],"summary":"Update annotation (org scoped)","operationId":"UpdateAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Annotation ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateAnnotationRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.AnnotationResponse"}},"400":{"description":"invalid id / invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/auth/logout":{"post":{"consumes":["application/json"],"produces":["application/json"],"tags":["Auth"],"summary":"Revoke refresh token family (logout everywhere)","operationId":"Logout","parameters":[{"description":"Refresh token","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.LogoutRequest"}}],"responses":{"204":{"description":"No Content"}}}},"/auth/refresh":{"post":{"consumes":["application/json"],"produces":["application/json"],"tags":["Auth"],"summary":"Rotate refresh token","operationId":"Refresh","parameters":[{"description":"Refresh token","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.RefreshRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.TokenPair"}}}}},"/auth/{provider}/callback":{"get":{"produces":["application/json"],"tags":["Auth"],"summary":"Handle social login callback","operationId":"AuthCallback","parameters":[{"type":"string","description":"google|github","name":"provider","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.TokenPair"}}}}},"/auth/{provider}/start":{"post":{"description":"Returns provider authorization URL for the frontend to redirect","produces":["application/json"],"tags":["Auth"],"summary":"Begin social login","operationId":"AuthStart","parameters":[{"type":"string","description":"google|github","name":"provider","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.AuthStartResponse"}}}}},"/clusters":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns clusters for the organization in X-Org-ID. Filter by `q` (name contains).","produces":["application/json"],"tags":["Clusters"],"summary":"List clusters (org scoped)","operationId":"ListClusters","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Name contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.ClusterResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list clusters","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a cluster. If `kubeconfig` is provided, it will be encrypted per-organization and stored securely (never returned).","consumes":["application/json"],"produces":["application/json"],"tags":["Clusters"],"summary":"Create cluster (org scoped)","operationId":"CreateCluster","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateClusterRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.ClusterResponse"}},"400":{"description":"invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/credentials":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns credential metadata for the current org. Secrets are never returned.","consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"List credentials (metadata only)","operationId":"ListCredentials","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Filter by provider (e.g., aws)","name":"provider","in":"query"},{"type":"string","description":"Filter by kind (e.g., aws_access_key)","name":"kind","in":"query"},{"type":"string","description":"Filter by scope kind (provider/service/resource)","name":"scope_kind","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.CredentialOut"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"internal server error","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Create a credential (encrypts secret)","operationId":"CreateCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"description":"Credential payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateCredentialRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.CredentialOut"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"internal server error","schema":{"type":"string"}}}}},"/credentials/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Get credential by ID (metadata only)","operationId":"GetCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Credential ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.CredentialOut"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"internal server error","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Delete credential","operationId":"DeleteCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Credential ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content"},"404":{"description":"not found","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Update credential metadata and/or rotate secret","operationId":"UpdateCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Credential ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateCredentialRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.CredentialOut"}},"403":{"description":"X-Org-ID required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/credentials/{id}/reveal":{"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Credentials"],"summary":"Reveal decrypted secret (one-time read)","operationId":"RevealCredential","parameters":[{"type":"string","description":"Organization ID (UUID)","name":"X-Org-ID","in":"header"},{"type":"string","description":"Credential ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"object","additionalProperties":true}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/dns/domains":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns domains for X-Org-ID. Filters: `domain_name`, `status`, `q` (contains).","consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"List domains (org scoped)","operationId":"ListDomains","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Exact domain name (lowercase, no trailing dot)","name":"domain_name","in":"query"},{"type":"string","description":"pending|provisioning|ready|failed","name":"status","in":"query"},{"type":"string","description":"Domain contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.DomainResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"db error","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a domain bound to a Route 53 scoped credential. Archer will backfill ZoneID if omitted.","consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"Create a domain (org scoped)","operationId":"CreateDomain","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Domain payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateDomainRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.DomainResponse"}},"400":{"description":"validation error","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"db error","schema":{"type":"string"}}}}},"/dns/domains/{domain_id}/records":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Filters: `name`, `type`, `status`.","consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"List record sets for a domain","operationId":"ListRecordSets","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Domain ID (UUID)","name":"domain_id","in":"path","required":true},{"type":"string","description":"Exact relative name or FQDN (server normalizes)","name":"name","in":"query"},{"type":"string","description":"RR type (A, AAAA, CNAME, TXT, MX, NS, SRV, CAA)","name":"type","in":"query"},{"type":"string","description":"pending|provisioning|ready|failed","name":"status","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.RecordSetResponse"}}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"domain not found","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"Create a record set (pending; Archer will UPSERT to Route 53)","operationId":"CreateRecordSet","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Domain ID (UUID)","name":"domain_id","in":"path","required":true},{"description":"Record set payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateRecordSetRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.RecordSetResponse"}},"400":{"description":"validation error","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"domain not found","schema":{"type":"string"}}}}},"/dns/domains/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"Get a domain (org scoped)","operationId":"GetDomain","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Domain ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.DomainResponse"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"Delete a domain","operationId":"DeleteDomain","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Domain ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content"},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"Update a domain (org scoped)","operationId":"UpdateDomain","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Domain ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateDomainRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.DomainResponse"}},"400":{"description":"validation error","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/dns/records/{id}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"Delete a record set (API removes row; worker can optionally handle external deletion policy)","operationId":"DeleteRecordSet","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Record Set ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content"},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["DNS"],"summary":"Update a record set (flips to pending for reconciliation)","operationId":"UpdateRecordSet","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Record Set ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateRecordSetRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.RecordSetResponse"}},"400":{"description":"validation error","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}}}}},"/healthz":{"get":{"description":"Returns 200 OK when the service is up","consumes":["application/json"],"produces":["application/json"],"tags":["Health"],"summary":"Basic health check","operationId":"HealthCheck // operationId","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.HealthStatus"}}}}},"/labels":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns node labels for the organization in X-Org-ID. Filters: `key`, `value`, and `q` (key contains). Add `include=node_pools` to include linked node groups.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"List node labels (org scoped)","operationId":"ListLabels","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Exact key","name":"key","in":"query"},{"type":"string","description":"Exact value","name":"value","in":"query"},{"type":"string","description":"Key contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.LabelResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list node taints","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a label.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"Create label (org scoped)","operationId":"CreateLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Label payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateLabelRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.LabelResponse"}},"400":{"description":"invalid json / missing fields / invalid node_pool_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/labels/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns one label.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"Get label by ID (org scoped)","operationId":"GetLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Label ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.LabelResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the label.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"Delete label (org scoped)","operationId":"DeleteLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Label ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update label fields.","consumes":["application/json"],"produces":["application/json"],"tags":["Labels"],"summary":"Update label (org scoped)","operationId":"UpdateLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Label ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateLabelRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.LabelResponse"}},"400":{"description":"invalid id / invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/me":{"get":{"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"produces":["application/json"],"tags":["Me"],"summary":"Get current user profile","operationId":"GetMe","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.meResponse"}}}},"patch":{"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Me"],"summary":"Update current user profile","operationId":"UpdateMe","parameters":[{"description":"Patch profile","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.updateMeRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/models.User"}}}}},"/me/api-keys":{"get":{"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"produces":["application/json"],"tags":["MeAPIKeys"],"summary":"List my API keys","operationId":"ListUserAPIKeys","responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/handlers.userAPIKeyOut"}}}}},"post":{"security":[{"BearerAuth":[]},{"ApiKeyAuth":[]}],"description":"Returns the plaintext key once. Store it securely on the client side.","consumes":["application/json"],"produces":["application/json"],"tags":["MeAPIKeys"],"summary":"Create a new user API key","operationId":"CreateUserAPIKey","parameters":[{"description":"Key options","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.createUserKeyRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/handlers.userAPIKeyOut"}}}}},"/me/api-keys/{id}":{"delete":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["MeAPIKeys"],"summary":"Delete a user API key","operationId":"DeleteUserAPIKey","parameters":[{"type":"string","description":"Key ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content"}}}},"/node-pools":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns node pools for the organization in X-Org-ID.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List node pools (org scoped)","operationId":"ListNodePools","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Name contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.NodePoolResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list node pools","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a node pool. Optionally attach initial servers.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Create node pool (org scoped)","operationId":"CreateNodePool","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"NodePool payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateNodePoolRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.NodePoolResponse"}},"400":{"description":"invalid json / missing fields / invalid server_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/node-pools/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns one node pool. Add `include=servers` to include servers.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Get node pool by ID (org scoped)","operationId":"GetNodePool","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.NodePoolResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the node pool.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Delete node pool (org scoped)","operationId":"DeleteNodePool","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update node pool fields.","consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Update node pool (org scoped)","operationId":"UpdateNodePool","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateNodePoolRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.NodePoolResponse"}},"400":{"description":"invalid id / invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/node-pools/{id}/annotations":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List annotations attached to a node pool (org scoped)","operationId":"ListNodePoolAnnotations","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.AnnotationResponse"}}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Attach annotation to a node pool (org scoped)","operationId":"AttachNodePoolAnnotations","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Group ID (UUID)","name":"id","in":"path","required":true},{"description":"Annotation IDs to attach","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.AttachAnnotationsRequest"}}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid server_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"attach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/annotations/{annotationId}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Detach one annotation from a node pool (org scoped)","operationId":"DetachNodePoolAnnotation","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Annotation ID (UUID)","name":"annotationId","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"detach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/labels":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List labels attached to a node pool (org scoped)","operationId":"ListNodePoolLabels","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Label Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.LabelResponse"}}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Attach labels to a node pool (org scoped)","operationId":"AttachNodePoolLabels","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"description":"Label IDs to attach","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.AttachLabelsRequest"}}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid server_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"attach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/labels/{labelId}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Detach one label from a node pool (org scoped)","operationId":"DetachNodePoolLabel","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Label ID (UUID)","name":"labelId","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"detach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/servers":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List servers attached to a node pool (org scoped)","operationId":"ListNodePoolServers","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.ServerResponse"}}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Attach servers to a node pool (org scoped)","operationId":"AttachNodePoolServers","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"description":"Server IDs to attach","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.AttachServersRequest"}}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid server_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"attach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/servers/{serverId}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Detach one server from a node pool (org scoped)","operationId":"DetachNodePoolServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Server ID (UUID)","name":"serverId","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"detach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/taints":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"List taints attached to a node pool (org scoped)","operationId":"ListNodePoolTaints","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.TaintResponse"}}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Attach taints to a node pool (org scoped)","operationId":"AttachNodePoolTaints","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"description":"Taint IDs to attach","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.AttachTaintsRequest"}}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid taint_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"attach failed","schema":{"type":"string"}}}}},"/node-pools/{id}/taints/{taintId}":{"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["NodePools"],"summary":"Detach one taint from a node pool (org scoped)","operationId":"DetachNodePoolTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Pool ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Taint ID (UUID)","name":"taintId","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"detach failed","schema":{"type":"string"}}}}},"/orgs":{"get":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"List organizations I belong to","operationId":"listMyOrgs","responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/models.Organization"}}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"post":{"security":[{"BearerAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Orgs"],"summary":"Create organization","operationId":"createOrg","parameters":[{"description":"Org payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.orgCreateReq"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/models.Organization"}},"400":{"description":"Bad Request","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"409":{"description":"Conflict","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}":{"get":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"Get organization","operationId":"getOrg","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/models.Organization"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"delete":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"Delete organization (owner)","operationId":"deleteOrg","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"Deleted"},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"patch":{"security":[{"BearerAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Orgs"],"summary":"Update organization (owner/admin)","operationId":"updateOrg","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"description":"Update payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.orgUpdateReq"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/models.Organization"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}},"404":{"description":"Not Found","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}/api-keys":{"get":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"List org-scoped API keys (no secrets)","operationId":"listOrgKeys","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/models.APIKey"}}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"post":{"security":[{"BearerAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Orgs"],"summary":"Create org key/secret pair (owner/admin)","operationId":"createOrgKey","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"description":"Key name + optional expiry","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.orgKeyCreateReq"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/handlers.orgKeyCreateResp"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}/api-keys/{key_id}":{"delete":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"Delete org key (owner/admin)","operationId":"deleteOrgKey","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"Key ID (UUID)","name":"key_id","in":"path","required":true}],"responses":{"204":{"description":"Deleted"},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}/members":{"get":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"List members in org","operationId":"listMembers","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/handlers.memberOut"}}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}},"post":{"security":[{"BearerAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Orgs"],"summary":"Add or update a member (owner/admin)","operationId":"addOrUpdateMember","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"description":"User \u0026 role","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/handlers.memberUpsertReq"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.memberOut"}},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/orgs/{id}/members/{user_id}":{"delete":{"security":[{"BearerAuth":[]}],"produces":["application/json"],"tags":["Orgs"],"summary":"Remove a member (owner/admin)","operationId":"removeMember","parameters":[{"type":"string","description":"Org ID (UUID)","name":"id","in":"path","required":true},{"type":"string","description":"User ID (UUID)","name":"user_id","in":"path","required":true}],"responses":{"204":{"description":"Removed"},"401":{"description":"Unauthorized","schema":{"$ref":"#/definitions/utils.ErrorResponse"}}}}},"/servers":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns servers for the organization in X-Org-ID. Optional filters: status, role.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"List servers (org scoped)","operationId":"ListServers","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Filter by status (pending|provisioning|ready|failed)","name":"status","in":"query"},{"type":"string","description":"Filter by role","name":"role","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.ServerResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list servers","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a server bound to the org in X-Org-ID. Validates that ssh_key_id belongs to the org.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Create server (org scoped)","operationId":"CreateServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Server payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateServerRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.ServerResponse"}},"400":{"description":"invalid json / missing fields / invalid status / invalid ssh_key_id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/servers/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns one server in the given organization.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Get server by ID (org scoped)","operationId":"GetServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Server ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.ServerResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the server.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Delete server (org scoped)","operationId":"DeleteServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Server ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update fields; changing ssh_key_id validates ownership.","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Update server (org scoped)","operationId":"UpdateServer","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Server ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateServerRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.ServerResponse"}},"400":{"description":"invalid id / invalid json / invalid status / invalid ssh_key_id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/servers/{id}/reset-hostkey":{"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Clears the stored SSH host key for this server. The next SSH connection will re-learn the host key (trust-on-first-use).","consumes":["application/json"],"produces":["application/json"],"tags":["Servers"],"summary":"Reset SSH host key (org scoped)","operationId":"ResetServerHostKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Server ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.ServerResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"reset failed","schema":{"type":"string"}}}}},"/ssh":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns ssh keys for the organization in X-Org-ID.","consumes":["application/json"],"produces":["application/json"],"tags":["Ssh"],"summary":"List ssh keys (org scoped)","operationId":"ListPublicSshKeys","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.SshResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list keys","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Generates an RSA or ED25519 keypair, saves it, and returns metadata. For RSA you may set bits (2048/3072/4096). Default is 4096. ED25519 ignores bits.","consumes":["application/json"],"produces":["application/json"],"tags":["Ssh"],"summary":"Create ssh keypair (org scoped)","operationId":"CreateSSHKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Key generation options","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateSSHRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.SshResponse"}},"400":{"description":"invalid json / invalid bits","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"generation/create failed","schema":{"type":"string"}}}}},"/ssh/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns public key fields. Append `?reveal=true` to include the private key PEM.","consumes":["application/json"],"produces":["application/json"],"tags":["Ssh"],"summary":"Get ssh key by ID (org scoped)","operationId":"GetSSHKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"SSH Key ID (UUID)","name":"id","in":"path","required":true},{"type":"boolean","description":"Reveal private key PEM","name":"reveal","in":"query"}],"responses":{"200":{"description":"When reveal=true","schema":{"$ref":"#/definitions/dto.SshRevealResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes a keypair.","consumes":["application/json"],"produces":["application/json"],"tags":["Ssh"],"summary":"Delete ssh keypair (org scoped)","operationId":"DeleteSSHKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"SSH Key ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}}},"/ssh/{id}/download":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Download `part=public|private|both` of the keypair. `both` returns a zip file.","produces":["application/json"],"tags":["Ssh"],"summary":"Download ssh key files by ID (org scoped)","operationId":"DownloadSSHKey","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header","required":true},{"type":"string","description":"SSH Key ID (UUID)","name":"id","in":"path","required":true},{"enum":["public","private","both"],"type":"string","description":"Which part to download","name":"part","in":"query","required":true}],"responses":{"200":{"description":"file content","schema":{"type":"string"}},"400":{"description":"invalid id / invalid part","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"download failed","schema":{"type":"string"}}}}},"/taints":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Returns node taints for the organization in X-Org-ID. Filters: `key`, `value`, and `q` (key contains). Add `include=node_pools` to include linked node pools.","consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"List node pool taints (org scoped)","operationId":"ListTaints","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Exact key","name":"key","in":"query"},{"type":"string","description":"Exact value","name":"value","in":"query"},{"type":"string","description":"key contains (case-insensitive)","name":"q","in":"query"}],"responses":{"200":{"description":"OK","schema":{"type":"array","items":{"$ref":"#/definitions/dto.TaintResponse"}}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"failed to list node taints","schema":{"type":"string"}}}},"post":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Creates a taint.","consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"Create node taint (org scoped)","operationId":"CreateTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"description":"Taint payload","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.CreateTaintRequest"}}],"responses":{"201":{"description":"Created","schema":{"$ref":"#/definitions/dto.TaintResponse"}},"400":{"description":"invalid json / missing fields / invalid node_pool_ids","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"create failed","schema":{"type":"string"}}}}},"/taints/{id}":{"get":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"Get node taint by ID (org scoped)","operationId":"GetTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Taint ID (UUID)","name":"id","in":"path","required":true}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.TaintResponse"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"fetch failed","schema":{"type":"string"}}}},"delete":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Permanently deletes the taint.","consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"Delete taint (org scoped)","operationId":"DeleteTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Taint ID (UUID)","name":"id","in":"path","required":true}],"responses":{"204":{"description":"No Content","schema":{"type":"string"}},"400":{"description":"invalid id","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"500":{"description":"delete failed","schema":{"type":"string"}}}},"patch":{"security":[{"BearerAuth":[]},{"OrgKeyAuth":[]},{"OrgSecretAuth":[]}],"description":"Partially update taint fields.","consumes":["application/json"],"produces":["application/json"],"tags":["Taints"],"summary":"Update node taint (org scoped)","operationId":"UpdateTaint","parameters":[{"type":"string","description":"Organization UUID","name":"X-Org-ID","in":"header"},{"type":"string","description":"Node Taint ID (UUID)","name":"id","in":"path","required":true},{"description":"Fields to update","name":"body","in":"body","required":true,"schema":{"$ref":"#/definitions/dto.UpdateTaintRequest"}}],"responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/dto.TaintResponse"}},"400":{"description":"invalid id / invalid json","schema":{"type":"string"}},"401":{"description":"Unauthorized","schema":{"type":"string"}},"403":{"description":"organization required","schema":{"type":"string"}},"404":{"description":"not found","schema":{"type":"string"}},"500":{"description":"update failed","schema":{"type":"string"}}}}},"/version":{"get":{"description":"Returns build/runtime metadata for the running service.","consumes":["application/json"],"produces":["application/json"],"tags":["Meta"],"summary":"Service version information","operationId":"Version // operationId","responses":{"200":{"description":"OK","schema":{"$ref":"#/definitions/handlers.VersionResponse"}}}}}},"definitions":{"dto.AnnotationResponse":{"type":"object","properties":{"created_at":{"type":"string"},"id":{"type":"string"},"key":{"type":"string"},"organization_id":{"type":"string"},"updated_at":{"type":"string"},"value":{"type":"string"}}},"dto.AttachAnnotationsRequest":{"type":"object","properties":{"annotation_ids":{"type":"array","items":{"type":"string"}}}},"dto.AttachLabelsRequest":{"type":"object","properties":{"label_ids":{"type":"array","items":{"type":"string"}}}},"dto.AttachServersRequest":{"type":"object","properties":{"server_ids":{"type":"array","items":{"type":"string"}}}},"dto.AttachTaintsRequest":{"type":"object","properties":{"taint_ids":{"type":"array","items":{"type":"string"}}}},"dto.AuthStartResponse":{"type":"object","properties":{"auth_url":{"type":"string","example":"https://accounts.google.com/o/oauth2/v2/auth?client_id=..."}}},"dto.ClusterResponse":{"type":"object","properties":{"bastion_server":{"$ref":"#/definitions/dto.ServerResponse"},"captain_domain":{"type":"string"},"certificate_key":{"type":"string"},"cluster_load_balancer":{"type":"string"},"control_load_balancer":{"type":"string"},"created_at":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"node_pools":{"type":"array","items":{"$ref":"#/definitions/dto.NodePoolResponse"}},"provider":{"type":"string"},"random_token":{"type":"string"},"region":{"type":"string"},"status":{"type":"string"},"updated_at":{"type":"string"}}},"dto.CreateAnnotationRequest":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"dto.CreateClusterRequest":{"type":"object","properties":{"captain_domain":{"type":"string"},"cluster_load_balancer":{"type":"string"},"control_load_balancer":{"type":"string"},"name":{"type":"string"},"provider":{"type":"string"},"region":{"type":"string"},"status":{"type":"string"}}},"dto.CreateCredentialRequest":{"type":"object","required":["kind","provider","schema_version","scope","scope_kind","scope_version","secret"],"properties":{"account_id":{"type":"string","maxLength":32},"kind":{"description":"aws_access_key, api_token, basic_auth, oauth2","type":"string"},"name":{"description":"human label","type":"string","maxLength":100},"provider":{"type":"string","enum":["aws","cloudflare","hetzner","digitalocean","generic"]},"region":{"type":"string","maxLength":32},"schema_version":{"description":"secret schema version","type":"integer","minimum":1},"scope":{"description":"{\"service\":\"route53\"} or {\"arn\":\"...\"}","type":"object"},"scope_kind":{"type":"string","enum":["provider","service","resource"]},"scope_version":{"description":"scope schema version","type":"integer","minimum":1},"secret":{"description":"encrypted later","type":"object"}}},"dto.CreateDomainRequest":{"type":"object","required":["credential_id","domain_name"],"properties":{"credential_id":{"type":"string"},"domain_name":{"type":"string"},"zone_id":{"type":"string","maxLength":128}}},"dto.CreateLabelRequest":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"dto.CreateNodePoolRequest":{"type":"object","properties":{"name":{"type":"string"},"role":{"type":"string","enum":["master","worker"]}}},"dto.CreateRecordSetRequest":{"type":"object","required":["name","type"],"properties":{"name":{"description":"Name relative to domain (\"endpoint\") OR FQDN (\"endpoint.example.com\").\nServer normalizes to relative.","type":"string","maxLength":253},"ttl":{"type":"integer","maximum":86400,"minimum":1},"type":{"type":"string"},"values":{"type":"array","items":{"type":"string"}}}},"dto.CreateSSHRequest":{"type":"object","properties":{"bits":{"description":"Only for RSA","type":"integer"},"comment":{"type":"string","example":"deploy@autoglue"},"name":{"type":"string"},"type":{"description":"\"rsa\" (default) or \"ed25519\"","type":"string"}}},"dto.CreateServerRequest":{"type":"object","properties":{"hostname":{"type":"string"},"private_ip_address":{"type":"string"},"public_ip_address":{"type":"string"},"role":{"type":"string","enum":["master","worker","bastion"],"example":"master|worker|bastion"},"ssh_key_id":{"type":"string"},"ssh_user":{"type":"string"},"status":{"type":"string","enum":["pending","provisioning","ready","failed"],"example":"pending|provisioning|ready|failed"}}},"dto.CreateTaintRequest":{"type":"object","properties":{"effect":{"type":"string"},"key":{"type":"string"},"value":{"type":"string"}}},"dto.CredentialOut":{"type":"object","properties":{"account_id":{"type":"string"},"created_at":{"type":"string"},"id":{"type":"string"},"kind":{"type":"string"},"name":{"type":"string"},"provider":{"type":"string"},"region":{"type":"string"},"schema_version":{"type":"integer"},"scope":{"type":"object"},"scope_kind":{"type":"string"},"scope_version":{"type":"integer"},"updated_at":{"type":"string"}}},"dto.DomainResponse":{"type":"object","properties":{"created_at":{"type":"string"},"credential_id":{"type":"string"},"domain_name":{"type":"string"},"id":{"type":"string"},"last_error":{"type":"string"},"organization_id":{"type":"string"},"status":{"type":"string"},"updated_at":{"type":"string"},"zone_id":{"type":"string"}}},"dto.EnqueueRequest":{"type":"object","properties":{"payload":{"type":"object"},"queue":{"type":"string","example":"default"},"run_at":{"type":"string","example":"2025-11-05T08:00:00Z"},"type":{"type":"string","example":"email.send"}}},"dto.JWK":{"type":"object","properties":{"alg":{"type":"string","example":"RS256"},"e":{"type":"string","example":"AQAB"},"kid":{"type":"string","example":"7c6f1d0a-7a98-4e6a-9dbf-6b1af4b9f345"},"kty":{"type":"string","example":"RSA"},"n":{"type":"string"},"use":{"type":"string","example":"sig"},"x":{"type":"string"}}},"dto.JWKS":{"type":"object","properties":{"keys":{"type":"array","items":{"$ref":"#/definitions/dto.JWK"}}}},"dto.Job":{"type":"object","properties":{"attempts":{"type":"integer","example":0},"created_at":{"type":"string","example":"2025-11-04T09:30:00Z"},"id":{"type":"string","example":"01HF7SZK8Z8WG1M3J7S2Z8M2N6"},"last_error":{"type":"string","example":"error message"},"max_attempts":{"type":"integer","example":3},"payload":{},"queue":{"type":"string","example":"default"},"run_at":{"type":"string","example":"2025-11-04T09:30:00Z"},"status":{"enum":["queued|running|succeeded|failed|canceled|retrying|scheduled"],"allOf":[{"$ref":"#/definitions/dto.JobStatus"}],"example":"queued"},"type":{"type":"string","example":"email.send"},"updated_at":{"type":"string","example":"2025-11-04T09:30:00Z"}}},"dto.JobStatus":{"type":"string","enum":["queued","running","succeeded","failed","canceled","retrying","scheduled"],"x-enum-varnames":["StatusQueued","StatusRunning","StatusSucceeded","StatusFailed","StatusCanceled","StatusRetrying","StatusScheduled"]},"dto.LabelResponse":{"type":"object","properties":{"created_at":{"type":"string"},"id":{"type":"string"},"key":{"type":"string"},"organization_id":{"type":"string"},"updated_at":{"type":"string"},"value":{"type":"string"}}},"dto.LogoutRequest":{"type":"object","properties":{"refresh_token":{"type":"string","example":"m0l9o8rT3t0V8d3eFf..."}}},"dto.NodePoolResponse":{"type":"object","properties":{"annotations":{"type":"array","items":{"$ref":"#/definitions/dto.AnnotationResponse"}},"created_at":{"type":"string"},"id":{"type":"string"},"labels":{"type":"array","items":{"$ref":"#/definitions/dto.LabelResponse"}},"name":{"type":"string"},"organization_id":{"type":"string"},"role":{"type":"string","enum":["master","worker"]},"servers":{"type":"array","items":{"$ref":"#/definitions/dto.ServerResponse"}},"taints":{"type":"array","items":{"$ref":"#/definitions/dto.TaintResponse"}},"updated_at":{"type":"string"}}},"dto.PageJob":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/definitions/dto.Job"}},"page":{"type":"integer","example":1},"page_size":{"type":"integer","example":25},"total":{"type":"integer","example":120}}},"dto.QueueInfo":{"type":"object","properties":{"failed":{"type":"integer","example":5},"name":{"type":"string","example":"default"},"pending":{"type":"integer","example":42},"running":{"type":"integer","example":3},"scheduled":{"type":"integer","example":7}}},"dto.RecordSetResponse":{"type":"object","properties":{"created_at":{"type":"string"},"domain_id":{"type":"string"},"fingerprint":{"type":"string"},"id":{"type":"string"},"last_error":{"type":"string"},"name":{"type":"string"},"owner":{"type":"string"},"status":{"type":"string"},"ttl":{"type":"integer"},"type":{"type":"string"},"updated_at":{"type":"string"},"values":{"description":"[]string JSON","type":"object"}}},"dto.RefreshRequest":{"type":"object","properties":{"refresh_token":{"type":"string","example":"m0l9o8rT3t0V8d3eFf..."}}},"dto.ServerResponse":{"type":"object","properties":{"created_at":{"type":"string"},"hostname":{"type":"string"},"id":{"type":"string"},"organization_id":{"type":"string"},"private_ip_address":{"type":"string"},"public_ip_address":{"type":"string"},"role":{"type":"string","enum":["master","worker","bastion"],"example":"master|worker|bastion"},"ssh_key_id":{"type":"string"},"ssh_user":{"type":"string"},"status":{"type":"string","enum":["pending","provisioning","ready","failed"],"example":"pending|provisioning|ready|failed"},"updated_at":{"type":"string"}}},"dto.SshResponse":{"type":"object","properties":{"created_at":{"type":"string"},"fingerprint":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"organization_id":{"type":"string"},"public_key":{"type":"string"},"updated_at":{"type":"string"}}},"dto.SshRevealResponse":{"type":"object","properties":{"created_at":{"type":"string"},"fingerprint":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"organization_id":{"type":"string"},"private_key":{"type":"string"},"public_key":{"type":"string"},"updated_at":{"type":"string"}}},"dto.TaintResponse":{"type":"object","properties":{"created_at":{"type":"string"},"effect":{"type":"string"},"id":{"type":"string"},"key":{"type":"string"},"organization_id":{"type":"string"},"updated_at":{"type":"string"},"value":{"type":"string"}}},"dto.TokenPair":{"type":"object","properties":{"access_token":{"type":"string","example":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ij..."},"expires_in":{"type":"integer","example":3600},"refresh_token":{"type":"string","example":"m0l9o8rT3t0V8d3eFf...."},"token_type":{"type":"string","example":"Bearer"}}},"dto.UpdateAnnotationRequest":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"dto.UpdateCredentialRequest":{"type":"object","properties":{"account_id":{"type":"string"},"name":{"type":"string"},"region":{"type":"string"},"scope":{"type":"object"},"scope_kind":{"type":"string"},"scope_version":{"type":"integer"},"secret":{"description":"set if rotating","type":"object"}}},"dto.UpdateDomainRequest":{"type":"object","properties":{"credential_id":{"type":"string"},"domain_name":{"type":"string"},"status":{"type":"string","enum":["pending","provisioning","ready","failed"]},"zone_id":{"type":"string","maxLength":128}}},"dto.UpdateLabelRequest":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}},"dto.UpdateNodePoolRequest":{"type":"object","properties":{"name":{"type":"string"},"role":{"type":"string","enum":["master","worker"]}}},"dto.UpdateRecordSetRequest":{"type":"object","properties":{"name":{"description":"Any change flips status back to pending (worker will UPSERT)","type":"string","maxLength":253},"status":{"type":"string","enum":["pending","provisioning","ready","failed"]},"ttl":{"type":"integer","maximum":86400,"minimum":1},"type":{"type":"string"},"values":{"type":"array","items":{"type":"string"}}}},"dto.UpdateServerRequest":{"type":"object","properties":{"hostname":{"type":"string"},"private_ip_address":{"type":"string"},"public_ip_address":{"type":"string"},"role":{"type":"string","enum":["master","worker","bastion"],"example":"master|worker|bastion"},"ssh_key_id":{"type":"string"},"ssh_user":{"type":"string"},"status":{"type":"string","enum":["pending","provisioning","ready","failed"],"example":"pending|provisioning|ready|failed"}}},"dto.UpdateTaintRequest":{"type":"object","properties":{"effect":{"type":"string"},"key":{"type":"string"},"value":{"type":"string"}}},"handlers.HealthStatus":{"type":"object","properties":{"status":{"type":"string","example":"ok"}}},"handlers.VersionResponse":{"type":"object","properties":{"built":{"type":"string","example":"2025-11-08T12:34:56Z"},"builtBy":{"type":"string","example":"ci"},"commit":{"type":"string","example":"a1b2c3d"},"commitTime":{"type":"string","example":"2025-11-08T12:31:00Z"},"go":{"type":"string","example":"go1.23.3"},"goArch":{"type":"string","example":"amd64"},"goOS":{"type":"string","example":"linux"},"modified":{"type":"boolean","example":false},"revision":{"type":"string","example":"a1b2c3d4e5f6abcdef"},"vcs":{"type":"string","example":"git"},"version":{"type":"string","example":"1.4.2"}}},"handlers.createUserKeyRequest":{"type":"object","properties":{"expires_in_hours":{"description":"optional TTL","type":"integer"},"name":{"type":"string"}}},"handlers.meResponse":{"type":"object","properties":{"avatar_url":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"display_name":{"type":"string"},"emails":{"type":"array","items":{"$ref":"#/definitions/models.UserEmail"}},"id":{"description":"example: 3fa85f64-5717-4562-b3fc-2c963f66afa6","type":"string","format":"uuid"},"is_admin":{"type":"boolean"},"is_disabled":{"type":"boolean"},"organizations":{"type":"array","items":{"$ref":"#/definitions/models.Organization"}},"primary_email":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"handlers.memberOut":{"type":"object","properties":{"email":{"type":"string"},"role":{"description":"owner/admin/member","type":"string"},"user_id":{"type":"string","format":"uuid"}}},"handlers.memberUpsertReq":{"type":"object","properties":{"role":{"type":"string","example":"member"},"user_id":{"type":"string","format":"uuid"}}},"handlers.orgCreateReq":{"type":"object","properties":{"domain":{"type":"string","example":"acme.com"},"name":{"type":"string","example":"Acme Corp"}}},"handlers.orgKeyCreateReq":{"type":"object","properties":{"expires_in_hours":{"type":"integer","example":720},"name":{"type":"string","example":"automation-bot"}}},"handlers.orgKeyCreateResp":{"type":"object","properties":{"created_at":{"type":"string"},"expires_at":{"type":"string"},"id":{"type":"string"},"name":{"type":"string"},"org_key":{"description":"shown once:","type":"string"},"org_secret":{"description":"shown once:","type":"string"},"scope":{"description":"\"org\"","type":"string"}}},"handlers.orgUpdateReq":{"type":"object","properties":{"domain":{"type":"string"},"name":{"type":"string"}}},"handlers.updateMeRequest":{"type":"object","properties":{"display_name":{"type":"string"}}},"handlers.userAPIKeyOut":{"type":"object","properties":{"created_at":{"type":"string"},"expires_at":{"type":"string"},"id":{"type":"string","format":"uuid"},"last_used_at":{"type":"string"},"name":{"type":"string"},"plain":{"description":"Shown only on create:","type":"string"},"scope":{"description":"\"user\"","type":"string"}}},"models.APIKey":{"type":"object","properties":{"created_at":{"type":"string","format":"date-time"},"expires_at":{"type":"string","format":"date-time"},"id":{"type":"string","format":"uuid"},"last_used_at":{"type":"string","format":"date-time"},"name":{"type":"string"},"org_id":{"type":"string","format":"uuid"},"prefix":{"type":"string"},"revoked":{"type":"boolean"},"scope":{"type":"string"},"updated_at":{"type":"string","format":"date-time"},"user_id":{"type":"string","format":"uuid"}}},"models.Organization":{"type":"object","properties":{"created_at":{"type":"string","format":"date-time"},"domain":{"type":"string"},"id":{"description":"example: 3fa85f64-5717-4562-b3fc-2c963f66afa6","type":"string","format":"uuid"},"name":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"models.User":{"type":"object","properties":{"avatar_url":{"type":"string"},"created_at":{"type":"string","format":"date-time"},"display_name":{"type":"string"},"id":{"description":"example: 3fa85f64-5717-4562-b3fc-2c963f66afa6","type":"string","format":"uuid"},"is_admin":{"type":"boolean"},"is_disabled":{"type":"boolean"},"primary_email":{"type":"string"},"updated_at":{"type":"string","format":"date-time"}}},"models.UserEmail":{"type":"object","properties":{"created_at":{"type":"string","format":"date-time"},"email":{"type":"string"},"id":{"description":"example: 3fa85f64-5717-4562-b3fc-2c963f66afa6","type":"string","format":"uuid"},"is_primary":{"type":"boolean"},"is_verified":{"type":"boolean"},"updated_at":{"type":"string","format":"date-time"},"user":{"$ref":"#/definitions/models.User"},"user_id":{"type":"string","format":"uuid"}}},"utils.ErrorResponse":{"type":"object","properties":{"code":{"description":"A machine-readable error code, e.g. \"validation_error\"\nexample: validation_error","type":"string"},"message":{"description":"Human-readable message\nexample: slug is required","type":"string"}}}},"securityDefinitions":{"ApiKeyAuth":{"description":"User API key","type":"apiKey","name":"X-API-KEY","in":"header"},"BearerAuth":{"description":"Bearer token authentication","type":"apiKey","name":"Authorization","in":"header"},"OrgKeyAuth":{"description":"Org-level key/secret authentication","type":"apiKey","name":"X-ORG-KEY","in":"header"},"OrgSecretAuth":{"description":"Org-level secret","type":"apiKey","name":"X-ORG-SECRET","in":"header"}}} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml index be4d086..1945026 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -158,6 +158,19 @@ definitions: - scope_version - secret type: object + dto.CreateDomainRequest: + properties: + credential_id: + type: string + domain_name: + type: string + zone_id: + maxLength: 128 + type: string + required: + - credential_id + - domain_name + type: object dto.CreateLabelRequest: properties: key: @@ -175,6 +188,28 @@ definitions: - worker type: string type: object + dto.CreateRecordSetRequest: + properties: + name: + description: |- + Name relative to domain ("endpoint") OR FQDN ("endpoint.example.com"). + Server normalizes to relative. + maxLength: 253 + type: string + ttl: + maximum: 86400 + minimum: 1 + type: integer + type: + type: string + values: + items: + type: string + type: array + required: + - name + - type + type: object dto.CreateSSHRequest: properties: bits: @@ -253,6 +288,27 @@ definitions: updated_at: type: string type: object + dto.DomainResponse: + properties: + created_at: + type: string + credential_id: + type: string + domain_name: + type: string + id: + type: string + last_error: + type: string + organization_id: + type: string + status: + type: string + updated_at: + type: string + zone_id: + type: string + type: object dto.EnqueueRequest: properties: payload: @@ -440,6 +496,34 @@ definitions: example: 7 type: integer type: object + dto.RecordSetResponse: + properties: + created_at: + type: string + domain_id: + type: string + fingerprint: + type: string + id: + type: string + last_error: + type: string + name: + type: string + owner: + type: string + status: + type: string + ttl: + type: integer + type: + type: string + updated_at: + type: string + values: + description: '[]string JSON' + type: object + type: object dto.RefreshRequest: properties: refresh_token: @@ -575,6 +659,23 @@ definitions: description: set if rotating type: object type: object + dto.UpdateDomainRequest: + properties: + credential_id: + type: string + domain_name: + type: string + status: + enum: + - pending + - provisioning + - ready + - failed + type: string + zone_id: + maxLength: 128 + type: string + type: object dto.UpdateLabelRequest: properties: key: @@ -592,6 +693,30 @@ definitions: - worker type: string type: object + dto.UpdateRecordSetRequest: + properties: + name: + description: Any change flips status back to pending (worker will UPSERT) + maxLength: 253 + type: string + status: + enum: + - pending + - provisioning + - ready + - failed + type: string + ttl: + maximum: 86400 + minimum: 1 + type: integer + type: + type: string + values: + items: + type: string + type: array + type: object dto.UpdateServerRequest: properties: hostname: @@ -1812,6 +1937,406 @@ paths: summary: Reveal decrypted secret (one-time read) tags: - Credentials + /dns/domains: + get: + consumes: + - application/json + description: 'Returns domains for X-Org-ID. Filters: `domain_name`, `status`, + `q` (contains).' + operationId: ListDomains + parameters: + - description: Organization UUID + in: header + name: X-Org-ID + type: string + - description: Exact domain name (lowercase, no trailing dot) + in: query + name: domain_name + type: string + - description: pending|provisioning|ready|failed + in: query + name: status + type: string + - description: Domain contains (case-insensitive) + in: query + name: q + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.DomainResponse' + type: array + "401": + description: Unauthorized + schema: + type: string + "403": + description: organization required + schema: + type: string + "500": + description: db error + schema: + type: string + security: + - BearerAuth: [] + - OrgKeyAuth: [] + - OrgSecretAuth: [] + summary: List domains (org scoped) + tags: + - DNS + post: + consumes: + - application/json + description: Creates a domain bound to a Route 53 scoped credential. Archer + will backfill ZoneID if omitted. + operationId: CreateDomain + parameters: + - description: Organization UUID + in: header + name: X-Org-ID + type: string + - description: Domain payload + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.CreateDomainRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/dto.DomainResponse' + "400": + description: validation error + schema: + type: string + "401": + description: Unauthorized + schema: + type: string + "403": + description: organization required + schema: + type: string + "500": + description: db error + schema: + type: string + security: + - BearerAuth: [] + - OrgKeyAuth: [] + - OrgSecretAuth: [] + summary: Create a domain (org scoped) + tags: + - DNS + /dns/domains/{domain_id}/records: + get: + consumes: + - application/json + description: 'Filters: `name`, `type`, `status`.' + operationId: ListRecordSets + parameters: + - description: Organization UUID + in: header + name: X-Org-ID + type: string + - description: Domain ID (UUID) + in: path + name: domain_id + required: true + type: string + - description: Exact relative name or FQDN (server normalizes) + in: query + name: name + type: string + - description: RR type (A, AAAA, CNAME, TXT, MX, NS, SRV, CAA) + in: query + name: type + type: string + - description: pending|provisioning|ready|failed + in: query + name: status + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.RecordSetResponse' + type: array + "403": + description: organization required + schema: + type: string + "404": + description: domain not found + schema: + type: string + security: + - BearerAuth: [] + - OrgKeyAuth: [] + - OrgSecretAuth: [] + summary: List record sets for a domain + tags: + - DNS + post: + consumes: + - application/json + operationId: CreateRecordSet + parameters: + - description: Organization UUID + in: header + name: X-Org-ID + type: string + - description: Domain ID (UUID) + in: path + name: domain_id + required: true + type: string + - description: Record set payload + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.CreateRecordSetRequest' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/dto.RecordSetResponse' + "400": + description: validation error + schema: + type: string + "403": + description: organization required + schema: + type: string + "404": + description: domain not found + schema: + type: string + security: + - BearerAuth: [] + - OrgKeyAuth: [] + - OrgSecretAuth: [] + summary: Create a record set (pending; Archer will UPSERT to Route 53) + tags: + - DNS + /dns/domains/{id}: + delete: + consumes: + - application/json + operationId: DeleteDomain + parameters: + - description: Organization UUID + in: header + name: X-Org-ID + type: string + - description: Domain ID (UUID) + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + "403": + description: organization required + schema: + type: string + "404": + description: not found + schema: + type: string + security: + - BearerAuth: [] + - OrgKeyAuth: [] + - OrgSecretAuth: [] + summary: Delete a domain + tags: + - DNS + get: + consumes: + - application/json + operationId: GetDomain + parameters: + - description: Organization UUID + in: header + name: X-Org-ID + type: string + - description: Domain ID (UUID) + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.DomainResponse' + "401": + description: Unauthorized + schema: + type: string + "403": + description: organization required + schema: + type: string + "404": + description: not found + schema: + type: string + security: + - BearerAuth: [] + - OrgKeyAuth: [] + - OrgSecretAuth: [] + summary: Get a domain (org scoped) + tags: + - DNS + patch: + consumes: + - application/json + operationId: UpdateDomain + parameters: + - description: Organization UUID + in: header + name: X-Org-ID + type: string + - description: Domain ID (UUID) + in: path + name: id + required: true + type: string + - description: Fields to update + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.UpdateDomainRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.DomainResponse' + "400": + description: validation error + schema: + type: string + "403": + description: organization required + schema: + type: string + "404": + description: not found + schema: + type: string + security: + - BearerAuth: [] + - OrgKeyAuth: [] + - OrgSecretAuth: [] + summary: Update a domain (org scoped) + tags: + - DNS + /dns/records/{id}: + delete: + consumes: + - application/json + operationId: DeleteRecordSet + parameters: + - description: Organization UUID + in: header + name: X-Org-ID + type: string + - description: Record Set ID (UUID) + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "204": + description: No Content + "403": + description: organization required + schema: + type: string + "404": + description: not found + schema: + type: string + security: + - BearerAuth: [] + - OrgKeyAuth: [] + - OrgSecretAuth: [] + summary: Delete a record set (API removes row; worker can optionally handle + external deletion policy) + tags: + - DNS + patch: + consumes: + - application/json + operationId: UpdateRecordSet + parameters: + - description: Organization UUID + in: header + name: X-Org-ID + type: string + - description: Record Set ID (UUID) + in: path + name: id + required: true + type: string + - description: Fields to update + in: body + name: body + required: true + schema: + $ref: '#/definitions/dto.UpdateRecordSetRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.RecordSetResponse' + "400": + description: validation error + schema: + type: string + "403": + description: organization required + schema: + type: string + "404": + description: not found + schema: + type: string + security: + - BearerAuth: [] + - OrgKeyAuth: [] + - OrgSecretAuth: [] + summary: Update a record set (flips to pending for reconciliation) + tags: + - DNS /healthz: get: consumes: @@ -3621,6 +4146,57 @@ paths: summary: Update server (org scoped) tags: - Servers + /servers/{id}/reset-hostkey: + post: + consumes: + - application/json + description: Clears the stored SSH host key for this server. The next SSH connection + will re-learn the host key (trust-on-first-use). + operationId: ResetServerHostKey + parameters: + - description: Organization UUID + in: header + name: X-Org-ID + type: string + - description: Server ID (UUID) + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.ServerResponse' + "400": + description: invalid id + schema: + type: string + "401": + description: Unauthorized + schema: + type: string + "403": + description: organization required + schema: + type: string + "404": + description: not found + schema: + type: string + "500": + description: reset failed + schema: + type: string + security: + - BearerAuth: [] + - OrgKeyAuth: [] + - OrgSecretAuth: [] + summary: Reset SSH host key (org scoped) + tags: + - Servers /ssh: get: consumes: diff --git a/go.mod b/go.mod index 336053e..17c67a8 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,11 @@ require ( github.com/aws/aws-sdk-go-v2 v1.39.6 github.com/aws/aws-sdk-go-v2/config v1.31.18 github.com/aws/aws-sdk-go-v2/credentials v1.18.22 + github.com/aws/aws-sdk-go-v2/service/route53 v1.59.4 github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 github.com/coreos/go-oidc/v3 v3.16.0 github.com/dyaksa/archer v1.1.3 + github.com/gin-gonic/gin v1.11.0 github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/cors v1.2.2 github.com/go-chi/httprate v0.15.0 @@ -18,6 +20,7 @@ require ( github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 github.com/rs/zerolog v1.34.0 + github.com/sosedoff/pgweb v0.16.2 github.com/spf13/cobra v1.10.1 github.com/spf13/viper v1.21.0 github.com/swaggo/http-swagger/v2 v2.0.2 @@ -32,7 +35,9 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect + github.com/BurntSushi/toml v1.1.0 // indirect github.com/KyleBanks/depth v1.2.1 // indirect + github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect @@ -47,8 +52,15 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.5 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 // indirect github.com/aws/smithy-go v1.23.2 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect @@ -59,23 +71,38 @@ require ( github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jessevdk/go-flags v1.5.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/jmoiron/sqlx v1.3.5 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/cpuid/v2 v2.2.10 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mr-tron/base58 v1.2.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.48.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sirupsen/logrus v1.9.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect @@ -84,12 +111,20 @@ require ( github.com/sv-tools/openapi v0.2.1 // indirect github.com/swaggo/files/v2 v2.0.0 // indirect github.com/swaggo/swag v1.8.1 // indirect + github.com/tuvistavie/securerandom v0.0.0-20140719024926-15512123a948 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect + go.uber.org/mock v0.5.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.46.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/tools v0.38.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gorm.io/driver/mysql v1.5.6 // indirect ) diff --git a/go.sum b/go.sum index 69135ae..6a44edb 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,13 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 h1:VauE2GcJNZFun2Och6tIT2zJZK1v6jxALQDA9BIji/E= +github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5/go.mod h1:gxOHeajFfvGQh/fxlC8oOKBe23xnnJTif00IFFbiT+o= github.com/agiledragon/gomonkey/v2 v2.3.1 h1:k+UnUY0EMNYUFUAQVETGY9uUTxjMdnUkP0ARyJS1zzs= github.com/agiledragon/gomonkey/v2 v2.3.1/go.mod h1:ap1AmDzcVOAz1YpeJ3TCzIgstoaWLA6jbbgxfB4w2iY= github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w= @@ -34,6 +38,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13 h1:kDqdFvMY github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.13/go.mod h1:lmKuogqSU3HzQCwZ9ZtcqOc5XGMqtDK7OIc2+DxiUEg= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 h1:zhBJXdhWIFZ1acfDYIhu4+LCzdUS2Vbcum7D01dXlHQ= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13/go.mod h1:JaaOeCE368qn2Hzi3sEzY6FgAZVCIYcC2nwbro2QCh8= +github.com/aws/aws-sdk-go-v2/service/route53 v1.59.4 h1:KEszjusgJ2dAqE5nSJY+5AHBkakfah8Sx6Vk3pjgrq8= +github.com/aws/aws-sdk-go-v2/service/route53 v1.59.4/go.mod h1:TUbfYOisWZWyT2qjmlMh93ERw1Ry8G4q/yT2Q8TsDag= github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0 h1:ef6gIJR+xv/JQWwpa5FYirzoQctfSJm7tuDe3SZsUf8= github.com/aws/aws-sdk-go-v2/service/s3 v1.90.0/go.mod h1:+wArOOrcHUevqdto9k1tKOF5++YTe9JEcPSc9Tx2ZSw= github.com/aws/aws-sdk-go-v2/service/sso v1.30.1 h1:0JPwLz1J+5lEOfy/g0SURC9cxhbQ1lIMHMa+AHZSzz0= @@ -44,6 +50,16 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.40.0 h1:ZGDJVmlpPFiNFCb/I42nYVKUanJA github.com/aws/aws-sdk-go-v2/service/sts v1.40.0/go.mod h1:E19xDjpzPZC7LS2knI9E6BaRFDK43Eul7vd6rSq2HWk= github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM= github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow= github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -52,6 +68,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a h1:saTgr5tMLFnmy/yg3qDTft4rE5DY2uJ/cCxCe3q0XTU= +github.com/dchest/bcrypt_pbkdf v0.0.0-20150205184540-83f37f9c154a/go.mod h1:Bw9BbhOJVNR+t0jCqx2GC6zv0TGBsShs56Y3gfSCvl0= github.com/dyaksa/archer v1.1.3 h1:jfe51tSNzzscFpu+Vilm4SKb0Lnv6FR1yaGspjab4x4= github.com/dyaksa/archer v1.1.3/go.mod h1:IYSp67u14JHTNuvvy6gG1eaX2TPywXvfk1FiyZwVEK4= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -60,6 +78,10 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= @@ -89,6 +111,7 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= @@ -96,6 +119,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= @@ -103,8 +128,9 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -117,16 +143,22 @@ github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= -github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -137,6 +169,7 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -147,12 +180,23 @@ github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA= github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= +github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/otiai10/copy v1.7.0 h1:hVoPiN+t+7d2nzzwMiDHPSOogsWAStewq3TwU05+clE= github.com/otiai10/copy v1.7.0/go.mod h1:rmRl6QPdJj6EiUqXQ/4Nn2lLXoNQjFCQbbNrxgc/t3U= @@ -162,14 +206,30 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= +github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sosedoff/pgweb v0.16.2 h1:1F1CWlCLSEgSctMva+nYuUibdhyiCUzlXyU5MQUJbFM= +github.com/sosedoff/pgweb v0.16.2/go.mod h1:ER7fsBddI3h7MQKO5RsUPi7Q/PWZYSKcI61kTp369Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= @@ -189,6 +249,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -208,42 +269,56 @@ github.com/swaggo/swag v1.8.1 h1:JuARzFX1Z1njbCGz+ZytBR15TFJwF2Q7fu8puJHhQYI= github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ= github.com/swaggo/swag/v2 v2.0.0-rc4 h1:SZ8cK68gcV6cslwrJMIOqPkJELRwq4gmjvk77MrvHvY= github.com/swaggo/swag/v2 v2.0.0-rc4/go.mod h1:Ow7Y8gF16BTCDn8YxZbyKn8FkMLRUHekv1kROJZpbvE= +github.com/tuvistavie/securerandom v0.0.0-20140719024926-15512123a948 h1:yL0l/u242MzDP6D0B5vGC+wxm5WRY+alQZy+dJk3bFI= +github.com/tuvistavie/securerandom v0.0.0-20140719024926-15512123a948/go.mod h1:a06d/M1pxWi51qiSrfGMHaEydtuXT06nha8N2aNQuXk= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= -golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= -golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200219091948-cb0a6d8edb6c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -251,8 +326,6 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= -golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -260,27 +333,25 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= -golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= -golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= -golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/api/mount_admin_routes.go b/internal/api/mount_admin_routes.go new file mode 100644 index 0000000..2dfb7c4 --- /dev/null +++ b/internal/api/mount_admin_routes.go @@ -0,0 +1,26 @@ +package api + +import ( + "net/http" + + "github.com/glueops/autoglue/internal/api/httpmiddleware" + "github.com/glueops/autoglue/internal/bg" + "github.com/glueops/autoglue/internal/handlers" + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +func mountAdminRoutes(r chi.Router, db *gorm.DB, jobs *bg.Jobs, authUser func(http.Handler) http.Handler) { + r.Route("/admin", func(admin chi.Router) { + admin.Route("/archer", func(archer chi.Router) { + archer.Use(authUser) + archer.Use(httpmiddleware.RequirePlatformAdmin()) + + archer.Get("/jobs", handlers.AdminListArcherJobs(db)) + archer.Post("/jobs", handlers.AdminEnqueueArcherJob(db, jobs)) + archer.Post("/jobs/{id}/retry", handlers.AdminRetryArcherJob(db)) + archer.Post("/jobs/{id}/cancel", handlers.AdminCancelArcherJob(db)) + archer.Get("/queues", handlers.AdminListArcherQueues(db)) + }) + }) +} diff --git a/internal/api/mount_annotation_routes.go b/internal/api/mount_annotation_routes.go new file mode 100644 index 0000000..505e6cf --- /dev/null +++ b/internal/api/mount_annotation_routes.go @@ -0,0 +1,20 @@ +package api + +import ( + "net/http" + + "github.com/glueops/autoglue/internal/handlers" + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +func mountAnnotationRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) { + r.Route("/annotations", func(a chi.Router) { + a.Use(authOrg) + a.Get("/", handlers.ListAnnotations(db)) + a.Post("/", handlers.CreateAnnotation(db)) + a.Get("/{id}", handlers.GetAnnotation(db)) + a.Patch("/{id}", handlers.UpdateAnnotation(db)) + a.Delete("/{id}", handlers.DeleteAnnotation(db)) + }) +} diff --git a/internal/api/mount_api_routes.go b/internal/api/mount_api_routes.go new file mode 100644 index 0000000..c0a10e0 --- /dev/null +++ b/internal/api/mount_api_routes.go @@ -0,0 +1,37 @@ +package api + +import ( + "github.com/glueops/autoglue/internal/api/httpmiddleware" + "github.com/glueops/autoglue/internal/bg" + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +func mountAPIRoutes(r chi.Router, db *gorm.DB, jobs *bg.Jobs) { + r.Route("/api", func(api chi.Router) { + api.Route("/v1", func(v1 chi.Router) { + authUser := httpmiddleware.AuthMiddleware(db, false) + authOrg := httpmiddleware.AuthMiddleware(db, true) + + // shared basics + mountMetaRoutes(v1) + mountAuthRoutes(v1, db) + + // admin + mountAdminRoutes(v1, db, jobs, authUser) + + // user/org scoped + mountMeRoutes(v1, db, authUser) + mountOrgRoutes(v1, db, authUser, authOrg) + + mountCredentialRoutes(v1, db, authOrg) + mountSSHRoutes(v1, db, authOrg) + mountServerRoutes(v1, db, authOrg) + mountTaintRoutes(v1, db, authOrg) + mountLabelRoutes(v1, db, authOrg) + mountAnnotationRoutes(v1, db, authOrg) + mountNodePoolRoutes(v1, db, authOrg) + mountDNSRoutes(v1, db, authOrg) + }) + }) +} diff --git a/internal/api/mount_auth_routes.go b/internal/api/mount_auth_routes.go new file mode 100644 index 0000000..c30f867 --- /dev/null +++ b/internal/api/mount_auth_routes.go @@ -0,0 +1,16 @@ +package api + +import ( + "github.com/glueops/autoglue/internal/handlers" + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +func mountAuthRoutes(r chi.Router, db *gorm.DB) { + r.Route("/auth", func(a chi.Router) { + a.Post("/{provider}/start", handlers.AuthStart(db)) + a.Get("/{provider}/callback", handlers.AuthCallback(db)) + a.Post("/refresh", handlers.Refresh(db)) + a.Post("/logout", handlers.Logout(db)) + }) +} diff --git a/internal/api/mount_credential_routes.go b/internal/api/mount_credential_routes.go new file mode 100644 index 0000000..527ced4 --- /dev/null +++ b/internal/api/mount_credential_routes.go @@ -0,0 +1,21 @@ +package api + +import ( + "net/http" + + "github.com/glueops/autoglue/internal/handlers" + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +func mountCredentialRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) { + r.Route("/credentials", func(c chi.Router) { + c.Use(authOrg) + c.Get("/", handlers.ListCredentials(db)) + c.Post("/", handlers.CreateCredential(db)) + c.Get("/{id}", handlers.GetCredential(db)) + c.Patch("/{id}", handlers.UpdateCredential(db)) + c.Delete("/{id}", handlers.DeleteCredential(db)) + c.Post("/{id}/reveal", handlers.RevealCredential(db)) + }) +} diff --git a/internal/api/mount_db_studio.go b/internal/api/mount_db_studio.go new file mode 100644 index 0000000..437421a --- /dev/null +++ b/internal/api/mount_db_studio.go @@ -0,0 +1,53 @@ +package api + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + pgapi "github.com/sosedoff/pgweb/pkg/api" + pgclient "github.com/sosedoff/pgweb/pkg/client" + pgcmd "github.com/sosedoff/pgweb/pkg/command" +) + +func PgwebHandler(dbURL, prefix string, readonly bool) (http.Handler, error) { + // Normalize prefix for pgweb: + // - no leading slash + // - always trailing slash if not empty + prefix = strings.Trim(prefix, "/") + if prefix != "" { + prefix = prefix + "/" + } + + pgcmd.Opts = pgcmd.Options{ + URL: dbURL, + Prefix: prefix, // e.g. "db-studio/" + ReadOnly: readonly, + Sessions: false, + LockSession: true, + SkipOpen: true, + } + + cli, err := pgclient.NewFromUrl(dbURL, nil) + if err != nil { + return nil, err + } + if readonly { + _ = cli.SetReadOnlyMode() + } + + if err := cli.Test(); err != nil { + return nil, err + } + + pgapi.DbClient = cli + + gin.SetMode(gin.ReleaseMode) + g := gin.New() + g.Use(gin.Recovery()) + + pgapi.SetupRoutes(g) + pgapi.SetupMetrics(g) + + return g, nil +} diff --git a/internal/api/mount_dns_routes.go b/internal/api/mount_dns_routes.go new file mode 100644 index 0000000..0ecb56e --- /dev/null +++ b/internal/api/mount_dns_routes.go @@ -0,0 +1,26 @@ +package api + +import ( + "net/http" + + "github.com/glueops/autoglue/internal/handlers" + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +func mountDNSRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) { + r.Route("/dns", func(d chi.Router) { + d.Use(authOrg) + + d.Get("/domains", handlers.ListDomains(db)) + d.Post("/domains", handlers.CreateDomain(db)) + d.Get("/domains/{id}", handlers.GetDomain(db)) + d.Patch("/domains/{id}", handlers.UpdateDomain(db)) + d.Delete("/domains/{id}", handlers.DeleteDomain(db)) + + d.Get("/domains/{domain_id}/records", handlers.ListRecordSets(db)) + d.Post("/domains/{domain_id}/records", handlers.CreateRecordSet(db)) + d.Patch("/records/{id}", handlers.UpdateRecordSet(db)) + d.Delete("/records/{id}", handlers.DeleteRecordSet(db)) + }) +} diff --git a/internal/api/mount_label_routes.go b/internal/api/mount_label_routes.go new file mode 100644 index 0000000..94d3404 --- /dev/null +++ b/internal/api/mount_label_routes.go @@ -0,0 +1,20 @@ +package api + +import ( + "net/http" + + "github.com/glueops/autoglue/internal/handlers" + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +func mountLabelRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) { + r.Route("/labels", func(l chi.Router) { + l.Use(authOrg) + l.Get("/", handlers.ListLabels(db)) + l.Post("/", handlers.CreateLabel(db)) + l.Get("/{id}", handlers.GetLabel(db)) + l.Patch("/{id}", handlers.UpdateLabel(db)) + l.Delete("/{id}", handlers.DeleteLabel(db)) + }) +} diff --git a/internal/api/mount_me_routes.go b/internal/api/mount_me_routes.go new file mode 100644 index 0000000..477bf17 --- /dev/null +++ b/internal/api/mount_me_routes.go @@ -0,0 +1,22 @@ +package api + +import ( + "net/http" + + "github.com/glueops/autoglue/internal/handlers" + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +func mountMeRoutes(r chi.Router, db *gorm.DB, authUser func(http.Handler) http.Handler) { + r.Route("/me", func(me chi.Router) { + me.Use(authUser) + + me.Get("/", handlers.GetMe(db)) + me.Patch("/", handlers.UpdateMe(db)) + + me.Get("/api-keys", handlers.ListUserAPIKeys(db)) + me.Post("/api-keys", handlers.CreateUserAPIKey(db)) + me.Delete("/api-keys/{id}", handlers.DeleteUserAPIKey(db)) + }) +} diff --git a/internal/api/mount_meta_routes.go b/internal/api/mount_meta_routes.go new file mode 100644 index 0000000..6cf1756 --- /dev/null +++ b/internal/api/mount_meta_routes.go @@ -0,0 +1,13 @@ +package api + +import ( + "github.com/glueops/autoglue/internal/handlers" + "github.com/go-chi/chi/v5" +) + +func mountMetaRoutes(r chi.Router) { + // Versioned JWKS for swagger + r.Get("/.well-known/jwks.json", handlers.JWKSHandler) + r.Get("/healthz", handlers.HealthCheck) + r.Get("/version", handlers.Version) +} diff --git a/internal/api/mount_node_pool_routes.go b/internal/api/mount_node_pool_routes.go new file mode 100644 index 0000000..84e7040 --- /dev/null +++ b/internal/api/mount_node_pool_routes.go @@ -0,0 +1,40 @@ +package api + +import ( + "net/http" + + "github.com/glueops/autoglue/internal/handlers" + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +func mountNodePoolRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) { + r.Route("/node-pools", func(n chi.Router) { + n.Use(authOrg) + n.Get("/", handlers.ListNodePools(db)) + n.Post("/", handlers.CreateNodePool(db)) + n.Get("/{id}", handlers.GetNodePool(db)) + n.Patch("/{id}", handlers.UpdateNodePool(db)) + n.Delete("/{id}", handlers.DeleteNodePool(db)) + + // Servers + n.Get("/{id}/servers", handlers.ListNodePoolServers(db)) + n.Post("/{id}/servers", handlers.AttachNodePoolServers(db)) + n.Delete("/{id}/servers/{serverId}", handlers.DetachNodePoolServer(db)) + + // Taints + n.Get("/{id}/taints", handlers.ListNodePoolTaints(db)) + n.Post("/{id}/taints", handlers.AttachNodePoolTaints(db)) + n.Delete("/{id}/taints/{taintId}", handlers.DetachNodePoolTaint(db)) + + // Labels + n.Get("/{id}/labels", handlers.ListNodePoolLabels(db)) + n.Post("/{id}/labels", handlers.AttachNodePoolLabels(db)) + n.Delete("/{id}/labels/{labelId}", handlers.DetachNodePoolLabel(db)) + + // Annotations + n.Get("/{id}/annotations", handlers.ListNodePoolAnnotations(db)) + n.Post("/{id}/annotations", handlers.AttachNodePoolAnnotations(db)) + n.Delete("/{id}/annotations/{annotationId}", handlers.DetachNodePoolAnnotation(db)) + }) +} diff --git a/internal/api/mount_org_routes.go b/internal/api/mount_org_routes.go new file mode 100644 index 0000000..00fb4e4 --- /dev/null +++ b/internal/api/mount_org_routes.go @@ -0,0 +1,35 @@ +package api + +import ( + "net/http" + + "github.com/glueops/autoglue/internal/handlers" + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +func mountOrgRoutes(r chi.Router, db *gorm.DB, authUser, authOrg func(http.Handler) http.Handler) { + r.Route("/orgs", func(o chi.Router) { + o.Use(authUser) + o.Get("/", handlers.ListMyOrgs(db)) + o.Post("/", handlers.CreateOrg(db)) + + o.Group(func(og chi.Router) { + og.Use(authOrg) + + og.Get("/{id}", handlers.GetOrg(db)) + og.Patch("/{id}", handlers.UpdateOrg(db)) + og.Delete("/{id}", handlers.DeleteOrg(db)) + + // members + og.Get("/{id}/members", handlers.ListMembers(db)) + og.Post("/{id}/members", handlers.AddOrUpdateMember(db)) + og.Delete("/{id}/members/{user_id}", handlers.RemoveMember(db)) + + // org-scoped key/secret pair + og.Get("/{id}/api-keys", handlers.ListOrgKeys(db)) + og.Post("/{id}/api-keys", handlers.CreateOrgKey(db)) + og.Delete("/{id}/api-keys/{key_id}", handlers.DeleteOrgKey(db)) + }) + }) +} diff --git a/internal/api/mount_pprof_routes.go b/internal/api/mount_pprof_routes.go new file mode 100644 index 0000000..27f92d1 --- /dev/null +++ b/internal/api/mount_pprof_routes.go @@ -0,0 +1,24 @@ +package api + +import ( + httpPprof "net/http/pprof" + + "github.com/go-chi/chi/v5" +) + +func mountPprofRoutes(r chi.Router) { + r.Route("/debug/pprof", func(pr chi.Router) { + pr.Get("/", httpPprof.Index) + pr.Get("/cmdline", httpPprof.Cmdline) + pr.Get("/profile", httpPprof.Profile) + pr.Get("/symbol", httpPprof.Symbol) + pr.Get("/trace", httpPprof.Trace) + + pr.Handle("/allocs", httpPprof.Handler("allocs")) + pr.Handle("/block", httpPprof.Handler("block")) + pr.Handle("/goroutine", httpPprof.Handler("goroutine")) + pr.Handle("/heap", httpPprof.Handler("heap")) + pr.Handle("/mutex", httpPprof.Handler("mutex")) + pr.Handle("/threadcreate", httpPprof.Handler("threadcreate")) + }) +} diff --git a/internal/api/mount_server_routes.go b/internal/api/mount_server_routes.go new file mode 100644 index 0000000..0712d2e --- /dev/null +++ b/internal/api/mount_server_routes.go @@ -0,0 +1,21 @@ +package api + +import ( + "net/http" + + "github.com/glueops/autoglue/internal/handlers" + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +func mountServerRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) { + r.Route("/servers", func(s chi.Router) { + s.Use(authOrg) + s.Get("/", handlers.ListServers(db)) + s.Post("/", handlers.CreateServer(db)) + s.Get("/{id}", handlers.GetServer(db)) + s.Patch("/{id}", handlers.UpdateServer(db)) + s.Delete("/{id}", handlers.DeleteServer(db)) + s.Post("/{id}/reset-hostkey", handlers.ResetServerHostKey(db)) + }) +} diff --git a/internal/api/mount_ssh_routes.go b/internal/api/mount_ssh_routes.go new file mode 100644 index 0000000..33d183f --- /dev/null +++ b/internal/api/mount_ssh_routes.go @@ -0,0 +1,20 @@ +package api + +import ( + "net/http" + + "github.com/glueops/autoglue/internal/handlers" + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +func mountSSHRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) { + r.Route("/ssh", func(s chi.Router) { + s.Use(authOrg) + s.Get("/", handlers.ListPublicSshKeys(db)) + s.Post("/", handlers.CreateSSHKey(db)) + s.Get("/{id}", handlers.GetSSHKey(db)) + s.Delete("/{id}", handlers.DeleteSSHKey(db)) + s.Get("/{id}/download", handlers.DownloadSSHKey(db)) + }) +} diff --git a/internal/api/mount_swagger_routes.go b/internal/api/mount_swagger_routes.go new file mode 100644 index 0000000..ae08042 --- /dev/null +++ b/internal/api/mount_swagger_routes.go @@ -0,0 +1,15 @@ +package api + +import ( + "github.com/glueops/autoglue/docs" + "github.com/go-chi/chi/v5" + httpSwagger "github.com/swaggo/http-swagger/v2" +) + +func mountSwaggerRoutes(r chi.Router) { + r.Get("/swagger/*", httpSwagger.Handler( + httpSwagger.URL("swagger.json"), + )) + r.Get("/swagger/swagger.json", serveSwaggerFromEmbed(docs.SwaggerJSON, "application/json")) + r.Get("/swagger/swagger.yaml", serveSwaggerFromEmbed(docs.SwaggerYAML, "application/x-yaml")) +} diff --git a/internal/api/mount_taint_routes.go b/internal/api/mount_taint_routes.go new file mode 100644 index 0000000..1b5d312 --- /dev/null +++ b/internal/api/mount_taint_routes.go @@ -0,0 +1,20 @@ +package api + +import ( + "net/http" + + "github.com/glueops/autoglue/internal/handlers" + "github.com/go-chi/chi/v5" + "gorm.io/gorm" +) + +func mountTaintRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) { + r.Route("/taints", func(t chi.Router) { + t.Use(authOrg) + t.Get("/", handlers.ListTaints(db)) + t.Post("/", handlers.CreateTaint(db)) + t.Get("/{id}", handlers.GetTaint(db)) + t.Patch("/{id}", handlers.UpdateTaint(db)) + t.Delete("/{id}", handlers.DeleteTaint(db)) + }) +} diff --git a/internal/api/mw_security.go b/internal/api/mw_security.go index 312d524..2217003 100644 --- a/internal/api/mw_security.go +++ b/internal/api/mw_security.go @@ -36,7 +36,7 @@ func SecurityHeaders(next http.Handler) http.Handler { // Google font files "font-src 'self' data: https://fonts.gstatic.com", // HMR connections - "connect-src 'self' http://localhost:5173 ws://localhost:5173 ws://localhost:8080", + "connect-src 'self' http://localhost:5173 ws://localhost:5173 ws://localhost:8080 https://api.github.com", "frame-ancestors 'none'", }, "; ")) } else { @@ -53,7 +53,7 @@ func SecurityHeaders(next http.Handler) http.Handler { "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", "img-src 'self' data: blob:", "font-src 'self' data: https://fonts.gstatic.com", - "connect-src 'self'", + "connect-src 'self' ws://localhost:8080 https://api.github.com", "frame-ancestors 'none'", }, "; ")) } diff --git a/internal/api/routes.go b/internal/api/routes.go index 3468052..0f117fd 100644 --- a/internal/api/routes.go +++ b/internal/api/routes.go @@ -3,11 +3,10 @@ package api import ( "fmt" "net/http" - httpPprof "net/http/pprof" "os" + "strings" "time" - "github.com/glueops/autoglue/docs" "github.com/glueops/autoglue/internal/api/httpmiddleware" "github.com/glueops/autoglue/internal/bg" "github.com/glueops/autoglue/internal/config" @@ -23,8 +22,6 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" - - httpSwagger "github.com/swaggo/http-swagger/v2" ) func NewRouter(db *gorm.DB, jobs *bg.Jobs, studio http.Handler) http.Handler { @@ -38,7 +35,6 @@ func NewRouter(db *gorm.DB, jobs *bg.Jobs, studio http.Handler) http.Handler { r.Use(middleware.RealIP) r.Use(zeroLogMiddleware()) r.Use(middleware.Recoverer) - // r.Use(middleware.RedirectSlashes) r.Use(SecurityHeaders) r.Use(requestBodyLimit(10 << 20)) r.Use(httprate.LimitByIP(100, 1*time.Minute)) @@ -60,198 +56,44 @@ func NewRouter(db *gorm.DB, jobs *bg.Jobs, studio http.Handler) http.Handler { MaxAge: 600, })) - r.Use(middleware.AllowContentType("application/json")) + r.Use(middleware.Maybe( + middleware.AllowContentType("application/json"), + func(r *http.Request) bool { + // return true => run AllowContentType + // return false => skip AllowContentType for this request + return !strings.HasPrefix(r.URL.Path, "/db-studio") + })) + //r.Use(middleware.AllowContentType("application/json")) + // Unversioned, non-auth endpoints r.Get("/.well-known/jwks.json", handlers.JWKSHandler) - r.Route("/api", func(api chi.Router) { - api.Route("/v1", func(v1 chi.Router) { - authUser := httpmiddleware.AuthMiddleware(db, false) - authOrg := httpmiddleware.AuthMiddleware(db, true) - // Also serving a versioned JWKS for swagger, which uses BasePath - v1.Get("/.well-known/jwks.json", handlers.JWKSHandler) - - v1.Get("/healthz", handlers.HealthCheck) - v1.Get("/version", handlers.Version) - - v1.Route("/auth", func(a chi.Router) { - a.Post("/{provider}/start", handlers.AuthStart(db)) - a.Get("/{provider}/callback", handlers.AuthCallback(db)) - a.Post("/refresh", handlers.Refresh(db)) - a.Post("/logout", handlers.Logout(db)) - }) - - v1.Route("/admin", func(admin chi.Router) { - admin.Route("/archer", func(archer chi.Router) { - archer.Use(authUser) - archer.Use(httpmiddleware.RequirePlatformAdmin()) - - archer.Get("/jobs", handlers.AdminListArcherJobs(db)) - archer.Post("/jobs", handlers.AdminEnqueueArcherJob(db, jobs)) - archer.Post("/jobs/{id}/retry", handlers.AdminRetryArcherJob(db)) - archer.Post("/jobs/{id}/cancel", handlers.AdminCancelArcherJob(db)) - archer.Get("/queues", handlers.AdminListArcherQueues(db)) - }) - }) - - v1.Route("/me", func(me chi.Router) { - me.Use(authUser) - - me.Get("/", handlers.GetMe(db)) - me.Patch("/", handlers.UpdateMe(db)) - - me.Get("/api-keys", handlers.ListUserAPIKeys(db)) - me.Post("/api-keys", handlers.CreateUserAPIKey(db)) - me.Delete("/api-keys/{id}", handlers.DeleteUserAPIKey(db)) - }) - - v1.Route("/orgs", func(o chi.Router) { - o.Use(authUser) - o.Get("/", handlers.ListMyOrgs(db)) - o.Post("/", handlers.CreateOrg(db)) - - o.Group(func(og chi.Router) { - og.Use(authOrg) - og.Get("/{id}", handlers.GetOrg(db)) - og.Patch("/{id}", handlers.UpdateOrg(db)) - og.Delete("/{id}", handlers.DeleteOrg(db)) - - // members - og.Get("/{id}/members", handlers.ListMembers(db)) - og.Post("/{id}/members", handlers.AddOrUpdateMember(db)) - og.Delete("/{id}/members/{user_id}", handlers.RemoveMember(db)) - - // org-scoped key/secret pair - og.Get("/{id}/api-keys", handlers.ListOrgKeys(db)) - og.Post("/{id}/api-keys", handlers.CreateOrgKey(db)) - og.Delete("/{id}/api-keys/{key_id}", handlers.DeleteOrgKey(db)) - }) - }) - - v1.Route("/credentials", func(c chi.Router) { - c.Use(authOrg) - c.Get("/", handlers.ListCredentials(db)) - c.Post("/", handlers.CreateCredential(db)) - c.Get("/{id}", handlers.GetCredential(db)) - c.Patch("/{id}", handlers.UpdateCredential(db)) - c.Delete("/{id}", handlers.DeleteCredential(db)) - c.Post("/{id}/reveal", handlers.RevealCredential(db)) - }) - - v1.Route("/ssh", func(s chi.Router) { - s.Use(authOrg) - s.Get("/", handlers.ListPublicSshKeys(db)) - s.Post("/", handlers.CreateSSHKey(db)) - s.Get("/{id}", handlers.GetSSHKey(db)) - s.Delete("/{id}", handlers.DeleteSSHKey(db)) - s.Get("/{id}/download", handlers.DownloadSSHKey(db)) - }) - - v1.Route("/servers", func(s chi.Router) { - s.Use(authOrg) - s.Get("/", handlers.ListServers(db)) - s.Post("/", handlers.CreateServer(db)) - s.Get("/{id}", handlers.GetServer(db)) - s.Patch("/{id}", handlers.UpdateServer(db)) - s.Delete("/{id}", handlers.DeleteServer(db)) - }) - - v1.Route("/taints", func(s chi.Router) { - s.Use(authOrg) - s.Get("/", handlers.ListTaints(db)) - s.Post("/", handlers.CreateTaint(db)) - s.Get("/{id}", handlers.GetTaint(db)) - s.Patch("/{id}", handlers.UpdateTaint(db)) - s.Delete("/{id}", handlers.DeleteTaint(db)) - }) - - v1.Route("/labels", func(l chi.Router) { - l.Use(authOrg) - l.Get("/", handlers.ListLabels(db)) - l.Post("/", handlers.CreateLabel(db)) - l.Get("/{id}", handlers.GetLabel(db)) - l.Patch("/{id}", handlers.UpdateLabel(db)) - l.Delete("/{id}", handlers.DeleteLabel(db)) - }) - - v1.Route("/annotations", func(a chi.Router) { - a.Use(authOrg) - a.Get("/", handlers.ListAnnotations(db)) - a.Post("/", handlers.CreateAnnotation(db)) - a.Get("/{id}", handlers.GetAnnotation(db)) - a.Patch("/{id}", handlers.UpdateAnnotation(db)) - a.Delete("/{id}", handlers.DeleteAnnotation(db)) - }) - - v1.Route("/node-pools", func(n chi.Router) { - n.Use(authOrg) - n.Get("/", handlers.ListNodePools(db)) - n.Post("/", handlers.CreateNodePool(db)) - n.Get("/{id}", handlers.GetNodePool(db)) - n.Patch("/{id}", handlers.UpdateNodePool(db)) - n.Delete("/{id}", handlers.DeleteNodePool(db)) - - // Servers - n.Get("/{id}/servers", handlers.ListNodePoolServers(db)) - n.Post("/{id}/servers", handlers.AttachNodePoolServers(db)) - n.Delete("/{id}/servers/{serverId}", handlers.DetachNodePoolServer(db)) - - // Taints - n.Get("/{id}/taints", handlers.ListNodePoolTaints(db)) - n.Post("/{id}/taints", handlers.AttachNodePoolTaints(db)) - n.Delete("/{id}/taints/{taintId}", handlers.DetachNodePoolTaint(db)) - - // Labels - n.Get("/{id}/labels", handlers.ListNodePoolLabels(db)) - n.Post("/{id}/labels", handlers.AttachNodePoolLabels(db)) - n.Delete("/{id}/labels/{labelId}", handlers.DetachNodePoolLabel(db)) - - // Annotations - n.Get("/{id}/annotations", handlers.ListNodePoolAnnotations(db)) - n.Post("/{id}/annotations", handlers.AttachNodePoolAnnotations(db)) - n.Delete("/{id}/annotations/{annotationId}", handlers.DetachNodePoolAnnotation(db)) - }) - }) - }) + // Versioned API + mountAPIRoutes(r, db, jobs) + // Optional DB studio if studio != nil { r.Group(func(gr chi.Router) { authUser := httpmiddleware.AuthMiddleware(db, false) adminOnly := httpmiddleware.RequirePlatformAdmin() - gr.Use(authUser) - gr.Use(adminOnly) + gr.Use(authUser, adminOnly) gr.Mount("/db-studio", studio) }) } + // pprof if config.IsDebug() { - r.Route("/debug/pprof", func(pr chi.Router) { - pr.Get("/", httpPprof.Index) - pr.Get("/cmdline", httpPprof.Cmdline) - pr.Get("/profile", httpPprof.Profile) - pr.Get("/symbol", httpPprof.Symbol) - pr.Get("/trace", httpPprof.Trace) - - pr.Handle("/allocs", httpPprof.Handler("allocs")) - pr.Handle("/block", httpPprof.Handler("block")) - pr.Handle("/goroutine", httpPprof.Handler("goroutine")) - pr.Handle("/heap", httpPprof.Handler("heap")) - pr.Handle("/mutex", httpPprof.Handler("mutex")) - pr.Handle("/threadcreate", httpPprof.Handler("threadcreate")) - }) + mountPprofRoutes(r) } + // Swagger if config.IsSwaggerEnabled() { - r.Get("/swagger/*", httpSwagger.Handler( - httpSwagger.URL("swagger.json"), - )) - r.Get("/swagger/swagger.json", serveSwaggerFromEmbed(docs.SwaggerJSON, "application/json")) - r.Get("/swagger/swagger.yaml", serveSwaggerFromEmbed(docs.SwaggerYAML, "application/x-yaml")) + mountSwaggerRoutes(r) } + // UI dev/prod if config.IsUIDev() { fmt.Println("Running in development mode") - // Dev: isolate proxy from chi middlewares so WS upgrade can hijack. proxy, err := web.DevProxy("http://localhost:5173") if err != nil { log.Error().Err(err).Msg("dev proxy init failed") @@ -259,23 +101,20 @@ func NewRouter(db *gorm.DB, jobs *bg.Jobs, studio http.Handler) http.Handler { } mux := http.NewServeMux() - // Send API/Swagger/pprof to chi mux.Handle("/api/", r) mux.Handle("/api", r) mux.Handle("/swagger/", r) mux.Handle("/db-studio/", r) mux.Handle("/debug/pprof/", r) - // Everything else (/, /brand-preview, assets) → proxy (no middlewares) mux.Handle("/", proxy) - return mux + } + + fmt.Println("Running in production mode") + if h, err := web.SPAHandler(); err == nil { + r.NotFound(h.ServeHTTP) } else { - fmt.Println("Running in production mode") - if h, err := web.SPAHandler(); err == nil { - r.NotFound(h.ServeHTTP) - } else { - log.Error().Err(err).Msg("spa handler init failed") - } + log.Error().Err(err).Msg("spa handler init failed") } return r diff --git a/internal/app/runtime.go b/internal/app/runtime.go index 8122978..673f5e5 100644 --- a/internal/app/runtime.go +++ b/internal/app/runtime.go @@ -41,6 +41,8 @@ func NewRuntime() *Runtime { &models.NodePool{}, &models.Cluster{}, &models.Credential{}, + &models.Domain{}, + &models.RecordSet{}, ) if err != nil { diff --git a/internal/bg/backup_s3.go b/internal/bg/backup_s3.go index a36e7a9..748e595 100644 --- a/internal/bg/backup_s3.go +++ b/internal/bg/backup_s3.go @@ -29,7 +29,7 @@ import ( ) type DbBackupArgs struct { - // kept in case you want to change retention or add dry-run later + IntervalS int `json:"interval_seconds,omitempty"` } type s3Scope struct { @@ -44,6 +44,13 @@ type encAWS struct { func DbBackupWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn { return func(ctx context.Context, j job.Job) (any, error) { + args := DbBackupArgs{IntervalS: 3600} + _ = j.ParseArguments(&args) + + if args.IntervalS <= 0 { + args.IntervalS = 3600 + } + if err := DbBackup(ctx, db); err != nil { return nil, err } @@ -53,7 +60,7 @@ func DbBackupWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn { queue = "db_backup_s3" } - next := time.Now().UTC().Add(1 * time.Hour) + next := time.Now().Add(time.Duration(args.IntervalS) * time.Second) payload := DbBackupArgs{} @@ -73,7 +80,6 @@ func DbBackupWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn { func DbBackup(ctx context.Context, db *gorm.DB) error { cfg, err := config.Load() - log.Info().Err(err).Msg("loading config") if err != nil { return fmt.Errorf("load config: %w", err) } diff --git a/internal/bg/bastion.go b/internal/bg/bastion.go index 972eb16..d871e66 100644 --- a/internal/bg/bastion.go +++ b/internal/bg/bastion.go @@ -2,8 +2,8 @@ package bg import ( "context" + "encoding/base64" "fmt" - "log" "net" "strings" "time" @@ -13,13 +13,16 @@ import ( "github.com/glueops/autoglue/internal/models" "github.com/glueops/autoglue/internal/utils" "github.com/google/uuid" + "github.com/rs/zerolog/log" "golang.org/x/crypto/ssh" "gorm.io/gorm" ) // ----- Public types ----- -type BastionBootstrapArgs struct{} +type BastionBootstrapArgs struct { + IntervalS int `json:"interval_seconds,omitempty"` +} type BastionBootstrapFailure struct { ID uuid.UUID `json:"id"` @@ -39,11 +42,17 @@ type BastionBootstrapResult struct { // ----- Worker ----- -func BastionBootstrapWorker(db *gorm.DB) archer.WorkerFn { +func BastionBootstrapWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn { return func(ctx context.Context, j job.Job) (any, error) { + args := BastionBootstrapArgs{IntervalS: 120} jobID := j.ID start := time.Now() + _ = j.ParseArguments(&args) + if args.IntervalS <= 0 { + args.IntervalS = 120 + } + var servers []models.Server if err := db. Preload("SshKey"). @@ -105,7 +114,7 @@ func BastionBootstrapWorker(db *gorm.DB) archer.WorkerFn { // 4) SSH + install docker host := net.JoinHostPort(*s.PublicIPAddress, "22") runCtx, cancel := context.WithTimeout(ctx, perHostTimeout) - out, err := sshInstallDockerWithOutput(runCtx, host, s.SSHUser, []byte(privKey)) + out, err := sshInstallDockerWithOutput(runCtx, db, s, host, s.SSHUser, []byte(privKey)) cancel() if err != nil { @@ -147,9 +156,17 @@ func BastionBootstrapWorker(db *gorm.DB) archer.WorkerFn { Failures: failures, } - // log.Printf("[bastion] level=INFO job=%s step=finish processed=%d ready=%d failed=%d elapsed_ms=%d", - // jobID, proc, ok, fail, res.ElapsedMs) + log.Debug().Int("processed", proc).Int("ready", ok).Int("failed", fail).Msg("[bastion] reconcile tick ok") + next := time.Now().Add(time.Duration(args.IntervalS) * time.Second) + _, _ = jobs.Enqueue( + ctx, + uuid.NewString(), + "bootstrap_bastion", + args, + archer.WithScheduleTime(next), + archer.WithMaxRetries(1), + ) return res, nil } } @@ -187,16 +204,24 @@ func logHostInfo(jobID string, s *models.Server, step, msg string, kv ...any) { // ----- SSH & command execution ----- // returns combined stdout/stderr so caller can log it on error -func sshInstallDockerWithOutput(ctx context.Context, host, user string, privateKeyPEM []byte) (string, error) { +func sshInstallDockerWithOutput( + ctx context.Context, + db *gorm.DB, + s *models.Server, + host, user string, + privateKeyPEM []byte, +) (string, error) { signer, err := ssh.ParsePrivateKey(privateKeyPEM) if err != nil { return "", fmt.Errorf("parse private key: %w", err) } + hkcb := makeDBHostKeyCallback(db, s) + config := &ssh.ClientConfig{ User: user, Auth: []ssh.AuthMethod{ssh.PublicKeys(signer)}, - HostKeyCallback: ssh.InsecureIgnoreHostKey(), // TODO: known_hosts verification + HostKeyCallback: hkcb, Timeout: 30 * time.Second, } @@ -494,3 +519,38 @@ func wrapSSHError(err error, output string) error { func sshEscape(s string) string { return fmt.Sprintf("%q", s) } + +// makeDBHostKeyCallback returns a HostKeyCallback bound to a specific server row. +// TOFU semantics: +// - If s.SSHHostKey is empty: store the current key in DB and accept. +// - If s.SSHHostKey is set: require exact match, else error (possible MITM/reinstall). +func makeDBHostKeyCallback(db *gorm.DB, s *models.Server) ssh.HostKeyCallback { + return func(hostname string, remote net.Addr, key ssh.PublicKey) error { + algo := key.Type() + enc := base64.StdEncoding.EncodeToString(key.Marshal()) + + // First-time connect: persist key (TOFU). + if s.SSHHostKey == "" { + if err := db.Model(&models.Server{}). + Where("id = ? AND (ssh_host_key IS NULL or ssh_host_key = '')", s.ID). + Updates(map[string]any{ + "ssh_host_key": enc, + "ssh_host_key_algo": algo, + }).Error; err != nil { + return fmt.Errorf("store new host key for %s (%s): %w", hostname, s.ID, err) + } + + s.SSHHostKey = enc + s.SSHHostKeyAlgo = algo + return nil + } + + if s.SSHHostKeyAlgo != algo || s.SSHHostKey != enc { + return fmt.Errorf( + "host key mismatch for %s (server_id=%s, stored=%s/%s, got=%s/%s) - POSSIBLE MITM or host reinstalled", + hostname, s.ID, s.SSHHostKeyAlgo, s.SSHHostKey, algo, enc, + ) + } + return nil + } +} diff --git a/internal/bg/bg.go b/internal/bg/bg.go index 0f6d3ad..414e18d 100644 --- a/internal/bg/bg.go +++ b/internal/bg/bg.go @@ -75,7 +75,7 @@ func NewJobs(gdb *gorm.DB, dbUrl string) (*Jobs, error) { c.Register( "bootstrap_bastion", - BastionBootstrapWorker(gdb), + BastionBootstrapWorker(gdb, jobs), archer.WithInstances(instances), archer.WithTimeout(time.Duration(timeoutSec)*time.Second), ) @@ -100,6 +100,13 @@ func NewJobs(gdb *gorm.DB, dbUrl string) (*Jobs, error) { archer.WithInstances(1), archer.WithTimeout(15*time.Minute), ) + + c.Register( + "dns_reconcile", + DNSReconsileWorker(gdb, jobs), + archer.WithInstances(1), + archer.WithTimeout(2*time.Minute), + ) return jobs, nil } diff --git a/internal/bg/dns.go b/internal/bg/dns.go new file mode 100644 index 0000000..edcbdcc --- /dev/null +++ b/internal/bg/dns.go @@ -0,0 +1,597 @@ +package bg + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + "time" + + "github.com/dyaksa/archer" + "github.com/dyaksa/archer/job" + "github.com/glueops/autoglue/internal/handlers/dto" + "github.com/glueops/autoglue/internal/models" + "github.com/glueops/autoglue/internal/utils" + "github.com/google/uuid" + "github.com/rs/zerolog/log" + "gorm.io/gorm" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + r53 "github.com/aws/aws-sdk-go-v2/service/route53" + r53types "github.com/aws/aws-sdk-go-v2/service/route53/types" +) + +/************* args & small DTOs *************/ + +type DNSReconcileArgs struct { + MaxDomains int `json:"max_domains,omitempty"` + MaxRecords int `json:"max_records,omitempty"` + IntervalS int `json:"interval_seconds,omitempty"` +} + +// TXT marker content (compact) +type ownershipMarker struct { + Ver string `json:"v"` // "ag1" + Org string `json:"org"` // org UUID + Rec string `json:"rec"` // record UUID + Fp string `json:"fp"` // short fp (first 16 of sha256) +} + +// ExternalDNS poison owner id – MUST NOT match any real external-dns --txt-owner-id +const externalDNSPoisonOwner = "autoglue-lock" + +// ExternalDNS poison content – fake owner so real external-dns skips it. +const externalDNSPoisonValue = "heritage=external-dns,external-dns/owner=" + externalDNSPoisonOwner + ",external-dns/resource=manual/autoglue" + +/************* entrypoint worker *************/ + +func DNSReconsileWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn { + return func(ctx context.Context, j job.Job) (any, error) { + args := DNSReconcileArgs{MaxDomains: 25, MaxRecords: 100, IntervalS: 30} + _ = j.ParseArguments(&args) + + if args.MaxDomains <= 0 { + args.MaxDomains = 25 + } + if args.MaxRecords <= 0 { + args.MaxRecords = 100 + } + if args.IntervalS <= 0 { + args.IntervalS = 30 + } + + processedDomains, processedRecords, err := reconcileDNSOnce(ctx, db, args) + if err != nil { + log.Error().Err(err).Msg("[dns] reconcile tick failed") + } else { + log.Debug(). + Int("domains", processedDomains). + Int("records", processedRecords). + Msg("[dns] reconcile tick ok") + } + + next := time.Now().Add(time.Duration(args.IntervalS) * time.Second) + _, _ = jobs.Enqueue(ctx, uuid.NewString(), "dns_reconcile", args, + archer.WithScheduleTime(next), + archer.WithMaxRetries(1), + ) + + return map[string]any{ + "domains_processed": processedDomains, + "records_processed": processedRecords, + }, nil + } +} + +/************* core tick *************/ + +func reconcileDNSOnce(ctx context.Context, db *gorm.DB, args DNSReconcileArgs) (int, int, error) { + var domains []models.Domain + + // 1) validate/backfill pending domains + if err := db. + Where("status = ?", "pending"). + Order("created_at ASC"). + Limit(args.MaxDomains). + Find(&domains).Error; err != nil { + return 0, 0, err + } + + domainsProcessed := 0 + for i := range domains { + if err := processDomain(ctx, db, &domains[i]); err != nil { + log.Error().Err(err).Str("domain", domains[i].DomainName).Msg("[dns] domain processing failed") + } else { + domainsProcessed++ + } + } + + // 2) apply pending record sets for ready domains + var readyDomains []models.Domain + if err := db.Where("status = ?", "ready").Find(&readyDomains).Error; err != nil { + return domainsProcessed, 0, err + } + + recordsProcessed := 0 + for i := range readyDomains { + n, err := processPendingRecordsForDomain(ctx, db, &readyDomains[i], args.MaxRecords) + if err != nil { + log.Error().Err(err).Str("domain", readyDomains[i].DomainName).Msg("[dns] record processing failed") + continue + } + recordsProcessed += n + } + + return domainsProcessed, recordsProcessed, nil +} + +/************* domain processing *************/ + +func processDomain(ctx context.Context, db *gorm.DB, d *models.Domain) error { + orgID := d.OrganizationID + + // 1) Load credential (org-guarded) + var cred models.Credential + if err := db.Where("id = ? AND organization_id = ?", d.CredentialID, orgID).First(&cred).Error; err != nil { + return setDomainFailed(db, d, fmt.Errorf("credential not found: %w", err)) + } + + // 2) Decrypt → dto.AWSCredential + secret, err := utils.DecryptForOrg(orgID, cred.EncryptedData, cred.IV, cred.Tag, db) + if err != nil { + return setDomainFailed(db, d, fmt.Errorf("decrypt: %w", err)) + } + var awsCred dto.AWSCredential + if err := jsonUnmarshalStrict([]byte(secret), &awsCred); err != nil { + return setDomainFailed(db, d, fmt.Errorf("secret decode: %w", err)) + } + + // 3) Client + r53c, _, err := newRoute53Client(ctx, awsCred) + if err != nil { + return setDomainFailed(db, d, err) + } + + // 4) Backfill zone id if missing + zoneID := strings.TrimSpace(d.ZoneID) + if zoneID == "" { + zid, err := findHostedZoneID(ctx, r53c, d.DomainName) + if err != nil { + return setDomainFailed(db, d, fmt.Errorf("discover zone id: %w", err)) + } + zoneID = zid + d.ZoneID = zoneID + } + + // 5) Sanity: can fetch zone + if _, err := r53c.GetHostedZone(ctx, &r53.GetHostedZoneInput{Id: aws.String(zoneID)}); err != nil { + return setDomainFailed(db, d, fmt.Errorf("get hosted zone: %w", err)) + } + + // 6) Mark ready + d.Status = "ready" + d.LastError = "" + if err := db.Save(d).Error; err != nil { + return err + } + return nil +} + +func setDomainFailed(db *gorm.DB, d *models.Domain, cause error) error { + d.Status = "failed" + d.LastError = truncateErr(cause.Error()) + _ = db.Save(d).Error + return cause +} + +/************* record processing *************/ + +func processPendingRecordsForDomain(ctx context.Context, db *gorm.DB, d *models.Domain, max int) (int, error) { + orgID := d.OrganizationID + + // reload credential + var cred models.Credential + if err := db.Where("id = ? AND organization_id = ?", d.CredentialID, orgID).First(&cred).Error; err != nil { + return 0, err + } + + secret, err := utils.DecryptForOrg(orgID, cred.EncryptedData, cred.IV, cred.Tag, db) + if err != nil { + return 0, err + } + + var awsCred dto.AWSCredential + if err := jsonUnmarshalStrict([]byte(secret), &awsCred); err != nil { + return 0, err + } + r53c, _, err := newRoute53Client(ctx, awsCred) + if err != nil { + return 0, err + } + + var records []models.RecordSet + if err := db. + Where("domain_id = ? AND status = ?", d.ID, "pending"). + Order("created_at ASC"). + Limit(max). + Find(&records).Error; err != nil { + return 0, err + } + + applied := 0 + for i := range records { + if err := applyRecord(ctx, db, r53c, d, &records[i]); err != nil { + log.Error().Err(err).Str("rr", records[i].Name).Msg("[dns] apply record failed") + _ = setRecordFailed(db, &records[i], err) + continue + } + applied++ + } + return applied, nil +} + +// core write + ownership + external-dns hardening + +func applyRecord(ctx context.Context, db *gorm.DB, r53c *r53.Client, d *models.Domain, r *models.RecordSet) error { + zoneID := strings.TrimSpace(d.ZoneID) + if zoneID == "" { + return errors.New("domain has no zone_id") + } + + rt := strings.ToUpper(r.Type) + + // FQDN & marker + fq := recordFQDN(r.Name, d.DomainName) // ends with "." + mname := markerName(fq) + expected := buildMarkerValue(d.OrganizationID.String(), r.ID.String(), r.Fingerprint) + + // ---- ExternalDNS preflight ---- + extOwned, err := hasExternalDNSOwnership(ctx, r53c, zoneID, fq, rt) + if err != nil { + return fmt.Errorf("external_dns_lookup: %w", err) + } + if extOwned { + r.Owner = "external" + _ = db.Save(r).Error + return fmt.Errorf("ownership_conflict: external-dns claims %s; refusing to modify", strings.TrimSuffix(fq, ".")) + } + + // ---- Autoglue ownership preflight via _autoglue. TXT ---- + markerVals, err := getMarkerTXTValues(ctx, r53c, zoneID, mname) + if err != nil { + return fmt.Errorf("marker lookup: %w", err) + } + hasForeignOwner := false + hasOurExact := false + for _, v := range markerVals { + mk, ok := parseMarkerValue(v) + if !ok { + continue + } + switch { + case mk.Org == d.OrganizationID.String() && mk.Rec == r.ID.String() && mk.Fp == shortFP(r.Fingerprint): + hasOurExact = true + case mk.Org != d.OrganizationID.String() || mk.Rec != r.ID.String(): + hasForeignOwner = true + } + } + if hasForeignOwner { + r.Owner = "external" + _ = db.Save(r).Error + return fmt.Errorf("ownership_conflict: marker for %s is owned by another controller; refusing to modify", strings.TrimSuffix(fq, ".")) + } + + // Build RR change (UPSERT) + rrChange := r53types.Change{ + Action: r53types.ChangeActionUpsert, + ResourceRecordSet: &r53types.ResourceRecordSet{ + Name: aws.String(fq), + Type: r53types.RRType(rt), + }, + } + + // Decode user values + var userVals []string + if len(r.Values) > 0 { + if err := jsonUnmarshalStrict([]byte(r.Values), &userVals); err != nil { + return fmt.Errorf("values decode: %w", err) + } + } + + // Quote TXT values as required by Route53 + recs := make([]r53types.ResourceRecord, 0, len(userVals)) + for _, v := range userVals { + v = strings.TrimSpace(v) + if rt == "TXT" && !(strings.HasPrefix(v, `"`) && strings.HasSuffix(v, `"`)) { + v = strconv.Quote(v) + } + recs = append(recs, r53types.ResourceRecord{Value: aws.String(v)}) + } + rrChange.ResourceRecordSet.ResourceRecords = recs + if r.TTL != nil { + ttl := int64(*r.TTL) + rrChange.ResourceRecordSet.TTL = aws.Int64(ttl) + } + + // Build marker TXT change (UPSERT) + markerChange := r53types.Change{ + Action: r53types.ChangeActionUpsert, + ResourceRecordSet: &r53types.ResourceRecordSet{ + Name: aws.String(mname), + Type: r53types.RRTypeTxt, + TTL: aws.Int64(300), + ResourceRecords: []r53types.ResourceRecord{ + {Value: aws.String(strconv.Quote(expected))}, + }, + }, + } + + // Build external-dns poison TXT changes + poisonChanges := buildExternalDNSPoisonTXTChanges(fq, rt) + + // Apply all in one batch (atomic-ish) + changes := []r53types.Change{rrChange, markerChange} + changes = append(changes, poisonChanges...) + + _, err = r53c.ChangeResourceRecordSets(ctx, &r53.ChangeResourceRecordSetsInput{ + HostedZoneId: aws.String(zoneID), + ChangeBatch: &r53types.ChangeBatch{Changes: changes}, + }) + if err != nil { + return err + } + + // Success → mark ready & ownership + r.Status = "ready" + r.LastError = "" + r.Owner = "autoglue" + if err := db.Save(r).Error; err != nil { + return err + } + _ = hasOurExact // could be used to skip marker write in future + return nil +} + +func setRecordFailed(db *gorm.DB, r *models.RecordSet, cause error) error { + msg := truncateErr(cause.Error()) + r.Status = "failed" + r.LastError = msg + // classify ownership on conflict + if strings.HasPrefix(msg, "ownership_conflict") { + r.Owner = "external" + } else if r.Owner == "" || r.Owner == "unknown" { + r.Owner = "unknown" + } + _ = db.Save(r).Error + return cause +} + +/************* AWS helpers *************/ + +func newRoute53Client(ctx context.Context, cred dto.AWSCredential) (*r53.Client, *aws.Config, error) { + // Route53 is global, but config still wants a region + region := strings.TrimSpace(cred.Region) + if region == "" { + region = "us-east-1" + } + cfg, err := config.LoadDefaultConfig(ctx, + config.WithRegion(region), + config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( + cred.AccessKeyID, cred.SecretAccessKey, "", + )), + ) + if err != nil { + return nil, nil, err + } + return r53.NewFromConfig(cfg), &cfg, nil +} + +func findHostedZoneID(ctx context.Context, c *r53.Client, domain string) (string, error) { + d := normalizeDomain(domain) + out, err := c.ListHostedZonesByName(ctx, &r53.ListHostedZonesByNameInput{ + DNSName: aws.String(d), + }) + if err != nil { + return "", err + } + for _, hz := range out.HostedZones { + if strings.TrimSuffix(aws.ToString(hz.Name), ".") == d { + return trimZoneID(aws.ToString(hz.Id)), nil + } + } + return "", fmt.Errorf("hosted zone not found for %q", d) +} + +func trimZoneID(id string) string { + return strings.TrimPrefix(id, "/hostedzone/") +} + +func normalizeDomain(s string) string { + s = strings.TrimSpace(strings.ToLower(s)) + return strings.TrimSuffix(s, ".") +} + +func recordFQDN(name, domain string) string { + name = strings.TrimSpace(name) + if name == "" || name == "@" { + return normalizeDomain(domain) + "." + } + if strings.HasSuffix(name, ".") { + return name + } + return fmt.Sprintf("%s.%s.", name, normalizeDomain(domain)) +} + +/************* TXT marker / external-dns helpers *************/ + +func markerName(fqdn string) string { + trimmed := strings.TrimSuffix(fqdn, ".") + return "_autoglue." + trimmed + "." +} + +func shortFP(full string) string { + if len(full) > 16 { + return full[:16] + } + return full +} + +func buildMarkerValue(orgID, recID, fp string) string { + return "v=ag1 org=" + orgID + " rec=" + recID + " fp=" + shortFP(fp) +} + +func parseMarkerValue(s string) (ownershipMarker, bool) { + out := ownershipMarker{} + fields := strings.Fields(s) + if len(fields) < 4 { + return out, false + } + kv := map[string]string{} + for _, f := range fields { + parts := strings.SplitN(f, "=", 2) + if len(parts) == 2 { + kv[parts[0]] = parts[1] + } + } + if kv["v"] == "" || kv["org"] == "" || kv["rec"] == "" || kv["fp"] == "" { + return out, false + } + out.Ver, out.Org, out.Rec, out.Fp = kv["v"], kv["org"], kv["rec"], kv["fp"] + return out, true +} + +func getMarkerTXTValues(ctx context.Context, c *r53.Client, zoneID, marker string) ([]string, error) { + return getTXTValues(ctx, c, zoneID, marker) +} + +// generic TXT fetcher +func getTXTValues(ctx context.Context, c *r53.Client, zoneID, name string) ([]string, error) { + out, err := c.ListResourceRecordSets(ctx, &r53.ListResourceRecordSetsInput{ + HostedZoneId: aws.String(zoneID), + StartRecordName: aws.String(name), + StartRecordType: r53types.RRTypeTxt, + MaxItems: aws.Int32(1), + }) + if err != nil { + return nil, err + } + if len(out.ResourceRecordSets) == 0 { + return nil, nil + } + rrset := out.ResourceRecordSets[0] + if aws.ToString(rrset.Name) != name || rrset.Type != r53types.RRTypeTxt { + return nil, nil + } + vals := make([]string, 0, len(rrset.ResourceRecords)) + for _, rr := range rrset.ResourceRecords { + vals = append(vals, aws.ToString(rr.Value)) + } + return vals, nil +} + +// detect external-dns-style ownership for this fqdn/type +func hasExternalDNSOwnership(ctx context.Context, c *r53.Client, zoneID, fqdn, rrType string) (bool, error) { + base := strings.TrimSuffix(fqdn, ".") + candidates := []string{ + // with txtPrefix=extdns-, external-dns writes both: + // extdns- and extdns-- + "extdns-" + base + ".", + "extdns-" + strings.ToLower(rrType) + "-" + base + ".", + } + for _, name := range candidates { + vals, err := getTXTValues(ctx, c, zoneID, name) + if err != nil { + return false, err + } + for _, raw := range vals { + v := strings.TrimSpace(raw) + // strip surrounding quotes if present + if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' { + if unq, err := strconv.Unquote(v); err == nil { + v = unq + } + } + meta := parseExternalDNSMeta(v) + if meta == nil { + continue + } + if meta["heritage"] == "external-dns" && + meta["external-dns/owner"] != "" && + meta["external-dns/owner"] != externalDNSPoisonOwner { + return true, nil + } + } + } + return false, nil +} + +// parseExternalDNSMeta parses the comma-separated external-dns TXT format into a small map. +func parseExternalDNSMeta(v string) map[string]string { + parts := strings.Split(v, ",") + if len(parts) == 0 { + return nil + } + meta := make(map[string]string, len(parts)) + for _, p := range parts { + p = strings.TrimSpace(p) + if p == "" { + continue + } + kv := strings.SplitN(p, "=", 2) + if len(kv) != 2 { + continue + } + meta[kv[0]] = kv[1] + } + if len(meta) == 0 { + return nil + } + return meta +} + +// build poison TXT records so external-dns thinks some *other* owner manages this +func buildExternalDNSPoisonTXTChanges(fqdn, rrType string) []r53types.Change { + base := strings.TrimSuffix(fqdn, ".") + names := []string{ + "extdns-" + base + ".", + "extdns-" + strings.ToLower(rrType) + "-" + base + ".", + } + val := strconv.Quote(externalDNSPoisonValue) + changes := make([]r53types.Change, 0, len(names)) + for _, n := range names { + changes = append(changes, r53types.Change{ + Action: r53types.ChangeActionUpsert, + ResourceRecordSet: &r53types.ResourceRecordSet{ + Name: aws.String(n), + Type: r53types.RRTypeTxt, + TTL: aws.Int64(300), + ResourceRecords: []r53types.ResourceRecord{ + {Value: aws.String(val)}, + }, + }, + }) + } + return changes +} + +/************* misc utils *************/ + +func truncateErr(s string) string { + const max = 2000 + if len(s) > max { + return s[:max] + } + return s +} + +// Strict unmarshal that treats "null" -> zero value correctly. +func jsonUnmarshalStrict(b []byte, dst any) error { + if len(b) == 0 { + return errors.New("empty json") + } + return json.Unmarshal(b, dst) +} diff --git a/internal/handlers/auth.go b/internal/handlers/auth.go index 13fe502..bcb33aa 100644 --- a/internal/handlers/auth.go +++ b/internal/handlers/auth.go @@ -371,6 +371,21 @@ func Refresh(db *gorm.DB) http.HandlerFunc { return } + secure := strings.HasPrefix(cfg.OAuthRedirectBase, "https://") + if xf := r.Header.Get("X-Forwarded-Proto"); xf != "" { + secure = strings.EqualFold(xf, "https") + } + + http.SetCookie(w, &http.Cookie{ + Name: "ag_jwt", + Value: "Bearer " + access, + Path: "/", + HttpOnly: true, + SameSite: http.SameSiteLaxMode, + Secure: secure, + MaxAge: int((time.Hour * 8).Seconds()), + }) + utils.WriteJSON(w, 200, dto.TokenPair{ AccessToken: access, RefreshToken: newPair.Plain, diff --git a/internal/handlers/dns.go b/internal/handlers/dns.go new file mode 100644 index 0000000..d34bfc2 --- /dev/null +++ b/internal/handlers/dns.go @@ -0,0 +1,802 @@ +package handlers + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + "time" + + "github.com/glueops/autoglue/internal/api/httpmiddleware" + "github.com/glueops/autoglue/internal/handlers/dto" + "github.com/glueops/autoglue/internal/models" + "github.com/glueops/autoglue/internal/utils" + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/rs/zerolog/log" + "gorm.io/datatypes" + "gorm.io/gorm" +) + +// ---------- Helpers ---------- + +func normLowerNoDot(s string) string { + s = strings.TrimSpace(strings.ToLower(s)) + return strings.TrimSuffix(s, ".") +} + +func fqdn(domain string, rel string) string { + d := normLowerNoDot(domain) + r := normLowerNoDot(rel) + if r == "" || r == "@" { + return d + } + return r + "." + d +} + +func canonicalJSONAny(v any) ([]byte, error) { + b, err := json.Marshal(v) + if err != nil { + return nil, err + } + var anyv any + if err := json.Unmarshal(b, &anyv); err != nil { + return nil, err + } + return marshalSortedDNS(anyv) +} + +func marshalSortedDNS(v any) ([]byte, error) { + switch vv := v.(type) { + case map[string]any: + keys := make([]string, 0, len(vv)) + for k := range vv { + keys = append(keys, k) + } + sortStrings(keys) + var buf bytes.Buffer + buf.WriteByte('{') + for i, k := range keys { + if i > 0 { + buf.WriteByte(',') + } + kb, _ := json.Marshal(k) + buf.Write(kb) + buf.WriteByte(':') + b, err := marshalSortedDNS(vv[k]) + if err != nil { + return nil, err + } + buf.Write(b) + } + buf.WriteByte('}') + return buf.Bytes(), nil + case []any: + var buf bytes.Buffer + buf.WriteByte('[') + for i, e := range vv { + if i > 0 { + buf.WriteByte(',') + } + b, err := marshalSortedDNS(e) + if err != nil { + return nil, err + } + buf.Write(b) + } + buf.WriteByte(']') + return buf.Bytes(), nil + default: + return json.Marshal(v) + } +} + +func sortStrings(a []string) { + for i := 0; i < len(a); i++ { + for j := i + 1; j < len(a); j++ { + if a[j] < a[i] { + a[i], a[j] = a[j], a[i] + } + } + } +} + +func sha256HexBytes(b []byte) string { + sum := sha256.Sum256(b) + return hex.EncodeToString(sum[:]) +} + +/* Fingerprint (provider-agnostic) */ +type desiredRecord struct { + ZoneID string `json:"zone_id"` + FQDN string `json:"fqdn"` + Type string `json:"type"` + TTL *int `json:"ttl,omitempty"` + Values []string `json:"values,omitempty"` +} + +func computeFingerprint(zoneID, fqdn, typ string, ttl *int, values datatypes.JSON) (string, error) { + var vals []string + if len(values) > 0 && string(values) != "null" { + if err := json.Unmarshal(values, &vals); err != nil { + return "", err + } + sortStrings(vals) + } + payload := &desiredRecord{ + ZoneID: zoneID, FQDN: fqdn, Type: strings.ToUpper(typ), TTL: ttl, Values: vals, + } + can, err := canonicalJSONAny(payload) + if err != nil { + return "", err + } + return sha256HexBytes(can), nil +} + +func mustSameOrgDomainWithCredential(db *gorm.DB, orgID uuid.UUID, credID uuid.UUID) error { + var cred models.Credential + if err := db.Where("id = ? AND organization_id = ?", credID, orgID).First(&cred).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fmt.Errorf("credential not found or belongs to different org") + } + return err + } + if cred.Provider != "aws" || cred.ScopeKind != "service" { + return fmt.Errorf("credential must be AWS Route 53 service scoped") + } + var scope map[string]any + if err := json.Unmarshal(cred.Scope, &scope); err != nil { + return fmt.Errorf("credential scope invalid json: %w", err) + } + if strings.ToLower(fmt.Sprint(scope["service"])) != "route53" { + return fmt.Errorf("credential scope.service must be route53") + } + return nil +} + +// ---------- Domain Handlers ---------- + +// ListDomains godoc +// +// @ID ListDomains +// @Summary List domains (org scoped) +// @Description Returns domains for X-Org-ID. Filters: `domain_name`, `status`, `q` (contains). +// @Tags DNS +// @Accept json +// @Produce json +// @Param X-Org-ID header string false "Organization UUID" +// @Param domain_name query string false "Exact domain name (lowercase, no trailing dot)" +// @Param status query string false "pending|provisioning|ready|failed" +// @Param q query string false "Domain contains (case-insensitive)" +// @Success 200 {array} dto.DomainResponse +// @Failure 401 {string} string "Unauthorized" +// @Failure 403 {string} string "organization required" +// @Failure 500 {string} string "db error" +// @Router /dns/domains [get] +// @Security BearerAuth +// @Security OrgKeyAuth +// @Security OrgSecretAuth +func ListDomains(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + orgID, ok := httpmiddleware.OrgIDFrom(r.Context()) + if !ok { + utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID") + return + } + q := db.Model(&models.Domain{}).Where("organization_id = ?", orgID) + if v := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("domain_name"))); v != "" { + q = q.Where("LOWER(domain_name) = ?", v) + } + if v := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("status"))); v != "" { + q = q.Where("status = ?", v) + } + if needle := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("q"))); needle != "" { + q = q.Where("LOWER(domain_name) LIKE ?", "%"+needle+"%") + } + + var rows []models.Domain + if err := q.Order("created_at DESC").Find(&rows).Error; err != nil { + utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error") + return + } + out := make([]dto.DomainResponse, 0, len(rows)) + for i := range rows { + out = append(out, domainOut(&rows[i])) + } + utils.WriteJSON(w, http.StatusOK, out) + } +} + +// GetDomain godoc +// +// @ID GetDomain +// @Summary Get a domain (org scoped) +// @Tags DNS +// @Accept json +// @Produce json +// @Param X-Org-ID header string false "Organization UUID" +// @Param id path string true "Domain ID (UUID)" +// @Success 200 {object} dto.DomainResponse +// @Failure 401 {string} string "Unauthorized" +// @Failure 403 {string} string "organization required" +// @Failure 404 {string} string "not found" +// @Router /dns/domains/{id} [get] +// @Security BearerAuth +// @Security OrgKeyAuth +// @Security OrgSecretAuth +func GetDomain(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + orgID, ok := httpmiddleware.OrgIDFrom(r.Context()) + if !ok { + utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID") + return + } + id, err := uuid.Parse(chi.URLParam(r, "id")) + if err != nil { + utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID") + return + } + var row models.Domain + if err := db.Where("organization_id = ? AND id = ?", orgID, id).First(&row).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + utils.WriteError(w, http.StatusNotFound, "not_found", "domain not found") + return + } + utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error") + return + } + utils.WriteJSON(w, http.StatusOK, domainOut(&row)) + } +} + +// CreateDomain godoc +// +// @ID CreateDomain +// @Summary Create a domain (org scoped) +// @Description Creates a domain bound to a Route 53 scoped credential. Archer will backfill ZoneID if omitted. +// @Tags DNS +// @Accept json +// @Produce json +// @Param X-Org-ID header string false "Organization UUID" +// @Param body body dto.CreateDomainRequest true "Domain payload" +// @Success 201 {object} dto.DomainResponse +// @Failure 400 {string} string "validation error" +// @Failure 401 {string} string "Unauthorized" +// @Failure 403 {string} string "organization required" +// @Failure 500 {string} string "db error" +// @Router /dns/domains [post] +// @Security BearerAuth +// @Security OrgKeyAuth +// @Security OrgSecretAuth +func CreateDomain(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + orgID, ok := httpmiddleware.OrgIDFrom(r.Context()) + if !ok { + utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID") + return + } + var in dto.CreateDomainRequest + if err := json.NewDecoder(r.Body).Decode(&in); err != nil { + utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error()) + return + } + if err := dto.DNSValidate(in); err != nil { + utils.WriteError(w, http.StatusBadRequest, "validation_error", err.Error()) + return + } + credID, _ := uuid.Parse(in.CredentialID) + if err := mustSameOrgDomainWithCredential(db, orgID, credID); err != nil { + utils.WriteError(w, http.StatusBadRequest, "invalid_credential", err.Error()) + return + } + + row := &models.Domain{ + OrganizationID: orgID, + DomainName: normLowerNoDot(in.DomainName), + ZoneID: strings.TrimSpace(in.ZoneID), + Status: "pending", + LastError: "", + CredentialID: credID, + } + if err := db.Create(row).Error; err != nil { + utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error()) + return + } + utils.WriteJSON(w, http.StatusCreated, domainOut(row)) + } +} + +// UpdateDomain godoc +// +// @ID UpdateDomain +// @Summary Update a domain (org scoped) +// @Tags DNS +// @Accept json +// @Produce json +// @Param X-Org-ID header string false "Organization UUID" +// @Param id path string true "Domain ID (UUID)" +// @Param body body dto.UpdateDomainRequest true "Fields to update" +// @Success 200 {object} dto.DomainResponse +// @Failure 400 {string} string "validation error" +// @Failure 403 {string} string "organization required" +// @Failure 404 {string} string "not found" +// @Router /dns/domains/{id} [patch] +// @Security BearerAuth +// @Security OrgKeyAuth +// @Security OrgSecretAuth +func UpdateDomain(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + orgID, ok := httpmiddleware.OrgIDFrom(r.Context()) + if !ok { + utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID") + return + } + id, err := uuid.Parse(chi.URLParam(r, "id")) + if err != nil { + utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID") + return + } + var row models.Domain + if err := db.Where("organization_id = ? AND id = ?", orgID, id).First(&row).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + utils.WriteError(w, http.StatusNotFound, "not_found", "domain not found") + return + } + utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error()) + return + } + var in dto.UpdateDomainRequest + if err := json.NewDecoder(r.Body).Decode(&in); err != nil { + utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error()) + return + } + if err := dto.DNSValidate(in); err != nil { + utils.WriteError(w, http.StatusBadRequest, "validation_error", err.Error()) + return + } + if in.DomainName != nil { + row.DomainName = normLowerNoDot(*in.DomainName) + } + if in.CredentialID != nil { + credID, _ := uuid.Parse(*in.CredentialID) + if err := mustSameOrgDomainWithCredential(db, orgID, credID); err != nil { + utils.WriteError(w, http.StatusBadRequest, "invalid_credential", err.Error()) + return + } + row.CredentialID = credID + row.Status = "pending" + row.LastError = "" + } + if in.ZoneID != nil { + row.ZoneID = strings.TrimSpace(*in.ZoneID) + } + if in.Status != nil { + row.Status = *in.Status + if row.Status == "pending" { + row.LastError = "" + } + } + if err := db.Save(&row).Error; err != nil { + utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error()) + return + } + utils.WriteJSON(w, http.StatusOK, domainOut(&row)) + } +} + +// DeleteDomain godoc +// +// @ID DeleteDomain +// @Summary Delete a domain +// @Tags DNS +// @Accept json +// @Produce json +// @Param X-Org-ID header string false "Organization UUID" +// @Param id path string true "Domain ID (UUID)" +// @Success 204 +// @Failure 403 {string} string "organization required" +// @Failure 404 {string} string "not found" +// @Router /dns/domains/{id} [delete] +// @Security BearerAuth +// @Security OrgKeyAuth +// @Security OrgSecretAuth +func DeleteDomain(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + orgID, ok := httpmiddleware.OrgIDFrom(r.Context()) + if !ok { + utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID") + return + } + id, err := uuid.Parse(chi.URLParam(r, "id")) + if err != nil { + utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID") + return + } + res := db.Where("organization_id = ? AND id = ?", orgID, id).Delete(&models.Domain{}) + if res.Error != nil { + utils.WriteError(w, http.StatusInternalServerError, "db_error", res.Error.Error()) + return + } + if res.RowsAffected == 0 { + utils.WriteError(w, http.StatusNotFound, "not_found", "domain not found") + return + } + w.WriteHeader(http.StatusNoContent) + } +} + +// ---------- Record Set Handlers ---------- + +// ListRecordSets godoc +// +// @ID ListRecordSets +// @Summary List record sets for a domain +// @Description Filters: `name`, `type`, `status`. +// @Tags DNS +// @Accept json +// @Produce json +// @Param X-Org-ID header string false "Organization UUID" +// @Param domain_id path string true "Domain ID (UUID)" +// @Param name query string false "Exact relative name or FQDN (server normalizes)" +// @Param type query string false "RR type (A, AAAA, CNAME, TXT, MX, NS, SRV, CAA)" +// @Param status query string false "pending|provisioning|ready|failed" +// @Success 200 {array} dto.RecordSetResponse +// @Failure 403 {string} string "organization required" +// @Failure 404 {string} string "domain not found" +// @Router /dns/domains/{domain_id}/records [get] +// @Security BearerAuth +// @Security OrgKeyAuth +// @Security OrgSecretAuth +func ListRecordSets(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + orgID, ok := httpmiddleware.OrgIDFrom(r.Context()) + if !ok { + utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID") + return + } + did, err := uuid.Parse(chi.URLParam(r, "domain_id")) + if err != nil { + log.Info().Msg(err.Error()) + utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid domain UUID:") + return + } + var domain models.Domain + if err := db.Where("organization_id = ? AND id = ?", orgID, did).First(&domain).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + utils.WriteError(w, http.StatusNotFound, "not_found", "domain not found") + return + } + utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error") + return + } + + q := db.Model(&models.RecordSet{}).Where("domain_id = ?", did) + if v := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("name"))); v != "" { + dn := strings.ToLower(domain.DomainName) + rel := v + // normalize apex or FQDN into relative + if v == dn || v == dn+"." { + rel = "" + } else { + rel = strings.TrimSuffix(v, "."+dn) + rel = normLowerNoDot(rel) + } + q = q.Where("LOWER(name) = ?", rel) + } + if v := strings.TrimSpace(strings.ToUpper(r.URL.Query().Get("type"))); v != "" { + q = q.Where("type = ?", v) + } + if v := strings.TrimSpace(strings.ToLower(r.URL.Query().Get("status"))); v != "" { + q = q.Where("status = ?", v) + } + + var rows []models.RecordSet + if err := q.Order("created_at DESC").Find(&rows).Error; err != nil { + utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error") + return + } + out := make([]dto.RecordSetResponse, 0, len(rows)) + for i := range rows { + out = append(out, recordOut(&rows[i])) + } + utils.WriteJSON(w, http.StatusOK, out) + } +} + +// CreateRecordSet godoc +// +// @ID CreateRecordSet +// @Summary Create a record set (pending; Archer will UPSERT to Route 53) +// @Tags DNS +// @Accept json +// @Produce json +// @Param X-Org-ID header string false "Organization UUID" +// @Param domain_id path string true "Domain ID (UUID)" +// @Param body body dto.CreateRecordSetRequest true "Record set payload" +// @Success 201 {object} dto.RecordSetResponse +// @Failure 400 {string} string "validation error" +// @Failure 403 {string} string "organization required" +// @Failure 404 {string} string "domain not found" +// @Router /dns/domains/{domain_id}/records [post] +// @Security BearerAuth +// @Security OrgKeyAuth +// @Security OrgSecretAuth +func CreateRecordSet(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + orgID, ok := httpmiddleware.OrgIDFrom(r.Context()) + if !ok { + utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID") + return + } + did, err := uuid.Parse(chi.URLParam(r, "domain_id")) + if err != nil { + utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid domain UUID") + return + } + var domain models.Domain + if err := db.Where("organization_id = ? AND id = ?", orgID, did).First(&domain).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + utils.WriteError(w, http.StatusNotFound, "not_found", "domain not found") + return + } + utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error()) + return + } + + var in dto.CreateRecordSetRequest + if err := json.NewDecoder(r.Body).Decode(&in); err != nil { + utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error()) + return + } + if err := dto.DNSValidate(in); err != nil { + utils.WriteError(w, http.StatusBadRequest, "validation_error", err.Error()) + return + } + t := strings.ToUpper(in.Type) + if t == "CNAME" && len(in.Values) != 1 { + utils.WriteError(w, http.StatusBadRequest, "validation_error", "CNAME requires exactly one value") + return + } + + rel := normLowerNoDot(in.Name) + fq := fqdn(domain.DomainName, rel) + + // Pre-flight: block duplicate tuple and protect from non-autoglue rows + var existing models.RecordSet + if err := db.Where("domain_id = ? AND LOWER(name) = ? AND type = ?", + domain.ID, strings.ToLower(rel), t).First(&existing).Error; err == nil { + if existing.Owner != "" && existing.Owner != "autoglue" { + utils.WriteError(w, http.StatusConflict, "ownership_conflict", + "record with the same (name,type) exists but is not owned by autoglue") + return + } + utils.WriteError(w, http.StatusConflict, "already_exists", + "a record with the same (name,type) already exists; use PATCH to modify") + return + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error()) + return + } + + valuesJSON, _ := json.Marshal(in.Values) + fp, err := computeFingerprint(domain.ZoneID, fq, t, in.TTL, datatypes.JSON(valuesJSON)) + if err != nil { + utils.WriteError(w, http.StatusInternalServerError, "fingerprint_error", err.Error()) + return + } + + row := &models.RecordSet{ + DomainID: domain.ID, + Name: rel, + Type: t, + TTL: in.TTL, + Values: datatypes.JSON(valuesJSON), + Fingerprint: fp, + Status: "pending", + LastError: "", + Owner: "autoglue", + } + if err := db.Create(row).Error; err != nil { + utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error()) + return + } + utils.WriteJSON(w, http.StatusCreated, recordOut(row)) + } +} + +// UpdateRecordSet godoc +// +// @ID UpdateRecordSet +// @Summary Update a record set (flips to pending for reconciliation) +// @Tags DNS +// @Accept json +// @Produce json +// @Param X-Org-ID header string false "Organization UUID" +// @Param id path string true "Record Set ID (UUID)" +// @Param body body dto.UpdateRecordSetRequest true "Fields to update" +// @Success 200 {object} dto.RecordSetResponse +// @Failure 400 {string} string "validation error" +// @Failure 403 {string} string "organization required" +// @Failure 404 {string} string "not found" +// @Router /dns/records/{id} [patch] +// @Security BearerAuth +// @Security OrgKeyAuth +// @Security OrgSecretAuth +func UpdateRecordSet(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + orgID, ok := httpmiddleware.OrgIDFrom(r.Context()) + if !ok { + utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID") + return + } + id, err := uuid.Parse(chi.URLParam(r, "id")) + if err != nil { + utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID") + return + } + + var row models.RecordSet + if err := db. + Joins("Domain"). + Where(`record_sets.id = ? AND "Domain"."organization_id" = ?`, id, orgID). + First(&row).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + utils.WriteError(w, http.StatusNotFound, "not_found", "record set not found") + return + } + utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error()) + return + } + var domain models.Domain + if err := db.Where("id = ?", row.DomainID).First(&domain).Error; err != nil { + utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error()) + return + } + + var in dto.UpdateRecordSetRequest + if err := json.NewDecoder(r.Body).Decode(&in); err != nil { + utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error()) + return + } + if err := dto.DNSValidate(in); err != nil { + utils.WriteError(w, http.StatusBadRequest, "validation_error", err.Error()) + return + } + if row.Owner != "" && row.Owner != "autoglue" { + utils.WriteError(w, http.StatusConflict, "ownership_conflict", + "record is not owned by autoglue; refuse to modify") + return + } + + // Mutations + if in.Name != nil { + row.Name = normLowerNoDot(*in.Name) + } + if in.Type != nil { + row.Type = strings.ToUpper(*in.Type) + } + if in.TTL != nil { + row.TTL = in.TTL + } + if in.Values != nil { + t := row.Type + if in.Type != nil { + t = strings.ToUpper(*in.Type) + } + if t == "CNAME" && len(*in.Values) != 1 { + utils.WriteError(w, http.StatusBadRequest, "validation_error", "CNAME requires exactly one value") + return + } + b, _ := json.Marshal(*in.Values) + row.Values = datatypes.JSON(b) + } + + if in.Status != nil { + row.Status = *in.Status + } else { + row.Status = "pending" + row.LastError = "" + } + + fq := fqdn(domain.DomainName, row.Name) + fp, err := computeFingerprint(domain.ZoneID, fq, row.Type, row.TTL, row.Values) + if err != nil { + utils.WriteError(w, http.StatusInternalServerError, "fingerprint_error", err.Error()) + return + } + row.Fingerprint = fp + + if err := db.Save(&row).Error; err != nil { + utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error()) + return + } + utils.WriteJSON(w, http.StatusOK, recordOut(&row)) + } +} + +// DeleteRecordSet godoc +// +// @ID DeleteRecordSet +// @Summary Delete a record set (API removes row; worker can optionally handle external deletion policy) +// @Tags DNS +// @Accept json +// @Produce json +// @Param X-Org-ID header string false "Organization UUID" +// @Param id path string true "Record Set ID (UUID)" +// @Success 204 +// @Failure 403 {string} string "organization required" +// @Failure 404 {string} string "not found" +// @Router /dns/records/{id} [delete] +// @Security BearerAuth +// @Security OrgKeyAuth +// @Security OrgSecretAuth +func DeleteRecordSet(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + orgID, ok := httpmiddleware.OrgIDFrom(r.Context()) + if !ok { + utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID") + return + } + id, err := uuid.Parse(chi.URLParam(r, "id")) + if err != nil { + utils.WriteError(w, http.StatusBadRequest, "bad_id", "invalid UUID") + return + } + sub := db.Model(&models.RecordSet{}). + Select("record_sets.id"). + Joins("JOIN domains ON domains.id = record_sets.domain_id"). + Where("record_sets.id = ? AND domains.organization_id = ?", id, orgID) + + res := db.Where("id IN (?)", sub).Delete(&models.RecordSet{}) + if res.Error != nil { + utils.WriteError(w, http.StatusInternalServerError, "db_error", res.Error.Error()) + return + } + if res.RowsAffected == 0 { + utils.WriteError(w, http.StatusNotFound, "not_found", "record set not found") + return + } + w.WriteHeader(http.StatusNoContent) + } +} + +// ---------- Out mappers ---------- + +func domainOut(m *models.Domain) dto.DomainResponse { + return dto.DomainResponse{ + ID: m.ID.String(), + OrganizationID: m.OrganizationID.String(), + DomainName: m.DomainName, + ZoneID: m.ZoneID, + Status: m.Status, + LastError: m.LastError, + CredentialID: m.CredentialID.String(), + CreatedAt: m.CreatedAt.UTC().Format(time.RFC3339), + UpdatedAt: m.UpdatedAt.UTC().Format(time.RFC3339), + } +} + +func recordOut(r *models.RecordSet) dto.RecordSetResponse { + vals := r.Values + if len(vals) == 0 { + vals = datatypes.JSON("[]") + } + return dto.RecordSetResponse{ + ID: r.ID.String(), + DomainID: r.DomainID.String(), + Name: r.Name, + Type: r.Type, + TTL: r.TTL, + Values: []byte(vals), + Fingerprint: r.Fingerprint, + Status: r.Status, + LastError: r.LastError, + Owner: r.Owner, + CreatedAt: r.CreatedAt.UTC().Format(time.RFC3339), + UpdatedAt: r.UpdatedAt.UTC().Format(time.RFC3339), + } +} diff --git a/internal/handlers/dto/dns.go b/internal/handlers/dto/dns.go new file mode 100644 index 0000000..e9cb088 --- /dev/null +++ b/internal/handlers/dto/dns.go @@ -0,0 +1,103 @@ +package dto + +import ( + "encoding/json" + "strings" + + "github.com/go-playground/validator/v10" +) + +var dnsValidate = validator.New() + +func init() { + _ = dnsValidate.RegisterValidation("fqdn", func(fl validator.FieldLevel) bool { + s := strings.TrimSpace(fl.Field().String()) + if s == "" || len(s) > 253 { + return false + } + // Minimal: lower-cased, no trailing dot in our API (normalize server-side) + // You can add stricter checks later. + return !strings.HasPrefix(s, ".") && !strings.Contains(s, "..") + }) + _ = dnsValidate.RegisterValidation("rrtype", func(fl validator.FieldLevel) bool { + switch strings.ToUpper(fl.Field().String()) { + case "A", "AAAA", "CNAME", "TXT", "MX", "NS", "SRV", "CAA": + return true + default: + return false + } + }) +} + +// ---- Domains ---- + +type CreateDomainRequest struct { + DomainName string `json:"domain_name" validate:"required,fqdn"` + CredentialID string `json:"credential_id" validate:"required,uuid4"` + ZoneID string `json:"zone_id,omitempty" validate:"omitempty,max=128"` +} + +type UpdateDomainRequest struct { + CredentialID *string `json:"credential_id,omitempty" validate:"omitempty,uuid4"` + ZoneID *string `json:"zone_id,omitempty" validate:"omitempty,max=128"` + Status *string `json:"status,omitempty" validate:"omitempty,oneof=pending provisioning ready failed"` + DomainName *string `json:"domain_name,omitempty" validate:"omitempty,fqdn"` +} + +type DomainResponse struct { + ID string `json:"id"` + OrganizationID string `json:"organization_id"` + DomainName string `json:"domain_name"` + ZoneID string `json:"zone_id"` + Status string `json:"status"` + LastError string `json:"last_error"` + CredentialID string `json:"credential_id"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +// ---- Record Sets ---- + +type AliasTarget struct { + HostedZoneID string `json:"hosted_zone_id" validate:"required"` + DNSName string `json:"dns_name" validate:"required"` + EvaluateTargetHealth bool `json:"evaluate_target_health"` +} + +type CreateRecordSetRequest struct { + // Name relative to domain ("endpoint") OR FQDN ("endpoint.example.com"). + // Server normalizes to relative. + Name string `json:"name" validate:"required,max=253"` + Type string `json:"type" validate:"required,rrtype"` + TTL *int `json:"ttl,omitempty" validate:"omitempty,gte=1,lte=86400"` + Values []string `json:"values" validate:"omitempty,dive,min=1,max=1024"` +} + +type UpdateRecordSetRequest struct { + // Any change flips status back to pending (worker will UPSERT) + Name *string `json:"name,omitempty" validate:"omitempty,max=253"` + Type *string `json:"type,omitempty" validate:"omitempty,rrtype"` + TTL *int `json:"ttl,omitempty" validate:"omitempty,gte=1,lte=86400"` + Values *[]string `json:"values,omitempty" validate:"omitempty,dive,min=1,max=1024"` + Status *string `json:"status,omitempty" validate:"omitempty,oneof=pending provisioning ready failed"` +} + +type RecordSetResponse struct { + ID string `json:"id"` + DomainID string `json:"domain_id"` + Name string `json:"name"` + Type string `json:"type"` + TTL *int `json:"ttl,omitempty"` + Values json.RawMessage `json:"values" swaggertype:"object"` // []string JSON + Fingerprint string `json:"fingerprint"` + Status string `json:"status"` + LastError string `json:"last_error"` + Owner string `json:"owner"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +// DNSValidate Quick helper to validate DTOs in handlers +func DNSValidate(i any) error { + return dnsValidate.Struct(i) +} diff --git a/internal/handlers/servers.go b/internal/handlers/servers.go index a951996..c7e837e 100644 --- a/internal/handlers/servers.go +++ b/internal/handlers/servers.go @@ -370,6 +370,63 @@ func DeleteServer(db *gorm.DB) http.HandlerFunc { } } +// ResetServerHostKey godoc +// +// @ID ResetServerHostKey +// @Summary Reset SSH host key (org scoped) +// @Description Clears the stored SSH host key for this server. The next SSH connection will re-learn the host key (trust-on-first-use). +// @Tags Servers +// @Accept json +// @Produce json +// @Param X-Org-ID header string false "Organization UUID" +// @Param id path string true "Server ID (UUID)" +// @Success 200 {object} dto.ServerResponse +// @Failure 400 {string} string "invalid id" +// @Failure 401 {string} string "Unauthorized" +// @Failure 403 {string} string "organization required" +// @Failure 404 {string} string "not found" +// @Failure 500 {string} string "reset failed" +// @Router /servers/{id}/reset-hostkey [post] +// @Security BearerAuth +// @Security OrgKeyAuth +// @Security OrgSecretAuth +func ResetServerHostKey(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + orgID, ok := httpmiddleware.OrgIDFrom(r.Context()) + if !ok { + utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID") + return + } + + id, err := uuid.Parse(chi.URLParam(r, "id")) + if err != nil { + utils.WriteError(w, http.StatusBadRequest, "id_invalid", "invalid id") + return + } + + var server models.Server + if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&server).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + utils.WriteError(w, http.StatusNotFound, "server_not_found", "server not found") + return + } + utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to get server") + return + } + + // Clear stored host key so next SSH handshake will TOFU and persist a new one. + server.SSHHostKey = "" + server.SSHHostKeyAlgo = "" + + if err := db.Save(&server).Error; err != nil { + utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to reset host key") + return + } + + utils.WriteJSON(w, http.StatusOK, server) + } +} + // --- Helpers --- func validStatus(status string) bool { diff --git a/internal/models/backup.go b/internal/models/backup.go new file mode 100644 index 0000000..f7ee0c1 --- /dev/null +++ b/internal/models/backup.go @@ -0,0 +1,18 @@ +package models + +import ( + "time" + + "github.com/google/uuid" +) + +type Backup struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + OrganizationID uuid.UUID `gorm:"type:uuid;not null;index;uniqueIndex:uniq_org_credential,priority:1"` + Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"` + Enabled bool `gorm:"not null;default:false" json:"enabled"` + CredentialID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex:uniq_org_credential,priority:2" json:"credential_id"` + Credential Credential `gorm:"foreignKey:CredentialID" json:"credential,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"` + UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"` +} diff --git a/internal/models/domain.go b/internal/models/domain.go index 8c93835..c4031e8 100644 --- a/internal/models/domain.go +++ b/internal/models/domain.go @@ -4,18 +4,38 @@ import ( "time" "github.com/google/uuid" + "gorm.io/datatypes" ) type Domain struct { ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` - OrganizationID uuid.UUID `gorm:"type:uuid;not null;uniqueIndex:idx_credentials_org_provider" json:"organization_id"` + OrganizationID uuid.UUID `gorm:"type:uuid;not null;index;uniqueIndex:uniq_org_domain,priority:1"` Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"` - ClusterID *uuid.UUID `gorm:"type:uuid" json:"cluster_id,omitempty"` - Cluster *Cluster `gorm:"foreignKey:ClusterID" json:"cluster,omitempty"` - DomainName string `gorm:"not null;index" json:"domain_name,omitempty"` - DomainID string - CredentialID uuid.UUID `gorm:"type:uuid;not null" json:"credential_id"` - Credential Credential `gorm:"foreignKey:CredentialID" json:"credential,omitempty"` - CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"` - UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"` + DomainName string `gorm:"type:varchar(253);not null;uniqueIndex:uniq_org_domain,priority:2"` + ZoneID string `gorm:"type:varchar(128);not null;default:''"` // backfilled for R53 (e.g. "/hostedzone/Z123...") + Status string `gorm:"type:varchar(20);not null;default:'pending'"` // pending, provisioning, ready, failed + LastError string `gorm:"type:text;not null;default:''"` + CredentialID uuid.UUID `gorm:"type:uuid;not null" json:"credential_id"` + Credential Credential `gorm:"foreignKey:CredentialID" json:"credential,omitempty"` + CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"` + UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"` +} + +type RecordSet struct { + ID uuid.UUID `gorm:"type:uuid;primaryKey;default:gen_random_uuid()"` + DomainID uuid.UUID `gorm:"type:uuid;not null;index"` + Domain Domain `gorm:"foreignKey:DomainID;constraint:OnDelete:CASCADE"` + Name string `gorm:"type:varchar(253);not null"` // e.g. "endpoint" (relative to DomainName) + Type string `gorm:"type:varchar(10);not null;index"` // A, AAAA, CNAME, TXT, MX, SRV, NS, CAA... + TTL *int `gorm:""` // nil for alias targets (Route 53 ignores TTL for alias) + Values datatypes.JSON `gorm:"type:jsonb;not null;default:'[]'"` + Fingerprint string `gorm:"type:char(64);not null;index"` // sha256 of canonical(name,type,ttl,values|alias) + Status string `gorm:"type:varchar(20);not null;default:'pending'"` + Owner string `gorm:"type:varchar(16);not null;default:'unknown'"` // 'autoglue' | 'external' | 'unknown' + LastError string `gorm:"type:text;not null;default:''"` + _ struct{} `gorm:"uniqueIndex:uniq_domain_name_type,priority:1"` // tag holder + _ struct{} `gorm:"uniqueIndex:uniq_domain_name_type,priority:2"` + _ struct{} `gorm:"uniqueIndex:uniq_domain_name_type,priority:3"` + CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"` + UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"` } diff --git a/internal/models/server.go b/internal/models/server.go index a42e840..7630cd6 100644 --- a/internal/models/server.go +++ b/internal/models/server.go @@ -22,6 +22,8 @@ type Server struct { Role string `gorm:"not null" json:"role" enums:"master,worker,bastion"` // e.g., "master", "worker", "bastion" Status string `gorm:"default:'pending'" json:"status" enums:"pending, provisioning, ready, failed"` // pending, provisioning, ready, failed NodePools []NodePool `gorm:"many2many:node_servers;constraint:OnDelete:CASCADE" json:"node_pools,omitempty"` + SSHHostKey string `gorm:"column:ssh_host_key"` + SSHHostKeyAlgo string `gorm:"column:ssh_host_key_algo"` CreatedAt time.Time `gorm:"not null;default:now()" json:"created_at" format:"date-time"` UpdatedAt time.Time `gorm:"not null;default:now()" json:"updated_at" format:"date-time"` } diff --git a/internal/web/pgweb_embed.go b/internal/web/pgweb_embed.go deleted file mode 100644 index 97ad673..0000000 --- a/internal/web/pgweb_embed.go +++ /dev/null @@ -1,85 +0,0 @@ -package web - -import ( - "crypto/sha256" - "embed" - "encoding/hex" - "errors" - "fmt" - "io" - "os" - "path/filepath" - "runtime" -) - -//go:embed pgwebbin/* -var pgwebFS embed.FS - -type pgwebAsset struct { - Path string - SHA256 string -} - -var pgwebIndex = map[string]pgwebAsset{ - "linux/amd64": {Path: "pgwebbin/pgweb-linux-amd64", SHA256: ""}, - "linux/arm64": {Path: "pgwebbin/pgweb-linux-arm64", SHA256: ""}, - "darwin/amd64": {Path: "pgwebbin/pgweb-darwin-amd64", SHA256: ""}, - "darwin/arm64": {Path: "pgwebbin/pgweb-darwin-arm64", SHA256: ""}, -} - -func ExtractPgweb() (string, error) { - key := runtime.GOOS + "/" + runtime.GOARCH - as, ok := pgwebIndex[key] - if !ok { - return "", fmt.Errorf("pgweb not embedded for %s", key) - } - f, err := pgwebFS.Open(as.Path) - if err != nil { - return "", fmt.Errorf("embedded pgweb missing: %w", err) - } - defer f.Close() - - tmpDir, err := os.MkdirTemp("", "pgweb-*") - if err != nil { - return "", err - } - - filename := "pgweb" - if runtime.GOOS == "windows" { - filename += ".exe" - } - outPath := filepath.Join(tmpDir, filename) - - out, err := os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o700) - if err != nil { - return "", err - } - defer out.Close() - - h := sha256.New() - if _, err = io.Copy(io.MultiWriter(out, h), f); err != nil { - return "", err - } - - if as.SHA256 != "" { - got := hex.EncodeToString(h.Sum(nil)) - if got != as.SHA256 { - return "", fmt.Errorf("pgweb checksum mismatch: got=%s want=%s", got, as.SHA256) - } - } - - // Make sure it’s executable on Unix; Windows ignores this. - _ = os.Chmod(outPath, 0o700) - return outPath, nil -} - -func CleanupPgweb(pgwebPath string) error { - if pgwebPath == "" { - return nil - } - dir := filepath.Dir(pgwebPath) - if dir == "" || dir == "/" || dir == "." { - return errors.New("refusing to remove suspicious directory") - } - return os.RemoveAll(dir) -} diff --git a/internal/web/pgweb_proxy.go b/internal/web/pgweb_proxy.go deleted file mode 100644 index 12f7729..0000000 --- a/internal/web/pgweb_proxy.go +++ /dev/null @@ -1,107 +0,0 @@ -package web - -import ( - "context" - "fmt" - "net" - "net/http" - "net/http/httputil" - "net/url" - "os" - "os/exec" - "time" -) - -type Pgweb struct { - cmd *exec.Cmd - host string - port string - bin string -} - -func StartPgweb(dbURL, host, port string, readonly bool, user, pass string) (*Pgweb, error) { - // pick random port if 0/empty - if port == "" || port == "0" { - l, err := net.Listen("tcp", net.JoinHostPort(host, "0")) - if err != nil { - return nil, err - } - defer l.Close() - _, p, _ := net.SplitHostPort(l.Addr().String()) - port = p - } - - args := []string{ - "--url", dbURL, - "--bind", host, - "--listen", port, - "--prefix", "db-studio", - "--skip-open", - } - if readonly { - args = append(args, "--readonly") - } - if user != "" && pass != "" { - args = append(args, "--auth-user", user, "--auth-pass", pass) - } - - pgwebBinary, err := ExtractPgweb() - if err != nil { - return nil, fmt.Errorf("pgweb extract: %w", err) - } - - cmd := exec.Command(pgwebBinary, args...) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - if err := cmd.Start(); err != nil { - return nil, err - } - - // wait for port to be ready - deadline := time.Now().Add(4 * time.Second) - for time.Now().Before(deadline) { - c, err := net.DialTimeout("tcp", net.JoinHostPort(host, port), 200*time.Millisecond) - if err == nil { - _ = c.Close() - return &Pgweb{cmd: cmd, host: host, port: port}, nil - } - time.Sleep(120 * time.Millisecond) - } - // still return object so caller can Stop() - //return &Pgweb{cmd: cmd, host: host, port: port, bin: pgwebBinary}, nil - return nil, fmt.Errorf("pgweb did not become ready on %s:%s", host, port) -} - -func (p *Pgweb) Proxy() http.HandlerFunc { - target, _ := url.Parse("http://" + net.JoinHostPort(p.host, p.port)) - proxy := httputil.NewSingleHostReverseProxy(target) - proxy.FlushInterval = 100 * time.Millisecond - return func(w http.ResponseWriter, r *http.Request) { - r.Host = target.Host - // Let pgweb handle its paths; we mount it at a prefix. - proxy.ServeHTTP(w, r) - } -} - -func (p *Pgweb) Stop(ctx context.Context) error { - if p == nil || p.cmd == nil || p.cmd.Process == nil { - return nil - } - _ = p.cmd.Process.Kill() - done := make(chan struct{}) - go func() { _, _ = p.cmd.Process.Wait(); close(done) }() - select { - case <-done: - if p.bin != "" { - _ = CleanupPgweb(p.bin) - } - case <-ctx.Done(): - return ctx.Err() - } - return nil -} - -func (p *Pgweb) Port() string { - return p.port -} diff --git a/internal/web/pgwebbin/pgweb-darwin-amd64 b/internal/web/pgwebbin/pgweb-darwin-amd64 deleted file mode 100755 index a8c76ca..0000000 Binary files a/internal/web/pgwebbin/pgweb-darwin-amd64 and /dev/null differ diff --git a/internal/web/pgwebbin/pgweb-darwin-arm64 b/internal/web/pgwebbin/pgweb-darwin-arm64 deleted file mode 100755 index 4b181d8..0000000 Binary files a/internal/web/pgwebbin/pgweb-darwin-arm64 and /dev/null differ diff --git a/internal/web/pgwebbin/pgweb-linux-amd64 b/internal/web/pgwebbin/pgweb-linux-amd64 deleted file mode 100755 index b2bc338..0000000 Binary files a/internal/web/pgwebbin/pgweb-linux-amd64 and /dev/null differ diff --git a/internal/web/pgwebbin/pgweb-linux-arm64 b/internal/web/pgwebbin/pgweb-linux-arm64 deleted file mode 100755 index a3ec3cc..0000000 Binary files a/internal/web/pgwebbin/pgweb-linux-arm64 and /dev/null differ diff --git a/tools/pgweb_fetch.go b/tools/pgweb_fetch.go index 8414d5d..b3dcc4a 100644 --- a/tools/pgweb_fetch.go +++ b/tools/pgweb_fetch.go @@ -27,22 +27,22 @@ func main() { { Name: "pgweb-linux-amd64", URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_linux_amd64.zip", version), - SHA256: "", + SHA256: "3d6c2063e1040b8a625eb7c43c9b84f8ed12cfc9a798eacbce85179963ee2554", }, { Name: "pgweb-linux-arm64", URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_linux_arm64.zip", version), - SHA256: "", + SHA256: "079c698a323ed6431ce7e6343ee5847c7da62afbf45dfb2e78f8289d7b381783", }, { Name: "pgweb-darwin-amd64", URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_darwin_amd64.zip", version), - SHA256: "", + SHA256: "c0a098e2eb9cf9f7c20161a2947522eb67eacbf2b6c3389c2f8e8c5ed7238957", }, { Name: "pgweb-darwin-arm64", URL: fmt.Sprintf("https://github.com/sosedoff/pgweb/releases/download/v%s/pgweb_darwin_arm64.zip", version), - SHA256: "", + SHA256: "c8f5fca847f461ba22a619e2d96cb1656cefdffd8f2aef2340e14fc5b518d3a2", }, } @@ -105,67 +105,33 @@ func fileSHA256(path string) (string, error) { } func unzipSingle(zipPath, outPath string) error { - // minimal unzip: because pgweb zip has only one binary - r, err := os.Open(zipPath) - if err != nil { - return err - } - defer r.Close() - - // use archive/zip - stat, err := os.Stat(zipPath) - if err != nil { - return err - } - return unzipFile(zipPath, outPath, stat.Size()) -} - -func unzipFile(zipFile, outFile string, _ int64) error { - r, err := os.Open(zipFile) - if err != nil { - return err - } - defer r.Close() - fi, _ := r.Stat() - - // rely on standard zip reader - data, err := io.ReadAll(r) - if err != nil { - return err - } - tmpZip := filepath.Join(os.TempDir(), fi.Name()) - if err := os.WriteFile(tmpZip, data, 0o644); err != nil { - return err - } - defer os.Remove(tmpZip) - - zr, err := os.Open(tmpZip) + zr, err := zip.OpenReader(zipPath) if err != nil { return err } defer zr.Close() - // extract using standard lib - zr2, err := zip.OpenReader(tmpZip) + + if len(zr.File) == 0 { + return fmt.Errorf("zip file %s is empty", zipPath) + } + + f := zr.File[0] + + rc, err := f.Open() if err != nil { return err } - defer zr2.Close() - for _, f := range zr2.File { - rc, err := f.Open() - if err != nil { - return err - } - defer rc.Close() - out, err := os.Create(outFile) - if err != nil { - return err - } - if _, err := io.Copy(out, rc); err != nil { - out.Close() - return err - } - out.Close() - break + defer rc.Close() + + out, err := os.Create(outPath) + if err != nil { + return err } + defer out.Close() + + if _, err := io.Copy(out, rc); err != nil { + return err + } + return nil } diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 3745553..30e8a1f 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -5,6 +5,7 @@ import { ProtectedRoute } from "@/components/protected-route.tsx" import { AnnotationPage } from "@/pages/annotations/annotation-page.tsx" import { Login } from "@/pages/auth/login.tsx" import { CredentialPage } from "@/pages/credentials/credential-page.tsx" +import { DnsPage } from "@/pages/dns/dns-page.tsx" import { JobsPage } from "@/pages/jobs/jobs-page.tsx" import { LabelsPage } from "@/pages/labels/labels-page.tsx" import { MePage } from "@/pages/me/me-page.tsx" @@ -35,6 +36,7 @@ export default function App() { } /> } /> } /> + } /> } /> diff --git a/ui/src/api/dns.ts b/ui/src/api/dns.ts new file mode 100644 index 0000000..d4c5c4f --- /dev/null +++ b/ui/src/api/dns.ts @@ -0,0 +1,49 @@ +import { withRefresh } from "@/api/with-refresh.ts" +import type { + DtoCreateDomainRequest, + DtoCreateRecordSetRequest, + DtoUpdateDomainRequest, + DtoUpdateRecordSetRequest, +} from "@/sdk" +import { makeDnsApi } from "@/sdkClient.ts" + +const dns = makeDnsApi() + +export const dnsApi = { + listDomains: () => + withRefresh(async () => { + return await dns.listDomains() + }), + getDomain: (id: string) => + withRefresh(async () => { + return await dns.getDomain({ id }) + }), + createDomain: async (body: DtoCreateDomainRequest) => + withRefresh(async () => { + return await dns.createDomain({ body }) + }), + updateDomain: async (id: string, body: DtoUpdateDomainRequest) => + withRefresh(async () => { + return await dns.updateDomain({ id, body }) + }), + deleteDomain: async (id: string) => + withRefresh(async () => { + return await dns.deleteDomain({ id }) + }), + listRecordSetsByDomain: async (domainId: string) => + withRefresh(async () => { + return await dns.listRecordSets({ domainId }) + }), + createRecordSetsByDomain: async (domainId: string, body: DtoCreateRecordSetRequest) => + withRefresh(async () => { + return await dns.createRecordSet({ domainId, body }) + }), + updateRecordSetsByDomain: async (id: string, body: DtoUpdateRecordSetRequest) => + withRefresh(async () => { + return await dns.updateRecordSet({ id, body }) + }), + deleteRecordSetsByDomain: async (id: string) => + withRefresh(async () => { + return await dns.deleteRecordSet({ id }) + }), +} \ No newline at end of file diff --git a/ui/src/layouts/nav-config.ts b/ui/src/layouts/nav-config.ts index f99c46c..b4acbb1 100644 --- a/ui/src/layouts/nav-config.ts +++ b/ui/src/layouts/nav-config.ts @@ -14,6 +14,7 @@ import { } from "lucide-react" import { AiOutlineCluster } from "react-icons/ai" import { GrUserWorker } from "react-icons/gr" +import { MdOutlineDns } from "react-icons/md" export type NavItem = { to: string @@ -23,6 +24,7 @@ export type NavItem = { export const mainNav: NavItem[] = [ { to: "/clusters", label: "Clusters", icon: AiOutlineCluster }, + { to: "/dns", label: "DNS", icon: MdOutlineDns }, { to: "/node-pools", label: "Node Pools", icon: BoxesIcon }, { to: "/annotations", label: "Annotations", icon: ComponentIcon }, { to: "/labels", label: "Labels", icon: TagsIcon }, diff --git a/ui/src/pages/credentials/credential-page.tsx b/ui/src/pages/credentials/credential-page.tsx index 4379c5a..902cfbf 100644 --- a/ui/src/pages/credentials/credential-page.tsx +++ b/ui/src/pages/credentials/credential-page.tsx @@ -2,7 +2,16 @@ import { useMemo, useState } from "react" import { credentialsApi } from "@/api/credentials" import { zodResolver } from "@hookform/resolvers/zod" import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" -import { AlertTriangle, Eye, Loader2, MoreHorizontal, Pencil, Plus, Search, Trash2, } from "lucide-react" +import { + AlertTriangle, + Eye, + Loader2, + MoreHorizontal, + Pencil, + Plus, + Search, + Trash2, +} from "lucide-react" import { Controller, useForm } from "react-hook-form" import { toast } from "sonner" import { z } from "zod" @@ -20,16 +29,36 @@ import { } from "@/components/ui/alert-dialog" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" -import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" import { Input } from "@/components/ui/input" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" import { Switch } from "@/components/ui/switch" import { Textarea } from "@/components/ui/textarea" diff --git a/ui/src/pages/dns/dns-page.tsx b/ui/src/pages/dns/dns-page.tsx new file mode 100644 index 0000000..58f2505 --- /dev/null +++ b/ui/src/pages/dns/dns-page.tsx @@ -0,0 +1,1040 @@ +import { useEffect, useMemo, useState } from "react" +import { credentialsApi } from "@/api/credentials" +import { dnsApi } from "@/api/dns" +import type { + DtoCreateDomainRequest, + DtoCreateRecordSetRequest, + DtoCredentialOut, + DtoDomainResponse, + DtoRecordSetResponse, + DtoUpdateDomainRequest, + DtoUpdateRecordSetRequest, +} from "@/sdk" +import { zodResolver } from "@hookform/resolvers/zod" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" +import { + AlertTriangle, + CheckCircle2, + Circle, + Loader2, + MoreHorizontal, + Pencil, + Plus, + Search, + Trash2, +} from "lucide-react" +import { useForm } from "react-hook-form" +import { toast } from "sonner" +import { z } from "zod" + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Card } from "@/components/ui/card" +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Textarea } from "@/components/ui/textarea" + +// ---------- helpers ---------- + +const statusIcon = (s?: string) => { + switch (s) { + case "ready": + return + case "provisioning": + return + case "failed": + return + default: + return + } +} + +const StatusBadge = ({ s }: { s?: string }) => ( + + {statusIcon(s)} + {s ?? "pending"} + +) + +const parseCommaList = (v: string) => + v + .split(",") + .map((s) => s.trim()) + .filter(Boolean) + +const joinCommaList = (arr?: string[] | null) => (arr && arr.length ? arr.join(",") : "") + +const rrtypes = ["A", "AAAA", "CNAME", "TXT", "MX", "NS", "SRV", "CAA"] + +const isR53 = (c: DtoCredentialOut) => + c.provider === "aws" && + c.scope_kind === "service" && + (() => { + const s = (c as any).scope + try { + const obj = typeof s === "string" ? JSON.parse(s) : s || {} + return obj?.service === "route53" + } catch { + return false + } + })() + +const credLabel = (c: DtoCredentialOut) => { + const bits = [c.name || "Unnamed", c.account_id, c.region].filter(Boolean) + return bits.join(" · ") +} + +// ---------- zod schemas ---------- + +const createDomainSchema = z.object({ + domain_name: z + .string() + .min(1, "Domain is required") + .max(253) + .transform((s) => s.trim().replace(/\.$/, "").toLowerCase()), + credential_id: z.string().uuid("Pick a credential"), + zone_id: z + .string() + .optional() + .or(z.literal("")) + .transform((v) => (v ? v.trim() : undefined)), +}) +type CreateDomainValues = z.input + +const updateDomainSchema = createDomainSchema.partial() +type UpdateDomainValues = z.infer + +const ttlSchema = z + .union([ + z.number(), + z + .string() + .regex(/^\d+$/) + .transform((s) => Number(s)), + ]) + .optional() + .refine((v) => v === undefined || (v >= 1 && v <= 86400), { + message: "TTL must be between 1 and 86400", + }) + +const createRecordSchema = z + .object({ + name: z + .string() + .min(1, "Name required") + .max(253) + .transform((s) => s.trim().replace(/\.$/, "").toLowerCase()), + type: z.enum(rrtypes as [string, ...string[]]), + ttl: ttlSchema, + valuesCsv: z.string().optional(), + }) + .superRefine((vals, ctx) => { + const arr = parseCommaList(vals.valuesCsv ?? "") + if (arr.length === 0) { + ctx.addIssue({ code: "custom", message: "At least one value is required" }) + } + if (vals.type === "CNAME" && arr.length !== 1) { + ctx.addIssue({ code: "custom", message: "CNAME requires exactly one value" }) + } + }) +type CreateRecordValues = z.input + +const updateRecordSchema = createRecordSchema.partial() +type UpdateRecordValues = z.input + +// ---------- main ---------- + +export const DnsPage = () => { + const [filter, setFilter] = useState("") + const [selected, setSelected] = useState(null) + + const [createDomOpen, setCreateDomOpen] = useState(false) + const [editDomOpen, setEditDomOpen] = useState(false) + + const [createRecOpen, setCreateRecOpen] = useState(false) + const [editRecOpen, setEditRecOpen] = useState(false) + const [editingRecord, setEditingRecord] = useState(null) + + const qc = useQueryClient() + + // ---- queries ---- + + const domainsQ = useQuery({ + queryKey: ["dns", "domains"], + queryFn: () => dnsApi.listDomains(), + }) + + const recordsQ = useQuery({ + queryKey: ["dns", "records", selected?.id], + queryFn: async () => { + if (!selected) return [] + return await dnsApi.listRecordSetsByDomain(selected.id as string) + }, + enabled: !!selected?.id, + }) + + const credentialQ = useQuery({ + queryKey: ["credentials", "r53"], + queryFn: () => credentialsApi.listCredentials(), + }) + + const r53Credentials = useMemo(() => (credentialQ.data ?? []).filter(isR53), [credentialQ.data]) + + useEffect(() => { + if (!selected && domainsQ.data && domainsQ.data.length) { + setSelected(domainsQ.data[0]!) + } + }, [domainsQ.data, selected]) + + const filteredDomains = useMemo(() => { + const list: DtoDomainResponse[] = domainsQ.data ?? [] + if (!filter.trim()) return list + const f = filter.toLowerCase() + return list.filter((d) => + [d.domain_name, d.zone_id, d.status, d.domain_name] + .filter(Boolean) + .map((x) => String(x).toLowerCase()) + .some((s) => s.includes(f)) + ) + }, [domainsQ.data, filter]) + + // ---- mutations: domains ---- + + const createDomainForm = useForm({ + resolver: zodResolver(createDomainSchema), + defaultValues: { + domain_name: "", + credential_id: "", + zone_id: "", + }, + }) + + const createDomainMut = useMutation({ + mutationFn: (v: CreateDomainValues) => + dnsApi.createDomain(v as unknown as DtoCreateDomainRequest), + onSuccess: async (d) => { + toast.success("Domain created") + setCreateDomOpen(false) + createDomainForm.reset() + await qc.invalidateQueries({ queryKey: ["dns", "domains"] }) + setSelected(d as DtoDomainResponse) + }, + onError: (e: any) => + toast.error("Failed to create domain", { description: e?.message ?? "Unknown error" }), + }) + + const editDomainForm = useForm({ + resolver: zodResolver(updateDomainSchema), + }) + + const openEditDomain = (d: DtoDomainResponse) => { + setSelected(d) + editDomainForm.reset({ + domain_name: d.domain_name, + credential_id: d.credential_id, + zone_id: d.zone_id || "", + }) + setEditDomOpen(true) + } + + const updateDomainMut = useMutation({ + mutationFn: (vals: UpdateDomainValues) => { + if (!selected) throw new Error("No domain selected") + return dnsApi.updateDomain(selected.id!, vals as unknown as DtoUpdateDomainRequest) + }, + onSuccess: async () => { + toast.success("Domain updated") + setEditDomOpen(false) + await qc.invalidateQueries({ queryKey: ["dns", "domains"] }) + await qc.invalidateQueries({ queryKey: ["dns", "records", selected?.id] }) + }, + onError: (e: any) => + toast.error("Failed to update domain", { description: e?.message ?? "Unknown error" }), + }) + + const deleteDomainMut = useMutation({ + mutationFn: (id: string) => dnsApi.deleteDomain(id), + onSuccess: async () => { + toast.success("Domain deleted") + await qc.invalidateQueries({ queryKey: ["dns", "domains"] }) + setSelected(null) + }, + onError: (e: any) => + toast.error("Failed to delete domain", { description: e?.message ?? "Unknown error" }), + }) + + // ---- mutations: record sets ---- + + const createRecForm = useForm({ + resolver: zodResolver(createRecordSchema), + defaultValues: { + name: "", + type: "A", + ttl: 300, + valuesCsv: "", + }, + }) + + const explainError = (e: any) => { + const msg: string = e?.response?.data?.error || e?.message || "Unknown error" + if (msg.includes("ownership_conflict")) { + return "Ownership conflict: this (name,type) exists but isn’t owned by autoglue." + } + if (msg.includes("already_exists")) { + return "A record with this (name,type) already exists. Use Edit instead." + } + return msg + } + + const createRecordMut = useMutation({ + mutationFn: async (vals: CreateRecordValues) => { + if (!selected) throw new Error("No domain selected") + const body: DtoCreateRecordSetRequest = { + name: vals.name, + type: vals.type, + // omit ttl when empty/undefined + ...(vals.ttl ? { ttl: vals.ttl as unknown as number } : {}), + values: parseCommaList(vals.valuesCsv ?? ""), + } + return dnsApi.createRecordSetsByDomain(selected.id!, body) + }, + onSuccess: async () => { + toast.success("Record set created") + setCreateRecOpen(false) + createRecForm.reset() + await qc.invalidateQueries({ queryKey: ["dns", "records", selected?.id] }) + }, + onError: (e: any) => + toast.error("Failed to create record set", { description: explainError(e) }), + }) + + const editRecForm = useForm({ + resolver: zodResolver(updateRecordSchema), + }) + + const openEditRecord = (r: DtoRecordSetResponse) => { + setEditingRecord(r) + const values = (r.values as any) || [] + editRecForm.reset({ + name: r.name, + type: r.type, + ttl: r.ttl ? Number(r.ttl) : undefined, + valuesCsv: joinCommaList(values), + }) + setEditRecOpen(true) + } + + const updateRecordMut = useMutation({ + mutationFn: async (vals: UpdateRecordValues) => { + if (!editingRecord) throw new Error("No record selected") + const body: DtoUpdateRecordSetRequest = {} + if (vals.name !== undefined) body.name = vals.name + if (vals.type !== undefined) body.type = vals.type + if (vals.ttl !== undefined && vals.ttl !== null) { + // if blank string came through it would have been filtered; when undefined, omit + body.ttl = vals.ttl as unknown as number | undefined + } + if (vals.valuesCsv !== undefined) { + body.values = parseCommaList(vals.valuesCsv) + } + return dnsApi.updateRecordSetsByDomain(editingRecord.id!, body) + }, + onSuccess: async () => { + toast.success("Record set updated") + setEditRecOpen(false) + setEditingRecord(null) + await qc.invalidateQueries({ queryKey: ["dns", "records", selected?.id] }) + }, + onError: (e: any) => + toast.error("Failed to update record set", { description: explainError(e) }), + }) + + const deleteRecordMut = useMutation({ + mutationFn: (id: string) => dnsApi.deleteRecordSetsByDomain(id), + onSuccess: async () => { + toast.success("Record set deleted") + await qc.invalidateQueries({ queryKey: ["dns", "records", selected?.id] }) + }, + onError: (e: any) => + toast.error("Failed to delete record set", { description: e?.message ?? "Unknown error" }), + }) + + // ---------- UI ---------- + + return ( +
+
+

DNS

+
+
+ + setFilter(e.target.value)} + placeholder="Search domains…" + className="w-64 pl-8" + /> +
+ + + + + + + Add Domain + +
+ createDomainMut.mutate(v))} + > + ( + + Domain + + + + + + )} + /> + + {/* CREDENTIAL SELECT (Create) */} + ( + + Route53 Credential + + {credentialQ.error && ( +

Failed to load credentials.

+ )} + +
+ )} + /> + + ( + + Zone ID (optional) + + + + + + )} + /> + + + + + + +
+
+
+
+ + {/* domains panel */} +
+ +
+
Domains
+ {domainsQ.isFetching && } +
+
+ + + + + + + + + + + {(filteredDomains ?? []).map((d) => ( + setSelected(d)} + > + + + + + + ))} + {(!filteredDomains || filteredDomains.length === 0) && ( + + + + )} + +
DomainZoneStatusActions
{d.domain_name}{d.zone_id || "—"} + + +
+ + + + + + + + Delete “{d.domain_name}”? + + This deletes the domain metadata. External DNS records are not + touched. + + + + Cancel + deleteDomainMut.mutate(d.id!)} + > + Delete + + + + +
+
+ No domains yet. +
+
+
+
+ +
+ {/* records panel */} + +
+
+ Records {selected ? `— ${selected.domain_name}` : ""} +
+
+ + + + + + + + Add Record + +
+ createRecordMut.mutate(v))} + > +
+ ( + + Name + + + + + + )} + /> + ( + + Type + + + + )} + /> + ( + + TTL (sec, optional) + + + field.onChange( + e.target.value === "" ? undefined : Number(e.target.value) + ) + } + placeholder="300" + /> + + + + )} + /> +
+ + ( + + Values (comma-separated) + +