mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 04:40:05 +01:00
feat: Complete AG Loadbalancer & Cluster API
Refactor routing logic (Chi can be a pain when you're managing large sets of routes, but its one of the better options when considering a potential gRPC future)
Upgrade API Generation to fully support OAS3.1
Update swagger interface to RapiDoc - the old swagger interface doesnt support OAS3.1 yet
Docs are now embedded as part of the UI - once logged in they pick up the cookies and org id from what gets set by the UI, but you can override it
Other updates include better portability of the db-studio
Signed-off-by: allanice001 <allanice001@gmail.com>
This commit is contained in:
@@ -32,6 +32,8 @@ func mountAPIRoutes(r chi.Router, db *gorm.DB, jobs *bg.Jobs) {
|
||||
mountAnnotationRoutes(v1, db, authOrg)
|
||||
mountNodePoolRoutes(v1, db, authOrg)
|
||||
mountDNSRoutes(v1, db, authOrg)
|
||||
mountLoadBalancerRoutes(v1, db, authOrg)
|
||||
mountClusterRoutes(v1, db, authOrg)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
39
internal/api/mount_cluster_routes.go
Normal file
39
internal/api/mount_cluster_routes.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/glueops/autoglue/internal/handlers"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func mountClusterRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||
r.Route("/clusters", func(c chi.Router) {
|
||||
c.Use(authOrg)
|
||||
c.Get("/", handlers.ListClusters(db))
|
||||
c.Post("/", handlers.CreateCluster(db))
|
||||
|
||||
c.Get("/{clusterID}", handlers.GetCluster(db))
|
||||
c.Patch("/{clusterID}", handlers.UpdateCluster(db))
|
||||
c.Delete("/{clusterID}", handlers.DeleteCluster(db))
|
||||
|
||||
c.Post("/{clusterID}/captain-domain", handlers.AttachCaptainDomain(db))
|
||||
c.Delete("/{clusterID}/captain-domain", handlers.DetachCaptainDomain(db))
|
||||
|
||||
c.Post("/{clusterID}/control-plane-record-set", handlers.AttachControlPlaneRecordSet(db))
|
||||
c.Delete("/{clusterID}/control-plane-record-set", handlers.DetachControlPlaneRecordSet(db))
|
||||
|
||||
c.Post("/{clusterID}/apps-load-balancer", handlers.AttachAppsLoadBalancer(db))
|
||||
c.Delete("/{clusterID}/apps-load-balancer", handlers.DetachAppsLoadBalancer(db))
|
||||
c.Post("/{clusterID}/glueops-load-balancer", handlers.AttachGlueOpsLoadBalancer(db))
|
||||
c.Delete("/{clusterID}/glueops-load-balancer", handlers.DetachGlueOpsLoadBalancer(db))
|
||||
|
||||
c.Post("/{clusterID}/bastion", handlers.AttachBastionServer(db))
|
||||
c.Delete("/{clusterID}/bastion", handlers.DetachBastionServer(db))
|
||||
|
||||
c.Post("/{clusterID}/kubeconfig", handlers.SetClusterKubeconfig(db))
|
||||
c.Delete("/{clusterID}/kubeconfig", handlers.ClearClusterKubeconfig(db))
|
||||
|
||||
})
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
pgcmd "github.com/sosedoff/pgweb/pkg/command"
|
||||
)
|
||||
|
||||
func PgwebHandler(dbURL, prefix string, readonly bool) (http.Handler, error) {
|
||||
func MountDbStudio(dbURL, prefix string, readonly bool) (http.Handler, error) {
|
||||
// Normalize prefix for pgweb:
|
||||
// - no leading slash
|
||||
// - always trailing slash if not empty
|
||||
|
||||
20
internal/api/mount_load_balancer_routes.go
Normal file
20
internal/api/mount_load_balancer_routes.go
Normal file
@@ -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 mountLoadBalancerRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) http.Handler) {
|
||||
r.Route("/load-balancers", func(l chi.Router) {
|
||||
l.Use(authOrg)
|
||||
l.Get("/", handlers.ListLoadBalancers(db))
|
||||
l.Post("/", handlers.CreateLoadBalancer(db))
|
||||
l.Get("/{id}", handlers.GetLoadBalancer(db))
|
||||
l.Patch("/{id}", handlers.UpdateLoadBalancer(db))
|
||||
l.Delete("/{id}", handlers.DeleteLoadBalancer(db))
|
||||
})
|
||||
}
|
||||
@@ -1,15 +1,87 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"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", RapidDocHandler("/swagger/swagger.yaml"))
|
||||
r.Get("/swagger/index.html", RapidDocHandler("/swagger/swagger.yaml"))
|
||||
r.Get("/swagger/swagger.json", serveSwaggerFromEmbed(docs.SwaggerJSON, "application/json"))
|
||||
r.Get("/swagger/swagger.yaml", serveSwaggerFromEmbed(docs.SwaggerYAML, "application/x-yaml"))
|
||||
}
|
||||
|
||||
var rapidDocTmpl = template.Must(template.New("redoc").Parse(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>AutoGlue API Docs</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { margin: 0; padding: 0; }
|
||||
.redoc-container { height: 100vh; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<rapi-doc
|
||||
id="autoglue-docs"
|
||||
spec-url="{{.SpecURL}}"
|
||||
render-style="read"
|
||||
theme="dark"
|
||||
show-header="false"
|
||||
persist-auth="true"
|
||||
allow-advanced-search="true"
|
||||
schema-description-expanded="true"
|
||||
allow-schema-description-expand-toggle="false"
|
||||
allow-spec-file-download="true"
|
||||
allow-spec-file-load="false"
|
||||
allow-spec-url-load="false"
|
||||
allow-try="true"
|
||||
schema-style="tree"
|
||||
fetch-credentials="include"
|
||||
default-api-server="{{.DefaultServer}}"
|
||||
api-key-name="X-ORG-ID"
|
||||
api-key-location="header"
|
||||
api-key-value=""
|
||||
/>
|
||||
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const rd = document.getElementById('autoglue-docs');
|
||||
if (!rd) return;
|
||||
|
||||
const storedOrg = localStorage.getItem('autoglue.org');
|
||||
if (storedOrg) {
|
||||
rd.setAttribute('api-key-value', storedOrg);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`))
|
||||
|
||||
func RapidDocHandler(specURL string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
scheme := "http"
|
||||
if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
host := r.Host
|
||||
defaultServer := fmt.Sprintf("%s://%s/api/v1", scheme, host)
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := rapidDocTmpl.Execute(w, map[string]string{
|
||||
"SpecURL": specURL,
|
||||
"DefaultServer": defaultServer,
|
||||
}); err != nil {
|
||||
http.Error(w, "failed to render docs", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,14 +29,14 @@ func SecurityHeaders(next http.Handler) http.Handler {
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
// Vite dev & inline preamble/eval:
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:5173",
|
||||
"script-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:5173 https://unpkg.com",
|
||||
// allow dev style + Google Fonts
|
||||
"style-src 'self' 'unsafe-inline' http://localhost:5173 https://fonts.googleapis.com",
|
||||
"img-src 'self' data: blob:",
|
||||
// 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 https://api.github.com",
|
||||
"connect-src 'self' http://localhost:5173 ws://localhost:5173 ws://localhost:8080 https://api.github.com https://unpkg.com",
|
||||
"frame-ancestors 'none'",
|
||||
}, "; "))
|
||||
} else {
|
||||
@@ -49,11 +49,11 @@ func SecurityHeaders(next http.Handler) http.Handler {
|
||||
"default-src 'self'",
|
||||
"base-uri 'self'",
|
||||
"form-action 'self'",
|
||||
"script-src 'self' 'unsafe-inline'",
|
||||
"script-src 'self' 'unsafe-inline' https://unpkg.com",
|
||||
"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' ws://localhost:8080 https://api.github.com",
|
||||
"connect-src 'self' ws://localhost:8080 https://api.github.com https://unpkg.com",
|
||||
"frame-ancestors 'none'",
|
||||
}, "; "))
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ func NewRouter(db *gorm.DB, jobs *bg.Jobs, studio http.Handler) http.Handler {
|
||||
r.Use(SecurityHeaders)
|
||||
r.Use(requestBodyLimit(10 << 20))
|
||||
r.Use(httprate.LimitByIP(100, 1*time.Minute))
|
||||
r.Use(middleware.StripSlashes)
|
||||
|
||||
allowed := getAllowedOrigins()
|
||||
r.Use(cors.Handler(cors.Options{
|
||||
@@ -103,18 +104,19 @@ func NewRouter(db *gorm.DB, jobs *bg.Jobs, studio http.Handler) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/api/", r)
|
||||
mux.Handle("/api", r)
|
||||
mux.Handle("/swagger", r)
|
||||
mux.Handle("/swagger/", r)
|
||||
mux.Handle("/db-studio/", r)
|
||||
mux.Handle("/debug/pprof/", r)
|
||||
mux.Handle("/", proxy)
|
||||
return mux
|
||||
}
|
||||
|
||||
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")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
|
||||
@@ -40,6 +40,7 @@ func serveSwaggerFromEmbed(data []byte, contentType string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
// nosemgrep: go.lang.security.audit.xss.no-direct-write-to-responsewriter
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,10 +39,11 @@ func NewRuntime() *Runtime {
|
||||
&models.Label{},
|
||||
&models.Annotation{},
|
||||
&models.NodePool{},
|
||||
&models.Cluster{},
|
||||
&models.Credential{},
|
||||
&models.Domain{},
|
||||
&models.RecordSet{},
|
||||
&models.LoadBalancer{},
|
||||
&models.Cluster{},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -140,10 +140,7 @@ func BastionBootstrapWorker(db *gorm.DB, jobs *Jobs) archer.WorkerFn {
|
||||
_ = setServerStatus(db, s.ID, "failed")
|
||||
continue
|
||||
}
|
||||
|
||||
ok++
|
||||
// logHostInfo(jobID, s, "done", "host completed",
|
||||
// "elapsed_ms", time.Since(hostStart).Milliseconds())
|
||||
}
|
||||
|
||||
res := BastionBootstrapResult{
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
// @Summary List annotations (org scoped)
|
||||
// @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.
|
||||
// @Tags Annotations
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param key query string false "Exact key"
|
||||
@@ -75,7 +74,6 @@ func ListAnnotations(db *gorm.DB) http.HandlerFunc {
|
||||
// @Summary Get annotation by ID (org scoped)
|
||||
// @Description Returns one annotation. Add `include=node_pools` to include node pools.
|
||||
// @Tags Annotations
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Annotation ID (UUID)"
|
||||
@@ -255,11 +253,10 @@ func UpdateAnnotation(db *gorm.DB) http.HandlerFunc {
|
||||
// @Summary Delete annotation (org scoped)
|
||||
// @Description Permanently deletes the annotation.
|
||||
// @Tags Annotations
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Annotation ID (UUID)"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Annotation ID (UUID)"
|
||||
// @Success 204 "No Content"
|
||||
// @Failure 400 {string} string "invalid id"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
|
||||
@@ -2,7 +2,9 @@ package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
@@ -252,10 +254,11 @@ func AuthCallback(db *gorm.DB) http.HandlerFunc {
|
||||
accessTTL := 1 * time.Hour
|
||||
refreshTTL := 30 * 24 * time.Hour
|
||||
|
||||
cfgLoaded, _ := config.Load()
|
||||
access, err := auth.IssueAccessToken(auth.IssueOpts{
|
||||
Subject: user.ID.String(),
|
||||
Issuer: cfg.JWTIssuer,
|
||||
Audience: cfg.JWTAudience,
|
||||
Issuer: cfgLoaded.JWTIssuer,
|
||||
Audience: cfgLoaded.JWTAudience,
|
||||
TTL: accessTTL,
|
||||
Claims: map[string]any{
|
||||
"email": email,
|
||||
@@ -273,7 +276,10 @@ func AuthCallback(db *gorm.DB) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
secure := strings.HasPrefix(cfg.OAuthRedirectBase, "https://")
|
||||
secure := true
|
||||
if u, err := url.Parse(cfg.OAuthRedirectBase); err == nil && isLocalDev(u) {
|
||||
secure = false
|
||||
}
|
||||
if xf := r.Header.Get("X-Forwarded-Proto"); xf != "" {
|
||||
secure = strings.EqualFold(xf, "https")
|
||||
}
|
||||
@@ -291,14 +297,7 @@ func AuthCallback(db *gorm.DB) http.HandlerFunc {
|
||||
// If the state indicates SPA popup mode, postMessage tokens to the opener and close
|
||||
state := r.URL.Query().Get("state")
|
||||
if strings.Contains(state, "mode=spa") {
|
||||
origin := ""
|
||||
for _, part := range strings.Split(state, "|") {
|
||||
if strings.HasPrefix(part, "origin=") {
|
||||
origin, _ = url.QueryUnescape(strings.TrimPrefix(part, "origin="))
|
||||
break
|
||||
}
|
||||
}
|
||||
// fallback: restrict to backend origin if none supplied
|
||||
origin := canonicalOrigin(cfg.OAuthRedirectBase)
|
||||
if origin == "" {
|
||||
origin = cfg.OAuthRedirectBase
|
||||
}
|
||||
@@ -371,7 +370,10 @@ func Refresh(db *gorm.DB) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
secure := strings.HasPrefix(cfg.OAuthRedirectBase, "https://")
|
||||
secure := true
|
||||
if uParsed, err := url.Parse(cfg.OAuthRedirectBase); err == nil && isLocalDev(uParsed) {
|
||||
secure = false
|
||||
}
|
||||
if xf := r.Header.Get("X-Forwarded-Proto"); xf != "" {
|
||||
secure = strings.EqualFold(xf, "https")
|
||||
}
|
||||
@@ -424,6 +426,11 @@ func Logout(db *gorm.DB) http.HandlerFunc {
|
||||
}
|
||||
|
||||
clearCookie:
|
||||
secure := true
|
||||
if uParsed, err := url.Parse(cfg.OAuthRedirectBase); err == nil && isLocalDev(uParsed) {
|
||||
secure = false
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "ag_jwt",
|
||||
Value: "",
|
||||
@@ -432,11 +439,10 @@ func Logout(db *gorm.DB) http.HandlerFunc {
|
||||
MaxAge: -1,
|
||||
Expires: time.Unix(0, 0),
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
Secure: strings.HasPrefix(cfg.OAuthRedirectBase, "https"),
|
||||
Secure: secure,
|
||||
})
|
||||
|
||||
w.WriteHeader(204)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,21 +512,63 @@ func ensureAutoMembership(db *gorm.DB, userID uuid.UUID, email string) error {
|
||||
}).Error
|
||||
}
|
||||
|
||||
// postMessage HTML template
|
||||
var postMessageTpl = template.Must(template.New("postmsg").Parse(`<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
<script>
|
||||
(function(){
|
||||
try {
|
||||
var data = JSON.parse(atob("{{.PayloadB64}}"));
|
||||
if (window.opener) {
|
||||
window.opener.postMessage(
|
||||
{ type: 'autoglue:auth', payload: data },
|
||||
"{{.Origin}}"
|
||||
);
|
||||
}
|
||||
} catch (e) {}
|
||||
window.close();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>`))
|
||||
|
||||
type postMessageData struct {
|
||||
Origin string
|
||||
PayloadB64 string
|
||||
}
|
||||
|
||||
// writePostMessageHTML sends a tiny HTML page that posts tokens to the SPA and closes the window.
|
||||
func writePostMessageHTML(w http.ResponseWriter, origin string, payload dto.TokenPair) {
|
||||
b, _ := json.Marshal(payload)
|
||||
|
||||
data := postMessageData{
|
||||
Origin: origin,
|
||||
PayloadB64: base64.StdEncoding.EncodeToString(b),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`<!doctype html><html><body><script>
|
||||
(function(){
|
||||
try {
|
||||
var data = ` + string(b) + `;
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({ type: 'autoglue:auth', payload: data }, '` + origin + `');
|
||||
}
|
||||
} catch (e) {}
|
||||
window.close();
|
||||
})();
|
||||
</script></body></html>`))
|
||||
_ = postMessageTpl.Execute(w, data)
|
||||
}
|
||||
|
||||
// canonicalOrigin returns scheme://host[:port] for a given URL, or "" if invalid.
|
||||
func canonicalOrigin(raw string) string {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil || u.Scheme == "" || u.Host == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Normalize: no path/query/fragment — just the origin.
|
||||
return (&url.URL{
|
||||
Scheme: u.Scheme,
|
||||
Host: u.Host,
|
||||
}).String()
|
||||
}
|
||||
|
||||
func isLocalDev(u *url.URL) bool {
|
||||
host := strings.ToLower(u.Hostname())
|
||||
return u.Scheme == "http" &&
|
||||
(host == "localhost" || host == "127.0.0.1")
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,24 +23,24 @@ import (
|
||||
)
|
||||
|
||||
// ListCredentials godoc
|
||||
// @ID ListCredentials
|
||||
// @Summary List credentials (metadata only)
|
||||
// @Description Returns credential metadata for the current org. Secrets are never returned.
|
||||
// @Tags Credentials
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param provider query string false "Filter by provider (e.g., aws)"
|
||||
// @Param kind query string false "Filter by kind (e.g., aws_access_key)"
|
||||
// @Param scope_kind query string false "Filter by scope kind (provider/service/resource)"
|
||||
// @Success 200 {array} dto.CredentialOut
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "internal server error"
|
||||
// @Router /credentials [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
//
|
||||
// @ID ListCredentials
|
||||
// @Summary List credentials (metadata only)
|
||||
// @Description Returns credential metadata for the current org. Secrets are never returned.
|
||||
// @Tags Credentials
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param provider query string false "Filter by provider (e.g., aws)"
|
||||
// @Param kind query string false "Filter by kind (e.g., aws_access_key)"
|
||||
// @Param scope_kind query string false "Filter by scope kind (provider/service/resource)"
|
||||
// @Success 200 {array} dto.CredentialOut
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "internal server error"
|
||||
// @Router /credentials [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func ListCredentials(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
@@ -73,21 +73,21 @@ func ListCredentials(db *gorm.DB) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// GetCredential godoc
|
||||
// @ID GetCredential
|
||||
// @Summary Get credential by ID (metadata only)
|
||||
// @Tags Credentials
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param id path string true "Credential ID (UUID)"
|
||||
// @Success 200 {object} dto.CredentialOut
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "internal server error"
|
||||
// @Router /credentials/{id} [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
//
|
||||
// @ID GetCredential
|
||||
// @Summary Get credential by ID (metadata only)
|
||||
// @Tags Credentials
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param id path string true "Credential ID (UUID)"
|
||||
// @Success 200 {object} dto.CredentialOut
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "internal server error"
|
||||
// @Router /credentials/{id} [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func GetCredential(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
@@ -117,21 +117,22 @@ func GetCredential(db *gorm.DB) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// CreateCredential godoc
|
||||
// @ID CreateCredential
|
||||
// @Summary Create a credential (encrypts secret)
|
||||
// @Tags Credentials
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param body body dto.CreateCredentialRequest true "Credential payload"
|
||||
// @Success 201 {object} dto.CredentialOut
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "internal server error"
|
||||
// @Router /credentials [post]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
//
|
||||
// @ID CreateCredential
|
||||
// @Summary Create a credential (encrypts secret)
|
||||
// @Tags Credentials
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param body body dto.CreateCredentialRequest true "Credential payload"
|
||||
// @Success 201 {object} dto.CredentialOut
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "internal server error"
|
||||
// @Router /credentials [post]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func CreateCredential(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
@@ -166,21 +167,22 @@ func CreateCredential(db *gorm.DB) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// UpdateCredential godoc
|
||||
// @ID UpdateCredential
|
||||
// @Summary Update credential metadata and/or rotate secret
|
||||
// @Tags Credentials
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param id path string true "Credential ID (UUID)"
|
||||
// @Param body body dto.UpdateCredentialRequest true "Fields to update"
|
||||
// @Success 200 {object} dto.CredentialOut
|
||||
// @Failure 403 {string} string "X-Org-ID required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /credentials/{id} [patch]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
//
|
||||
// @ID UpdateCredential
|
||||
// @Summary Update credential metadata and/or rotate secret
|
||||
// @Tags Credentials
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param id path string true "Credential ID (UUID)"
|
||||
// @Param body body dto.UpdateCredentialRequest true "Fields to update"
|
||||
// @Success 200 {object} dto.CredentialOut
|
||||
// @Failure 403 {string} string "X-Org-ID required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /credentials/{id} [patch]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func UpdateCredential(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
@@ -296,19 +298,19 @@ func UpdateCredential(db *gorm.DB) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// DeleteCredential godoc
|
||||
// @ID DeleteCredential
|
||||
// @Summary Delete credential
|
||||
// @Tags Credentials
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param id path string true "Credential ID (UUID)"
|
||||
// @Success 204
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /credentials/{id} [delete]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
//
|
||||
// @ID DeleteCredential
|
||||
// @Summary Delete credential
|
||||
// @Tags Credentials
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param id path string true "Credential ID (UUID)"
|
||||
// @Success 204
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /credentials/{id} [delete]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func DeleteCredential(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
@@ -335,20 +337,21 @@ func DeleteCredential(db *gorm.DB) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// RevealCredential godoc
|
||||
// @ID RevealCredential
|
||||
// @Summary Reveal decrypted secret (one-time read)
|
||||
// @Tags Credentials
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param id path string true "Credential ID (UUID)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /credentials/{id}/reveal [post]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
//
|
||||
// @ID RevealCredential
|
||||
// @Summary Reveal decrypted secret (one-time read)
|
||||
// @Tags Credentials
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization ID (UUID)"
|
||||
// @Param id path string true "Credential ID (UUID)"
|
||||
// @Success 200 {object} map[string]any
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /credentials/{id}/reveal [post]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func RevealCredential(db *gorm.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
|
||||
|
||||
@@ -166,7 +166,6 @@ func mustSameOrgDomainWithCredential(db *gorm.DB, orgID uuid.UUID, credID uuid.U
|
||||
// @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)"
|
||||
@@ -213,21 +212,20 @@ func ListDomains(db *gorm.DB) http.HandlerFunc {
|
||||
|
||||
// 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
|
||||
// @ID GetDomain
|
||||
// @Summary Get a domain (org scoped)
|
||||
// @Tags DNS
|
||||
// @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())
|
||||
@@ -261,7 +259,7 @@ func GetDomain(db *gorm.DB) http.HandlerFunc {
|
||||
// @Tags DNS
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @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"
|
||||
@@ -312,22 +310,22 @@ func CreateDomain(db *gorm.DB) http.HandlerFunc {
|
||||
|
||||
// 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
|
||||
// @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())
|
||||
@@ -390,20 +388,19 @@ func UpdateDomain(db *gorm.DB) http.HandlerFunc {
|
||||
|
||||
// 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
|
||||
// @ID DeleteDomain
|
||||
// @Summary Delete a domain
|
||||
// @Tags DNS
|
||||
// @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())
|
||||
@@ -437,13 +434,12 @@ func DeleteDomain(db *gorm.DB) http.HandlerFunc {
|
||||
// @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"
|
||||
// @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"
|
||||
@@ -509,22 +505,22 @@ func ListRecordSets(db *gorm.DB) http.HandlerFunc {
|
||||
|
||||
// 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
|
||||
// @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())
|
||||
@@ -610,22 +606,22 @@ func CreateRecordSet(db *gorm.DB) http.HandlerFunc {
|
||||
|
||||
// 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
|
||||
// @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())
|
||||
@@ -720,20 +716,19 @@ func UpdateRecordSet(db *gorm.DB) http.HandlerFunc {
|
||||
|
||||
// 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
|
||||
// @ID DeleteRecordSet
|
||||
// @Summary Delete a record set (API removes row; worker can optionally handle external deletion policy)
|
||||
// @Tags DNS
|
||||
// @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())
|
||||
|
||||
@@ -7,28 +7,52 @@ import (
|
||||
)
|
||||
|
||||
type ClusterResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
Region string `json:"region"`
|
||||
Status string `json:"status"`
|
||||
CaptainDomain string `json:"captain_domain"`
|
||||
ClusterLoadBalancer string `json:"cluster_load_balancer"`
|
||||
RandomToken string `json:"random_token"`
|
||||
CertificateKey string `json:"certificate_key"`
|
||||
ControlLoadBalancer string `json:"control_load_balancer"`
|
||||
NodePools []NodePoolResponse `json:"node_pools,omitempty"`
|
||||
BastionServer *ServerResponse `json:"bastion_server,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CaptainDomain *DomainResponse `json:"captain_domain,omitempty"`
|
||||
ControlPlaneRecordSet *RecordSetResponse `json:"control_plane_record_set,omitempty"`
|
||||
AppsLoadBalancer *LoadBalancerResponse `json:"apps_load_balancer,omitempty"`
|
||||
GlueOpsLoadBalancer *LoadBalancerResponse `json:"glueops_load_balancer,omitempty"`
|
||||
BastionServer *ServerResponse `json:"bastion_server,omitempty"`
|
||||
Provider string `json:"provider"`
|
||||
Region string `json:"region"`
|
||||
Status string `json:"status"`
|
||||
LastError string `json:"last_error"`
|
||||
RandomToken string `json:"random_token"`
|
||||
CertificateKey string `json:"certificate_key"`
|
||||
NodePools []NodePoolResponse `json:"node_pools,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CreateClusterRequest struct {
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
Region string `json:"region"`
|
||||
Status string `json:"status"`
|
||||
CaptainDomain string `json:"captain_domain"`
|
||||
ClusterLoadBalancer *string `json:"cluster_load_balancer"`
|
||||
ControlLoadBalancer *string `json:"control_load_balancer"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
Region string `json:"region"`
|
||||
}
|
||||
|
||||
type UpdateClusterRequest struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Provider *string `json:"provider,omitempty"`
|
||||
Region *string `json:"region,omitempty"`
|
||||
}
|
||||
|
||||
type AttachCaptainDomainRequest struct {
|
||||
DomainID uuid.UUID `json:"domain_id"`
|
||||
}
|
||||
|
||||
type AttachRecordSetRequest struct {
|
||||
RecordSetID uuid.UUID `json:"record_set_id"`
|
||||
}
|
||||
|
||||
type AttachLoadBalancerRequest struct {
|
||||
LoadBalancerID uuid.UUID `json:"load_balancer_id"`
|
||||
}
|
||||
|
||||
type AttachBastionRequest struct {
|
||||
ServerID uuid.UUID `json:"server_id"`
|
||||
}
|
||||
|
||||
type SetKubeconfigRequest struct {
|
||||
Kubeconfig string `json:"kubeconfig"`
|
||||
}
|
||||
|
||||
32
internal/handlers/dto/load_balancers.go
Normal file
32
internal/handlers/dto/load_balancers.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type LoadBalancerResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind" enums:"glueops|public"`
|
||||
PublicIPAddress string `json:"public_ip_address"`
|
||||
PrivateIPAddress string `json:"private_ip_address"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CreateLoadBalancerRequest struct {
|
||||
Name string `json:"name" example:"glueops"`
|
||||
Kind string `json:"kind" example:"public" enums:"glueops|public"`
|
||||
PublicIPAddress string `json:"public_ip_address" example:"8.8.8.8"`
|
||||
PrivateIPAddress string `json:"private_ip_address" example:"192.168.0.2"`
|
||||
}
|
||||
|
||||
type UpdateLoadBalancerRequest struct {
|
||||
Name *string `json:"name" example:"glue"`
|
||||
Kind *string `json:"kind" example:"public" enums:"glueops|public"`
|
||||
PublicIPAddress *string `json:"public_ip_address" example:"8.8.8.8"`
|
||||
PrivateIPAddress *string `json:"private_ip_address" example:"192.168.0.2"`
|
||||
}
|
||||
@@ -16,7 +16,6 @@ type HealthStatus struct {
|
||||
// @Description Returns 200 OK when the service is up
|
||||
// @Tags Health
|
||||
// @ID HealthCheck // operationId
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} HealthStatus
|
||||
// @Router /healthz [get]
|
||||
|
||||
@@ -25,7 +25,6 @@ import (
|
||||
// @Summary List Archer jobs (admin)
|
||||
// @Description Paginated background jobs with optional filters. Search `q` may match id, type, error, payload (implementation-dependent).
|
||||
// @Tags ArcherAdmin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param status query string false "Filter by status" Enums(queued,running,succeeded,failed,canceled,retrying,scheduled)
|
||||
// @Param queue query string false "Filter by queue name / worker name"
|
||||
@@ -283,7 +282,6 @@ func AdminCancelArcherJob(db *gorm.DB) http.HandlerFunc {
|
||||
// @Summary List Archer queues (admin)
|
||||
// @Description Summary metrics per queue (pending, running, failed, scheduled).
|
||||
// @Tags ArcherAdmin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} dto.QueueInfo
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
// @Summary List node labels (org scoped)
|
||||
// @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.
|
||||
// @Tags Labels
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param key query string false "Exact key"
|
||||
@@ -74,7 +73,6 @@ func ListLabels(db *gorm.DB) http.HandlerFunc {
|
||||
// @Summary Get label by ID (org scoped)
|
||||
// @Description Returns one label.
|
||||
// @Tags Labels
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Label ID (UUID)"
|
||||
@@ -253,11 +251,10 @@ func UpdateLabel(db *gorm.DB) http.HandlerFunc {
|
||||
// @Summary Delete label (org scoped)
|
||||
// @Description Permanently deletes the label.
|
||||
// @Tags Labels
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Label ID (UUID)"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Label ID (UUID)"
|
||||
// @Success 204 "No Content"
|
||||
// @Failure 400 {string} string "invalid id"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
|
||||
283
internal/handlers/load_balancers.go
Normal file
283
internal/handlers/load_balancers.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"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"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// ListLoadBalancers godoc
|
||||
//
|
||||
// @ID ListLoadBalancers
|
||||
// @Summary List load balancers (org scoped)
|
||||
// @Description Returns load balancers for the organization in X-Org-ID.
|
||||
// @Tags LoadBalancers
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Success 200 {array} dto.LoadBalancerResponse
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "failed to list clusters"
|
||||
// @Router /load-balancers [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func ListLoadBalancers(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 rows []models.LoadBalancer
|
||||
if err := db.Where("organization_id = ?", orgID).Find(&rows).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
out := make([]dto.LoadBalancerResponse, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, loadBalancerOut(&row))
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusOK, out)
|
||||
}
|
||||
}
|
||||
|
||||
// GetLoadBalancer godoc
|
||||
//
|
||||
// @ID GetLoadBalancers
|
||||
// @Summary Get a load balancer (org scoped)
|
||||
// @Description Returns load balancer for the organization in X-Org-ID.
|
||||
// @Tags LoadBalancers
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "LoadBalancer ID (UUID)"
|
||||
// @Success 200 {array} dto.LoadBalancerResponse
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "failed to list clusters"
|
||||
// @Router /load-balancers/{id} [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func GetLoadBalancer(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.LoadBalancer
|
||||
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&row).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
utils.WriteError(w, http.StatusNotFound, "not_found", "load balancer not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||
return
|
||||
}
|
||||
out := loadBalancerOut(&row)
|
||||
utils.WriteJSON(w, http.StatusOK, out)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateLoadBalancer godoc
|
||||
//
|
||||
// @ID CreateLoadBalancer
|
||||
// @Summary Create a load balancer
|
||||
// @Tags LoadBalancers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param body body dto.CreateLoadBalancerRequest true "Record set payload"
|
||||
// @Success 201 {object} dto.LoadBalancerResponse
|
||||
// @Failure 400 {string} string "validation error"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "domain not found"
|
||||
// @Router /load-balancers [post]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func CreateLoadBalancer(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.CreateLoadBalancerRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if strings.ToLower(in.Kind) != "glueops" || strings.ToLower(in.Kind) != "public" {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_kind", "invalid kind only 'glueops' or 'public'")
|
||||
return
|
||||
}
|
||||
|
||||
row := &models.LoadBalancer{
|
||||
OrganizationID: orgID,
|
||||
Name: in.Name,
|
||||
Kind: strings.ToLower(in.Kind),
|
||||
PublicIPAddress: in.PublicIPAddress,
|
||||
PrivateIPAddress: in.PrivateIPAddress,
|
||||
}
|
||||
if err := db.Create(row).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusCreated, loadBalancerOut(row))
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateLoadBalancer godoc
|
||||
//
|
||||
// @ID UpdateLoadBalancer
|
||||
// @Summary Update a load balancer (org scoped)
|
||||
// @Tags LoadBalancers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Load Balancer ID (UUID)"
|
||||
// @Param body body dto.UpdateLoadBalancerRequest true "Fields to update"
|
||||
// @Success 200 {object} dto.LoadBalancerResponse
|
||||
// @Failure 400 {string} string "validation error"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /load-balancers/{id} [patch]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func UpdateLoadBalancer(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
|
||||
}
|
||||
|
||||
row := &models.LoadBalancer{}
|
||||
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", "load balancer not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var in dto.UpdateLoadBalancerRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||
return
|
||||
}
|
||||
if in.Name != nil {
|
||||
row.Name = *in.Name
|
||||
}
|
||||
if in.Kind != nil {
|
||||
if strings.ToLower(*in.Kind) != "glueops" || strings.ToLower(*in.Kind) != "public" {
|
||||
utils.WriteError(w, http.StatusBadRequest, "bad_kind", "invalid kind only 'glueops' or 'public'")
|
||||
return
|
||||
}
|
||||
row.Kind = strings.ToLower(*in.Kind)
|
||||
}
|
||||
if in.PublicIPAddress != nil {
|
||||
row.PublicIPAddress = *in.PublicIPAddress
|
||||
}
|
||||
if in.PrivateIPAddress != nil {
|
||||
row.PrivateIPAddress = *in.PrivateIPAddress
|
||||
}
|
||||
if err := db.Save(row).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
utils.WriteJSON(w, http.StatusOK, loadBalancerOut(row))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteLoadBalancer godoc
|
||||
//
|
||||
// @ID DeleteLoadBalancer
|
||||
// @Summary Delete a load balancer
|
||||
// @Tags LoadBalancers
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Load Balancer ID (UUID)"
|
||||
// @Success 204
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Router /load-balancers/{id} [delete]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
// @Security OrgSecretAuth
|
||||
func DeleteLoadBalancer(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
|
||||
}
|
||||
|
||||
row := &models.LoadBalancer{}
|
||||
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", "load balancer not found")
|
||||
return
|
||||
}
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Delete(row).Error; err != nil {
|
||||
utils.WriteError(w, http.StatusInternalServerError, "db_error", err.Error())
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Out mappers ----------
|
||||
|
||||
func loadBalancerOut(m *models.LoadBalancer) dto.LoadBalancerResponse {
|
||||
return dto.LoadBalancerResponse{
|
||||
ID: m.ID,
|
||||
OrganizationID: m.OrganizationID,
|
||||
Name: m.Name,
|
||||
Kind: m.Kind,
|
||||
PublicIPAddress: m.PublicIPAddress,
|
||||
PrivateIPAddress: m.PrivateIPAddress,
|
||||
CreatedAt: m.CreatedAt.UTC(),
|
||||
UpdatedAt: m.UpdatedAt.UTC(),
|
||||
}
|
||||
}
|
||||
@@ -25,14 +25,13 @@ import (
|
||||
// @Summary List node pools (org scoped)
|
||||
// @Description Returns node pools for the organization in X-Org-ID.
|
||||
// @Tags NodePools
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param q query string false "Name contains (case-insensitive)"
|
||||
// @Success 200 {array} dto.NodePoolResponse
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "failed to list node pools"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param q query string false "Name contains (case-insensitive)"
|
||||
// @Success 200 {array} dto.NodePoolResponse
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "failed to list node pools"
|
||||
// @Router /node-pools [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
@@ -145,16 +144,15 @@ func ListNodePools(db *gorm.DB) http.HandlerFunc {
|
||||
// @Summary Get node pool by ID (org scoped)
|
||||
// @Description Returns one node pool. Add `include=servers` to include servers.
|
||||
// @Tags NodePools
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Success 200 {object} dto.NodePoolResponse
|
||||
// @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 "fetch failed"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Success 200 {object} dto.NodePoolResponse
|
||||
// @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 "fetch failed"
|
||||
// @Router /node-pools/{id} [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
@@ -194,13 +192,13 @@ func GetNodePool(db *gorm.DB) http.HandlerFunc {
|
||||
// @Tags NodePools
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param body body dto.CreateNodePoolRequest true "NodePool payload"
|
||||
// @Success 201 {object} dto.NodePoolResponse
|
||||
// @Failure 400 {string} string "invalid json / missing fields / invalid server_ids"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "create failed"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param body body dto.CreateNodePoolRequest true "NodePool payload"
|
||||
// @Success 201 {object} dto.NodePoolResponse
|
||||
// @Failure 400 {string} string "invalid json / missing fields / invalid server_ids"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "create failed"
|
||||
// @Router /node-pools [post]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
@@ -257,15 +255,15 @@ func CreateNodePool(db *gorm.DB) http.HandlerFunc {
|
||||
// @Tags NodePools
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Param body body dto.UpdateNodePoolRequest true "Fields to update"
|
||||
// @Success 200 {object} dto.NodePoolResponse
|
||||
// @Failure 400 {string} string "invalid id / invalid json"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "update failed"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Param body body dto.UpdateNodePoolRequest true "Fields to update"
|
||||
// @Success 200 {object} dto.NodePoolResponse
|
||||
// @Failure 400 {string} string "invalid id / invalid json"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "update failed"
|
||||
// @Router /node-pools/{id} [patch]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
@@ -327,15 +325,14 @@ func UpdateNodePool(db *gorm.DB) http.HandlerFunc {
|
||||
// @Summary Delete node pool (org scoped)
|
||||
// @Description Permanently deletes the node pool.
|
||||
// @Tags NodePools
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Failure 400 {string} string "invalid id"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "delete failed"
|
||||
// @Success 204 "No Content"
|
||||
// @Failure 400 {string} string "invalid id"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 500 {string} string "delete failed"
|
||||
// @Router /node-pools/{id} [delete]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
@@ -369,16 +366,15 @@ func DeleteNodePool(db *gorm.DB) http.HandlerFunc {
|
||||
// @ID ListNodePoolServers
|
||||
// @Summary List servers attached to a node pool (org scoped)
|
||||
// @Tags NodePools
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Success 200 {array} 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 "fetch failed"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Success 200 {array} 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 "fetch failed"
|
||||
// @Router /node-pools/{id}/servers [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
@@ -434,15 +430,15 @@ func ListNodePoolServers(db *gorm.DB) http.HandlerFunc {
|
||||
// @Tags NodePools
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Param body body dto.AttachServersRequest true "Server IDs to attach"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Failure 400 {string} string "invalid id / invalid server_ids"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "attach failed"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Param body body dto.AttachServersRequest true "Server IDs to attach"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Failure 400 {string} string "invalid id / invalid server_ids"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "attach failed"
|
||||
// @Router /node-pools/{id}/servers [post]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
@@ -521,17 +517,16 @@ func AttachNodePoolServers(db *gorm.DB) http.HandlerFunc {
|
||||
// @ID DetachNodePoolServer
|
||||
// @Summary Detach one server from a node pool (org scoped)
|
||||
// @Tags NodePools
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Param serverId path string true "Server ID (UUID)"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @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 "detach failed"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Param serverId path string true "Server ID (UUID)"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @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 "detach failed"
|
||||
// @Router /node-pools/{id}/servers/{serverId} [delete]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
@@ -588,16 +583,15 @@ func DetachNodePoolServer(db *gorm.DB) http.HandlerFunc {
|
||||
// @ID ListNodePoolTaints
|
||||
// @Summary List taints attached to a node pool (org scoped)
|
||||
// @Tags NodePools
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Success 200 {array} dto.TaintResponse
|
||||
// @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 "fetch failed"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Success 200 {array} dto.TaintResponse
|
||||
// @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 "fetch failed"
|
||||
// @Router /node-pools/{id}/taints [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
@@ -646,15 +640,15 @@ func ListNodePoolTaints(db *gorm.DB) http.HandlerFunc {
|
||||
// @Tags NodePools
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Param body body dto.AttachTaintsRequest true "Taint IDs to attach"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Failure 400 {string} string "invalid id / invalid taint_ids"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "attach failed"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Param body body dto.AttachTaintsRequest true "Taint IDs to attach"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Failure 400 {string} string "invalid id / invalid taint_ids"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "attach failed"
|
||||
// @Router /node-pools/{id}/taints [post]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
@@ -730,17 +724,16 @@ func AttachNodePoolTaints(db *gorm.DB) http.HandlerFunc {
|
||||
// @ID DetachNodePoolTaint
|
||||
// @Summary Detach one taint from a node pool (org scoped)
|
||||
// @Tags NodePools
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Param taintId path string true "Taint ID (UUID)"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @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 "detach failed"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Param taintId path string true "Taint ID (UUID)"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @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 "detach failed"
|
||||
// @Router /node-pools/{id}/taints/{taintId} [delete]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
@@ -798,16 +791,15 @@ func DetachNodePoolTaint(db *gorm.DB) http.HandlerFunc {
|
||||
// @ID ListNodePoolLabels
|
||||
// @Summary List labels attached to a node pool (org scoped)
|
||||
// @Tags NodePools
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Label Pool ID (UUID)"
|
||||
// @Success 200 {array} dto.LabelResponse
|
||||
// @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 "fetch failed"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Label Pool ID (UUID)"
|
||||
// @Success 200 {array} dto.LabelResponse
|
||||
// @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 "fetch failed"
|
||||
// @Router /node-pools/{id}/labels [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
@@ -859,15 +851,15 @@ func ListNodePoolLabels(db *gorm.DB) http.HandlerFunc {
|
||||
// @Tags NodePools
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Param body body dto.AttachLabelsRequest true "Label IDs to attach"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Failure 400 {string} string "invalid id / invalid server_ids"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "attach failed"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Param body body dto.AttachLabelsRequest true "Label IDs to attach"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Failure 400 {string} string "invalid id / invalid server_ids"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "attach failed"
|
||||
// @Router /node-pools/{id}/labels [post]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
@@ -940,17 +932,16 @@ func AttachNodePoolLabels(db *gorm.DB) http.HandlerFunc {
|
||||
// @ID DetachNodePoolLabel
|
||||
// @Summary Detach one label from a node pool (org scoped)
|
||||
// @Tags NodePools
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Param labelId path string true "Label ID (UUID)"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @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 "detach failed"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Param labelId path string true "Label ID (UUID)"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @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 "detach failed"
|
||||
// @Router /node-pools/{id}/labels/{labelId} [delete]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
@@ -1008,16 +999,15 @@ func DetachNodePoolLabel(db *gorm.DB) http.HandlerFunc {
|
||||
// @ID ListNodePoolAnnotations
|
||||
// @Summary List annotations attached to a node pool (org scoped)
|
||||
// @Tags NodePools
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Success 200 {array} dto.AnnotationResponse
|
||||
// @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 "fetch failed"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Success 200 {array} dto.AnnotationResponse
|
||||
// @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 "fetch failed"
|
||||
// @Router /node-pools/{id}/annotations [get]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
@@ -1069,15 +1059,15 @@ func ListNodePoolAnnotations(db *gorm.DB) http.HandlerFunc {
|
||||
// @Tags NodePools
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Group ID (UUID)"
|
||||
// @Param body body dto.AttachAnnotationsRequest true "Annotation IDs to attach"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Failure 400 {string} string "invalid id / invalid server_ids"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "attach failed"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Group ID (UUID)"
|
||||
// @Param body body dto.AttachAnnotationsRequest true "Annotation IDs to attach"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Failure 400 {string} string "invalid id / invalid server_ids"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
// @Failure 404 {string} string "not found"
|
||||
// @Failure 500 {string} string "attach failed"
|
||||
// @Router /node-pools/{id}/annotations [post]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
@@ -1151,17 +1141,16 @@ func AttachNodePoolAnnotations(db *gorm.DB) http.HandlerFunc {
|
||||
// @ID DetachNodePoolAnnotation
|
||||
// @Summary Detach one annotation from a node pool (org scoped)
|
||||
// @Tags NodePools
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Param annotationId path string true "Annotation ID (UUID)"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @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 "detach failed"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Pool ID (UUID)"
|
||||
// @Param annotationId path string true "Annotation ID (UUID)"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @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 "detach failed"
|
||||
// @Router /node-pools/{id}/annotations/{annotationId} [delete]
|
||||
// @Security BearerAuth
|
||||
// @Security OrgKeyAuth
|
||||
|
||||
381
internal/handlers/node_pools_test.go
Normal file
381
internal/handlers/node_pools_test.go
Normal file
@@ -0,0 +1,381 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/glueops/autoglue/internal/common"
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"github.com/glueops/autoglue/internal/testutil/pgtest"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
code := m.Run()
|
||||
pgtest.Stop()
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestParseUUIDs_Success(t *testing.T) {
|
||||
u1 := uuid.New()
|
||||
u2 := uuid.New()
|
||||
|
||||
got, err := parseUUIDs([]string{u1.String(), u2.String()})
|
||||
if err != nil {
|
||||
t.Fatalf("parseUUIDs returned error: %v", err)
|
||||
}
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("expected 2 UUIDs, got %d", len(got))
|
||||
}
|
||||
if got[0] != u1 || got[1] != u2 {
|
||||
t.Fatalf("unexpected UUIDs: got=%v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseUUIDs_Invalid(t *testing.T) {
|
||||
_, err := parseUUIDs([]string{"not-a-uuid"})
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for invalid UUID, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
// --- ensureServersBelongToOrg ---
|
||||
|
||||
func TestEnsureServersBelongToOrg_AllBelong(t *testing.T) {
|
||||
db := pgtest.DB(t)
|
||||
|
||||
org := models.Organization{Name: "org-a"}
|
||||
if err := db.Create(&org).Error; err != nil {
|
||||
t.Fatalf("create org: %v", err)
|
||||
}
|
||||
|
||||
sshKey := createTestSshKey(t, db, org.ID, "org-a-key")
|
||||
|
||||
s1 := models.Server{
|
||||
OrganizationID: org.ID,
|
||||
Hostname: "srv-1",
|
||||
SSHUser: "ubuntu",
|
||||
SshKeyID: sshKey.ID,
|
||||
Role: "worker",
|
||||
Status: "pending",
|
||||
}
|
||||
s2 := models.Server{
|
||||
OrganizationID: org.ID,
|
||||
Hostname: "srv-2",
|
||||
SSHUser: "ubuntu",
|
||||
SshKeyID: sshKey.ID,
|
||||
Role: "worker",
|
||||
Status: "pending",
|
||||
}
|
||||
|
||||
if err := db.Create(&s1).Error; err != nil {
|
||||
t.Fatalf("create server 1: %v", err)
|
||||
}
|
||||
if err := db.Create(&s2).Error; err != nil {
|
||||
t.Fatalf("create server 2: %v", err)
|
||||
}
|
||||
|
||||
ids := []uuid.UUID{s1.ID, s2.ID}
|
||||
if err := ensureServersBelongToOrg(org.ID, ids, db); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureServersBelongToOrg_ForeignOrgFails(t *testing.T) {
|
||||
db := pgtest.DB(t)
|
||||
|
||||
orgA := models.Organization{Name: "org-a"}
|
||||
orgB := models.Organization{Name: "org-b"}
|
||||
|
||||
if err := db.Create(&orgA).Error; err != nil {
|
||||
t.Fatalf("create orgA: %v", err)
|
||||
}
|
||||
if err := db.Create(&orgB).Error; err != nil {
|
||||
t.Fatalf("create orgB: %v", err)
|
||||
}
|
||||
|
||||
sshKeyA := createTestSshKey(t, db, orgA.ID, "org-a-key")
|
||||
sshKeyB := createTestSshKey(t, db, orgB.ID, "org-b-key")
|
||||
|
||||
s1 := models.Server{
|
||||
OrganizationID: orgA.ID,
|
||||
Hostname: "srv-a-1",
|
||||
SSHUser: "ubuntu",
|
||||
SshKeyID: sshKeyA.ID,
|
||||
Role: "worker",
|
||||
Status: "pending",
|
||||
}
|
||||
s2 := models.Server{
|
||||
OrganizationID: orgB.ID,
|
||||
Hostname: "srv-b-1",
|
||||
SSHUser: "ubuntu",
|
||||
SshKeyID: sshKeyB.ID,
|
||||
Role: "worker",
|
||||
Status: "pending",
|
||||
}
|
||||
|
||||
if err := db.Create(&s1).Error; err != nil {
|
||||
t.Fatalf("create server s1: %v", err)
|
||||
}
|
||||
if err := db.Create(&s2).Error; err != nil {
|
||||
t.Fatalf("create server s2: %v", err)
|
||||
}
|
||||
|
||||
ids := []uuid.UUID{s1.ID, s2.ID}
|
||||
if err := ensureServersBelongToOrg(orgA.ID, ids, db); err == nil {
|
||||
t.Fatalf("expected error when one server belongs to a different org")
|
||||
}
|
||||
}
|
||||
|
||||
// --- ensureTaintsBelongToOrg ---
|
||||
|
||||
func TestEnsureTaintsBelongToOrg_AllBelong(t *testing.T) {
|
||||
db := pgtest.DB(t)
|
||||
|
||||
org := models.Organization{Name: "org-taints"}
|
||||
if err := db.Create(&org).Error; err != nil {
|
||||
t.Fatalf("create org: %v", err)
|
||||
}
|
||||
|
||||
t1 := models.Taint{
|
||||
OrganizationID: org.ID,
|
||||
Key: "key1",
|
||||
Value: "val1",
|
||||
Effect: "NoSchedule",
|
||||
}
|
||||
t2 := models.Taint{
|
||||
OrganizationID: org.ID,
|
||||
Key: "key2",
|
||||
Value: "val2",
|
||||
Effect: "PreferNoSchedule",
|
||||
}
|
||||
|
||||
if err := db.Create(&t1).Error; err != nil {
|
||||
t.Fatalf("create taint 1: %v", err)
|
||||
}
|
||||
if err := db.Create(&t2).Error; err != nil {
|
||||
t.Fatalf("create taint 2: %v", err)
|
||||
}
|
||||
|
||||
ids := []uuid.UUID{t1.ID, t2.ID}
|
||||
if err := ensureTaintsBelongToOrg(org.ID, ids, db); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureTaintsBelongToOrg_ForeignOrgFails(t *testing.T) {
|
||||
db := pgtest.DB(t)
|
||||
|
||||
orgA := models.Organization{Name: "org-a"}
|
||||
orgB := models.Organization{Name: "org-b"}
|
||||
if err := db.Create(&orgA).Error; err != nil {
|
||||
t.Fatalf("create orgA: %v", err)
|
||||
}
|
||||
if err := db.Create(&orgB).Error; err != nil {
|
||||
t.Fatalf("create orgB: %v", err)
|
||||
}
|
||||
|
||||
t1 := models.Taint{
|
||||
OrganizationID: orgA.ID,
|
||||
Key: "key1",
|
||||
Value: "val1",
|
||||
Effect: "NoSchedule",
|
||||
}
|
||||
t2 := models.Taint{
|
||||
OrganizationID: orgB.ID,
|
||||
Key: "key2",
|
||||
Value: "val2",
|
||||
Effect: "NoSchedule",
|
||||
}
|
||||
|
||||
if err := db.Create(&t1).Error; err != nil {
|
||||
t.Fatalf("create taint 1: %v", err)
|
||||
}
|
||||
if err := db.Create(&t2).Error; err != nil {
|
||||
t.Fatalf("create taint 2: %v", err)
|
||||
}
|
||||
|
||||
ids := []uuid.UUID{t1.ID, t2.ID}
|
||||
if err := ensureTaintsBelongToOrg(orgA.ID, ids, db); err == nil {
|
||||
t.Fatalf("expected error when a taint belongs to another org")
|
||||
}
|
||||
}
|
||||
|
||||
// --- ensureLabelsBelongToOrg ---
|
||||
|
||||
func TestEnsureLabelsBelongToOrg_AllBelong(t *testing.T) {
|
||||
db := pgtest.DB(t)
|
||||
|
||||
org := models.Organization{Name: "org-labels"}
|
||||
if err := db.Create(&org).Error; err != nil {
|
||||
t.Fatalf("create org: %v", err)
|
||||
}
|
||||
|
||||
l1 := models.Label{
|
||||
AuditFields: common.AuditFields{
|
||||
OrganizationID: org.ID,
|
||||
},
|
||||
Key: "env",
|
||||
Value: "dev",
|
||||
}
|
||||
l2 := models.Label{
|
||||
AuditFields: common.AuditFields{
|
||||
OrganizationID: org.ID,
|
||||
},
|
||||
Key: "env",
|
||||
Value: "prod",
|
||||
}
|
||||
|
||||
if err := db.Create(&l1).Error; err != nil {
|
||||
t.Fatalf("create label 1: %v", err)
|
||||
}
|
||||
if err := db.Create(&l2).Error; err != nil {
|
||||
t.Fatalf("create label 2: %v", err)
|
||||
}
|
||||
|
||||
ids := []uuid.UUID{l1.ID, l2.ID}
|
||||
if err := ensureLabelsBelongToOrg(org.ID, ids, db); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureLabelsBelongToOrg_ForeignOrgFails(t *testing.T) {
|
||||
db := pgtest.DB(t)
|
||||
|
||||
orgA := models.Organization{Name: "org-a"}
|
||||
orgB := models.Organization{Name: "org-b"}
|
||||
if err := db.Create(&orgA).Error; err != nil {
|
||||
t.Fatalf("create orgA: %v", err)
|
||||
}
|
||||
if err := db.Create(&orgB).Error; err != nil {
|
||||
t.Fatalf("create orgB: %v", err)
|
||||
}
|
||||
|
||||
l1 := models.Label{
|
||||
AuditFields: common.AuditFields{
|
||||
OrganizationID: orgA.ID,
|
||||
},
|
||||
Key: "env",
|
||||
Value: "dev",
|
||||
}
|
||||
l2 := models.Label{
|
||||
AuditFields: common.AuditFields{
|
||||
OrganizationID: orgB.ID,
|
||||
},
|
||||
Key: "env",
|
||||
Value: "prod",
|
||||
}
|
||||
|
||||
if err := db.Create(&l1).Error; err != nil {
|
||||
t.Fatalf("create label 1: %v", err)
|
||||
}
|
||||
if err := db.Create(&l2).Error; err != nil {
|
||||
t.Fatalf("create label 2: %v", err)
|
||||
}
|
||||
|
||||
ids := []uuid.UUID{l1.ID, l2.ID}
|
||||
if err := ensureLabelsBelongToOrg(orgA.ID, ids, db); err == nil {
|
||||
t.Fatalf("expected error when a label belongs to another org")
|
||||
}
|
||||
}
|
||||
|
||||
// --- ensureAnnotaionsBelongToOrg (typo in original name is preserved) ---
|
||||
|
||||
func TestEnsureAnnotationsBelongToOrg_AllBelong(t *testing.T) {
|
||||
db := pgtest.DB(t)
|
||||
|
||||
org := models.Organization{Name: "org-annotations"}
|
||||
if err := db.Create(&org).Error; err != nil {
|
||||
t.Fatalf("create org: %v", err)
|
||||
}
|
||||
|
||||
a1 := models.Annotation{
|
||||
AuditFields: common.AuditFields{
|
||||
OrganizationID: org.ID,
|
||||
},
|
||||
Key: "team",
|
||||
Value: "core",
|
||||
}
|
||||
a2 := models.Annotation{
|
||||
AuditFields: common.AuditFields{
|
||||
OrganizationID: org.ID,
|
||||
},
|
||||
Key: "team",
|
||||
Value: "platform",
|
||||
}
|
||||
|
||||
if err := db.Create(&a1).Error; err != nil {
|
||||
t.Fatalf("create annotation 1: %v", err)
|
||||
}
|
||||
if err := db.Create(&a2).Error; err != nil {
|
||||
t.Fatalf("create annotation 2: %v", err)
|
||||
}
|
||||
|
||||
ids := []uuid.UUID{a1.ID, a2.ID}
|
||||
if err := ensureAnnotaionsBelongToOrg(org.ID, ids, db); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureAnnotationsBelongToOrg_ForeignOrgFails(t *testing.T) {
|
||||
db := pgtest.DB(t)
|
||||
|
||||
orgA := models.Organization{Name: "org-a"}
|
||||
orgB := models.Organization{Name: "org-b"}
|
||||
if err := db.Create(&orgA).Error; err != nil {
|
||||
t.Fatalf("create orgA: %v", err)
|
||||
}
|
||||
if err := db.Create(&orgB).Error; err != nil {
|
||||
t.Fatalf("create orgB: %v", err)
|
||||
}
|
||||
|
||||
a1 := models.Annotation{
|
||||
AuditFields: common.AuditFields{
|
||||
OrganizationID: orgA.ID,
|
||||
},
|
||||
Key: "team",
|
||||
Value: "core",
|
||||
}
|
||||
a2 := models.Annotation{
|
||||
AuditFields: common.AuditFields{
|
||||
OrganizationID: orgB.ID,
|
||||
},
|
||||
Key: "team",
|
||||
Value: "platform",
|
||||
}
|
||||
|
||||
if err := db.Create(&a1).Error; err != nil {
|
||||
t.Fatalf("create annotation 1: %v", err)
|
||||
}
|
||||
if err := db.Create(&a2).Error; err != nil {
|
||||
t.Fatalf("create annotation 2: %v", err)
|
||||
}
|
||||
|
||||
ids := []uuid.UUID{a1.ID, a2.ID}
|
||||
if err := ensureAnnotaionsBelongToOrg(orgA.ID, ids, db); err == nil {
|
||||
t.Fatalf("expected error when an annotation belongs to another org")
|
||||
}
|
||||
}
|
||||
|
||||
func createTestSshKey(t *testing.T, db *gorm.DB, orgID uuid.UUID, name string) models.SshKey {
|
||||
t.Helper()
|
||||
|
||||
key := models.SshKey{
|
||||
AuditFields: common.AuditFields{
|
||||
OrganizationID: orgID,
|
||||
},
|
||||
Name: name,
|
||||
PublicKey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestKey",
|
||||
EncryptedPrivateKey: "encrypted",
|
||||
PrivateIV: "iv",
|
||||
PrivateTag: "tag",
|
||||
Fingerprint: "fp-" + name,
|
||||
}
|
||||
|
||||
if err := db.Create(&key).Error; err != nil {
|
||||
t.Fatalf("create ssh key %s: %v", name, err)
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
// @Summary List servers (org scoped)
|
||||
// @Description Returns servers for the organization in X-Org-ID. Optional filters: status, role.
|
||||
// @Tags Servers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param status query string false "Filter by status (pending|provisioning|ready|failed)"
|
||||
@@ -89,7 +88,6 @@ func ListServers(db *gorm.DB) http.HandlerFunc {
|
||||
// @Summary Get server by ID (org scoped)
|
||||
// @Description Returns one server in the given organization.
|
||||
// @Tags Servers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Server ID (UUID)"
|
||||
@@ -329,11 +327,10 @@ func UpdateServer(db *gorm.DB) http.HandlerFunc {
|
||||
// @Summary Delete server (org scoped)
|
||||
// @Description Permanently deletes the server.
|
||||
// @Tags Servers
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Server ID (UUID)"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Server ID (UUID)"
|
||||
// @Success 204 "No Content"
|
||||
// @Failure 400 {string} string "invalid id"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
|
||||
78
internal/handlers/servers_test.go
Normal file
78
internal/handlers/servers_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"github.com/glueops/autoglue/internal/testutil/pgtest"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestValidStatus(t *testing.T) {
|
||||
// known-good statuses from servers.go
|
||||
valid := []string{"pending", "provisioning", "ready", "failed"}
|
||||
for _, s := range valid {
|
||||
if !validStatus(s) {
|
||||
t.Errorf("expected validStatus(%q) = true, got false", s)
|
||||
}
|
||||
}
|
||||
|
||||
invalid := []string{"foobar", "unknown"}
|
||||
for _, s := range invalid {
|
||||
if validStatus(s) {
|
||||
t.Errorf("expected validStatus(%q) = false, got true", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureKeyBelongsToOrg_Success(t *testing.T) {
|
||||
db := pgtest.DB(t)
|
||||
|
||||
org := models.Organization{Name: "servers-org"}
|
||||
if err := db.Create(&org).Error; err != nil {
|
||||
t.Fatalf("create org: %v", err)
|
||||
}
|
||||
|
||||
key := createTestSshKey(t, db, org.ID, "org-key")
|
||||
|
||||
if err := ensureKeyBelongsToOrg(org.ID, key.ID, db); err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureKeyBelongsToOrg_WrongOrg(t *testing.T) {
|
||||
db := pgtest.DB(t)
|
||||
|
||||
orgA := models.Organization{Name: "org-a"}
|
||||
orgB := models.Organization{Name: "org-b"}
|
||||
|
||||
if err := db.Create(&orgA).Error; err != nil {
|
||||
t.Fatalf("create orgA: %v", err)
|
||||
}
|
||||
if err := db.Create(&orgB).Error; err != nil {
|
||||
t.Fatalf("create orgB: %v", err)
|
||||
}
|
||||
|
||||
keyA := createTestSshKey(t, db, orgA.ID, "org-a-key")
|
||||
|
||||
// ask for orgB with a key that belongs to orgA → should fail
|
||||
if err := ensureKeyBelongsToOrg(orgB.ID, keyA.ID, db); err == nil {
|
||||
t.Fatalf("expected error when ssh key belongs to a different org, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureKeyBelongsToOrg_NotFound(t *testing.T) {
|
||||
db := pgtest.DB(t)
|
||||
|
||||
org := models.Organization{Name: "org-nokey"}
|
||||
if err := db.Create(&org).Error; err != nil {
|
||||
t.Fatalf("create org: %v", err)
|
||||
}
|
||||
|
||||
// random keyID that doesn't exist
|
||||
randomKeyID := uuid.New()
|
||||
|
||||
if err := ensureKeyBelongsToOrg(org.ID, randomKeyID, db); err == nil {
|
||||
t.Fatalf("expected error when ssh key does not exist, got nil")
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import (
|
||||
// @Summary List ssh keys (org scoped)
|
||||
// @Description Returns ssh keys for the organization in X-Org-ID.
|
||||
// @Tags Ssh
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Success 200 {array} dto.SshResponse
|
||||
@@ -189,7 +188,6 @@ func CreateSSHKey(db *gorm.DB) http.HandlerFunc {
|
||||
// @Summary Get ssh key by ID (org scoped)
|
||||
// @Description Returns public key fields. Append `?reveal=true` to include the private key PEM.
|
||||
// @Tags Ssh
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "SSH Key ID (UUID)"
|
||||
@@ -283,11 +281,10 @@ func GetSSHKey(db *gorm.DB) http.HandlerFunc {
|
||||
// @Summary Delete ssh keypair (org scoped)
|
||||
// @Description Permanently deletes a keypair.
|
||||
// @Tags Ssh
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "SSH Key ID (UUID)"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "SSH Key ID (UUID)"
|
||||
// @Success 204 "No Content"
|
||||
// @Failure 400 {string} string "invalid id"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
// @Summary List node pool taints (org scoped)
|
||||
// @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.
|
||||
// @Tags Taints
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param key query string false "Exact key"
|
||||
@@ -70,7 +69,6 @@ func ListTaints(db *gorm.DB) http.HandlerFunc {
|
||||
// @ID GetTaint
|
||||
// @Summary Get node taint by ID (org scoped)
|
||||
// @Tags Taints
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Taint ID (UUID)"
|
||||
@@ -279,11 +277,10 @@ func UpdateTaint(db *gorm.DB) http.HandlerFunc {
|
||||
// @Summary Delete taint (org scoped)
|
||||
// @Description Permanently deletes the taint.
|
||||
// @Tags Taints
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Taint ID (UUID)"
|
||||
// @Success 204 {string} string "No Content"
|
||||
// @Param X-Org-ID header string false "Organization UUID"
|
||||
// @Param id path string true "Node Taint ID (UUID)"
|
||||
// @Success 204 "No Content"
|
||||
// @Failure 400 {string} string "invalid id"
|
||||
// @Failure 401 {string} string "Unauthorized"
|
||||
// @Failure 403 {string} string "organization required"
|
||||
|
||||
@@ -30,7 +30,6 @@ type VersionResponse struct {
|
||||
// @Description Returns build/runtime metadata for the running service.
|
||||
// @Tags Meta
|
||||
// @ID Version // operationId
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} VersionResponse
|
||||
// @Router /version [get]
|
||||
|
||||
@@ -6,28 +6,47 @@ import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const (
|
||||
ClusterStatusPrePending = "pre_pending" // needs validation
|
||||
ClusterStatusIncomplete = "incomplete" // invalid/missing shape
|
||||
ClusterStatusPending = "pending" // valid shape, waiting for provisioning
|
||||
ClusterStatusProvisioning = "provisioning"
|
||||
ClusterStatusReady = "ready"
|
||||
ClusterStatusFailed = "failed" // provisioning/runtime failure
|
||||
)
|
||||
|
||||
type Cluster struct {
|
||||
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
|
||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"`
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
Region string `json:"region"`
|
||||
Status string `json:"status"`
|
||||
CaptainDomain string `gorm:"not null" json:"captain_domain"` // nonprod.earth.onglueops.rocks
|
||||
AppsLoadBalancer string `json:"cluster_load_balancer"` // {public_ip: 1.2.3.4, private_ip: 10.0.30.1, name: apps.CaqptainDomain}
|
||||
GlueOpsLoadBalancer string `json:"control_load_balancer"` // {public_ip: 5.6.7.8, private_ip: 10.0.22.1, name: CaptainDomain}
|
||||
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
|
||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"`
|
||||
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
||||
|
||||
ControlPlane string `json:"control_plane"` // <- dns cntlpn
|
||||
Name string `gorm:"not null" json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
Region string `json:"region"`
|
||||
|
||||
RandomToken string `json:"random_token"`
|
||||
CertificateKey string `json:"certificate_key"`
|
||||
EncryptedKubeconfig string `gorm:"type:text" json:"-"`
|
||||
KubeIV string `json:"-"`
|
||||
KubeTag string `json:"-"`
|
||||
NodePools []NodePool `gorm:"many2many:cluster_node_pools;constraint:OnDelete:CASCADE" json:"node_pools,omitempty"`
|
||||
BastionServerID *uuid.UUID `gorm:"type:uuid" json:"bastion_server_id,omitempty"`
|
||||
BastionServer *Server `gorm:"foreignKey:BastionServerID" json:"bastion_server,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()"`
|
||||
Status string `gorm:"type:varchar(20);not null;default:'pre_pending'" json:"status"`
|
||||
LastError string `gorm:"type:text;not null;default:''" json:"last_error"`
|
||||
|
||||
CaptainDomainID *uuid.UUID `gorm:"type:uuid" json:"captain_domain_id"`
|
||||
CaptainDomain Domain `gorm:"foreignKey:CaptainDomainID" json:"captain_domain"`
|
||||
ControlPlaneRecordSetID *uuid.UUID `gorm:"type:uuid" json:"control_plane_record_set_id,omitempty"`
|
||||
ControlPlaneRecordSet *RecordSet `gorm:"foreignKey:ControlPlaneRecordSetID" json:"control_plane_record_set,omitempty"`
|
||||
AppsLoadBalancerID *uuid.UUID `gorm:"type:uuid" json:"apps_load_balancer_id,omitempty"`
|
||||
AppsLoadBalancer *LoadBalancer `gorm:"foreignKey:AppsLoadBalancerID" json:"apps_load_balancer,omitempty"`
|
||||
GlueOpsLoadBalancerID *uuid.UUID `gorm:"type:uuid" json:"glueops_load_balancer_id,omitempty"`
|
||||
GlueOpsLoadBalancer *LoadBalancer `gorm:"foreignKey:GlueOpsLoadBalancerID" json:"glueops_load_balancer,omitempty"`
|
||||
BastionServerID *uuid.UUID `gorm:"type:uuid" json:"bastion_server_id,omitempty"`
|
||||
BastionServer *Server `gorm:"foreignKey:BastionServerID" json:"bastion_server,omitempty"`
|
||||
|
||||
NodePools []NodePool `gorm:"many2many:cluster_node_pools;constraint:OnDelete:CASCADE" json:"node_pools,omitempty"`
|
||||
|
||||
RandomToken string `json:"random_token"`
|
||||
CertificateKey string `json:"certificate_key"`
|
||||
|
||||
EncryptedKubeconfig string `gorm:"type:text" json:"-"`
|
||||
KubeIV string `json:"-"`
|
||||
KubeTag string `json:"-"`
|
||||
|
||||
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()"`
|
||||
}
|
||||
|
||||
19
internal/models/load_balancer.go
Normal file
19
internal/models/load_balancer.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type LoadBalancer struct {
|
||||
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;default:gen_random_uuid()"`
|
||||
OrganizationID uuid.UUID `json:"organization_id" gorm:"type:uuid;index"`
|
||||
Organization Organization `json:"organization" gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE"`
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
Kind string `json:"kind" gorm:"not null"`
|
||||
PublicIPAddress string `json:"public_ip_address" gorm:"not null"`
|
||||
PrivateIPAddress string `json:"private_ip_address" gorm:"not null"`
|
||||
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()"`
|
||||
}
|
||||
119
internal/testutil/pgtest/pgtest.go
Normal file
119
internal/testutil/pgtest/pgtest.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package pgtest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
|
||||
"github.com/glueops/autoglue/internal/db"
|
||||
"github.com/glueops/autoglue/internal/models"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
epg *embeddedpostgres.EmbeddedPostgres
|
||||
gdb *gorm.DB
|
||||
initErr error
|
||||
dsn string
|
||||
)
|
||||
|
||||
// initDB is called once via sync.Once. It starts embedded Postgres,
|
||||
// opens a GORM connection and runs the same migrations as NewRuntime.
|
||||
func initDB() {
|
||||
const port uint32 = 55432
|
||||
|
||||
cfg := embeddedpostgres.
|
||||
DefaultConfig().
|
||||
Database("autoglue_test").
|
||||
Username("autoglue").
|
||||
Password("autoglue").
|
||||
Port(port).
|
||||
StartTimeout(30 * time.Second)
|
||||
|
||||
epg = embeddedpostgres.NewDatabase(cfg)
|
||||
if err := epg.Start(); err != nil {
|
||||
initErr = fmt.Errorf("start embedded postgres: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
dsn = fmt.Sprintf(
|
||||
"host=127.0.0.1 port=%d user=%s password=%s dbname=%s sslmode=disable",
|
||||
port,
|
||||
"autoglue",
|
||||
"autoglue",
|
||||
"autoglue_test",
|
||||
)
|
||||
|
||||
dbConn, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
initErr = fmt.Errorf("open gorm: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Use the same model list as app.NewRuntime so schema matches prod
|
||||
if err := db.Run(
|
||||
dbConn,
|
||||
&models.Job{},
|
||||
&models.MasterKey{},
|
||||
&models.SigningKey{},
|
||||
&models.User{},
|
||||
&models.Organization{},
|
||||
&models.Account{},
|
||||
&models.Membership{},
|
||||
&models.APIKey{},
|
||||
&models.UserEmail{},
|
||||
&models.RefreshToken{},
|
||||
&models.OrganizationKey{},
|
||||
&models.SshKey{},
|
||||
&models.Server{},
|
||||
&models.Taint{},
|
||||
&models.Label{},
|
||||
&models.Annotation{},
|
||||
&models.NodePool{},
|
||||
&models.Cluster{},
|
||||
&models.Credential{},
|
||||
&models.Domain{},
|
||||
&models.RecordSet{},
|
||||
); err != nil {
|
||||
initErr = fmt.Errorf("migrate: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
gdb = dbConn
|
||||
}
|
||||
|
||||
// DB returns a lazily-initialized *gorm.DB backed by embedded Postgres.
|
||||
//
|
||||
// Call this from any test that needs a real DB. If init fails, the test
|
||||
// will fail immediately with a clear message.
|
||||
func DB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
once.Do(initDB)
|
||||
if initErr != nil {
|
||||
t.Fatalf("failed to init embedded postgres: %v", initErr)
|
||||
}
|
||||
return gdb
|
||||
}
|
||||
|
||||
// URL returns the DSN for the embedded Postgres instance, useful for code
|
||||
// that expects a DB URL (e.g. bg.NewJobs).
|
||||
func URL(t *testing.T) string {
|
||||
t.Helper()
|
||||
DB(t) // ensure initialized
|
||||
return dsn
|
||||
}
|
||||
|
||||
// Stop stops the embedded Postgres process. Call from TestMain in at
|
||||
// least one package, or let the OS clean it up on process exit.
|
||||
func Stop() {
|
||||
if epg != nil {
|
||||
if err := epg.Stop(); err != nil {
|
||||
log.Printf("stop embedded postgres: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,13 @@ func SPAHandler() (http.Handler, error) {
|
||||
return
|
||||
}
|
||||
|
||||
filePath := strings.TrimPrefix(path.Clean(r.URL.Path), "/")
|
||||
raw := strings.TrimSpace(r.URL.Path)
|
||||
if raw == "" || raw == "/" {
|
||||
raw = "/index.html"
|
||||
}
|
||||
|
||||
clean := path.Clean("/" + raw) // nosemgrep: autoglue.filesystem.no-path-clean
|
||||
filePath := strings.TrimPrefix(clean, "/")
|
||||
if filePath == "" {
|
||||
filePath = "index.html"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user