Files
autoglue/internal/handlers/node_pools.go
2025-12-15 18:04:22 +00:00

1260 lines
41 KiB
Go

package handlers
import (
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"github.com/glueops/autoglue/internal/api/httpmiddleware"
"github.com/glueops/autoglue/internal/common"
"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"
)
// -- Node Pools Core
// ListNodePools godoc
//
// @ID ListNodePools
// @Summary List node pools (org scoped)
// @Description Returns node pools for the organization in X-Org-ID.
// @Tags NodePools
// @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"
// @Router /node-pools [get]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func ListNodePools(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
q := db.Where("organization_id = ?", orgID)
if needle := strings.TrimSpace(r.URL.Query().Get("q")); needle != "" {
q = q.Where("name LIKE ?", "%"+needle+"%")
}
var pools []models.NodePool
if err := q.
Preload("Servers").
Preload("Labels").
Preload("Taints").
Preload("Annotations").
Order("created_at DESC").
Find(&pools).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
out := make([]dto.NodePoolResponse, 0, len(pools))
for _, p := range pools {
npr := dto.NodePoolResponse{
AuditFields: p.AuditFields,
Name: p.Name,
Role: dto.NodeRole(p.Role),
Servers: make([]dto.ServerResponse, 0, len(p.Servers)),
Labels: make([]dto.LabelResponse, 0, len(p.Labels)),
Taints: make([]dto.TaintResponse, 0, len(p.Taints)),
Annotations: make([]dto.AnnotationResponse, 0, len(p.Annotations)),
}
//Servers
for _, s := range p.Servers {
outSrv := dto.ServerResponse{
ID: s.ID,
Hostname: s.Hostname,
PublicIPAddress: s.PublicIPAddress,
PrivateIPAddress: s.PrivateIPAddress,
OrganizationID: s.OrganizationID,
SshKeyID: s.SshKeyID,
SSHUser: s.SSHUser,
Role: s.Role,
Status: s.Status,
CreatedAt: s.CreatedAt.UTC().Format(time.RFC3339),
UpdatedAt: s.UpdatedAt.UTC().Format(time.RFC3339),
// add more fields as needed
}
npr.Servers = append(npr.Servers, outSrv)
}
//Labels
for _, l := range p.Labels {
outL := dto.LabelResponse{
AuditFields: common.AuditFields{
ID: l.ID,
OrganizationID: l.OrganizationID,
CreatedAt: l.CreatedAt,
UpdatedAt: l.UpdatedAt,
},
Key: l.Key,
Value: l.Value,
}
npr.Labels = append(npr.Labels, outL)
}
// Taints
for _, t := range p.Taints {
outT := dto.TaintResponse{
ID: t.ID,
OrganizationID: t.OrganizationID,
CreatedAt: t.CreatedAt.UTC().Format(time.RFC3339),
UpdatedAt: t.UpdatedAt.UTC().Format(time.RFC3339),
Key: t.Key,
Value: t.Value,
Effect: t.Effect,
}
npr.Taints = append(npr.Taints, outT)
}
// Annotations
for _, a := range p.Annotations {
outA := dto.AnnotationResponse{
AuditFields: common.AuditFields{
ID: a.ID,
OrganizationID: a.OrganizationID,
CreatedAt: a.CreatedAt,
UpdatedAt: a.UpdatedAt,
},
Key: a.Key,
Value: a.Value,
}
npr.Annotations = append(npr.Annotations, outA)
}
out = append(out, npr)
}
utils.WriteJSON(w, http.StatusOK, out)
}
}
// GetNodePool godoc
//
// @ID GetNodePool
// @Summary Get node pool by ID (org scoped)
// @Description Returns one node pool. Add `include=servers` to include servers.
// @Tags NodePools
// @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"
// @Router /node-pools/{id} [get]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func GetNodePool(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "id required")
return
}
var out dto.NodePoolResponse
if err := db.Model(&models.NodePool{}).Preload("Servers").Where("id = ? AND organization_id = ?", id, orgID).First(&out, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "node_pool_not_found", "node pool not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
utils.WriteJSON(w, http.StatusOK, out)
}
}
// CreateNodePool godoc
//
// @ID CreateNodePool
// @Summary Create node pool (org scoped)
// @Description Creates a node pool. Optionally attach initial servers.
// @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"
// @Router /node-pools [post]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func CreateNodePool(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 req dto.CreateNodePoolRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "bad request")
return
}
req.Name = strings.TrimSpace(req.Name)
req.Role = dto.NodeRole(strings.TrimSpace(string(req.Role)))
if req.Name == "" || req.Role == "" {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "missing name/role")
return
}
n := models.NodePool{
AuditFields: common.AuditFields{
OrganizationID: orgID,
},
Name: req.Name,
Role: string(req.Role),
}
if err := db.Create(&n).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
out := dto.NodePoolResponse{
AuditFields: n.AuditFields,
Name: n.Name,
Role: dto.NodeRole(n.Role),
}
utils.WriteJSON(w, http.StatusCreated, out)
}
}
// UpdateNodePool godoc
//
// @ID UpdateNodePool
// @Summary Update node pool (org scoped)
// @Description Partially update node pool fields.
// @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"
// @Router /node-pools/{id} [patch]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func UpdateNodePool(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "id required")
return
}
var n models.NodePool
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&n).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "node_pool_not_found", "node pool not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
var req dto.UpdateNodePoolRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "bad request")
return
}
if req.Name != nil {
n.Name = strings.TrimSpace(*req.Name)
}
if req.Role != nil {
v := dto.NodeRole(strings.TrimSpace(string(*req.Role)))
n.Role = string(v)
}
if err := db.Save(&n).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
out := dto.NodePoolResponse{
AuditFields: n.AuditFields,
Name: n.Name,
Role: dto.NodeRole(n.Role),
}
utils.WriteJSON(w, http.StatusOK, out)
}
}
// DeleteNodePool godoc
//
// @ID DeleteNodePool
// @Summary Delete node pool (org scoped)
// @Description Permanently deletes the node pool.
// @Tags NodePools
// @Produce json
// @Param X-Org-ID header string false "Organization UUID"
// @Param id path string true "Node Pool ID (UUID)"
// @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
// @Security OrgSecretAuth
func DeleteNodePool(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "id required")
return
}
if err := db.Where("id = ? AND organization_id = ?", id, orgID).Delete(&models.NodePool{}).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
w.WriteHeader(http.StatusNoContent)
}
}
// -- Node Pools Servers
// ListNodePoolServers godoc
//
// @ID ListNodePoolServers
// @Summary List servers attached to a node pool (org scoped)
// @Tags NodePools
// @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"
// @Router /node-pools/{id}/servers [get]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func ListNodePoolServers(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "id required")
return
}
var np models.NodePool
if err := db.Where("id = ? AND organization_id = ?", id, orgID).Preload("Servers").First(&np).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "node_pool_not_found", "node pool not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
out := make([]dto.ServerResponse, 0, len(np.Servers))
for _, server := range np.Servers {
out = append(out, dto.ServerResponse{
ID: server.ID,
OrganizationID: server.OrganizationID,
Hostname: server.Hostname,
PrivateIPAddress: server.PrivateIPAddress,
PublicIPAddress: server.PublicIPAddress,
Role: server.Role,
SshKeyID: server.SshKeyID,
SSHUser: server.SSHUser,
Status: server.Status,
CreatedAt: server.CreatedAt.UTC().Format(time.RFC3339),
UpdatedAt: server.UpdatedAt.UTC().Format(time.RFC3339),
})
}
utils.WriteJSON(w, http.StatusOK, out)
}
}
// AttachNodePoolServers godoc
//
// @ID AttachNodePoolServers
// @Summary Attach servers 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)"
// @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
// @Security OrgSecretAuth
func AttachNodePoolServers(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "id required")
return
}
var np models.NodePool
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&np).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "node_pool_not_found", "node pool not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
var req dto.AttachServersRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "bad request")
return
}
ids, err := parseUUIDs(req.ServerIDs)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "invalid server_ids")
return
}
if len(ids) == 0 {
// nothing to attach
utils.WriteError(w, http.StatusBadRequest, "bad_request", "nothing to attach")
return
}
// validate IDs belong to org
if err := ensureServersBelongToOrg(orgID, ids, db); err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "invalid server_ids for this organization")
return
}
// fetch only the requested servers
var servers []models.Server
if err := db.Where("organization_id = ? AND id IN ?", orgID, ids).Find(&servers).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "attach db error")
return
}
if len(servers) == 0 {
w.WriteHeader(http.StatusNoContent)
return
}
if err := db.Model(&np).Association("Servers").Append(&servers); err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "attach failed")
return
}
w.WriteHeader(http.StatusNoContent)
}
}
// DetachNodePoolServer godoc
//
// @ID DetachNodePoolServer
// @Summary Detach one server from a node pool (org scoped)
// @Tags NodePools
// @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"
// @Router /node-pools/{id}/servers/{serverId} [delete]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func DetachNodePoolServer(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "node pool id required")
return
}
serverId, err := uuid.Parse(chi.URLParam(r, "serverId"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "server id required")
return
}
var np models.NodePool
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&np).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "node_pool_not_found", "node pool not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
var s models.Server
if err := db.Where("id = ? AND organization_id = ?", serverId, orgID).First(&s).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "server_not_found", "server not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
if err := db.Model(&np).Association("Servers").Delete(&s); err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "detach error")
return
}
w.WriteHeader(http.StatusNoContent)
}
}
// -- Node Pools Taints
// ListNodePoolTaints godoc
//
// @ID ListNodePoolTaints
// @Summary List taints attached to a node pool (org scoped)
// @Tags NodePools
// @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"
// @Router /node-pools/{id}/taints [get]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func ListNodePoolTaints(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "node pool id required")
return
}
var np models.NodePool
if err := db.Where("id = ? AND organization_id = ?", id, orgID).Preload("Taints").First(&np).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "node_pool_not_found", "node pool not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
out := make([]dto.TaintResponse, 0, len(np.Taints))
for _, t := range np.Taints {
out = append(out, dto.TaintResponse{
ID: t.ID,
Key: t.Key,
Value: t.Value,
Effect: t.Effect,
})
}
utils.WriteJSON(w, http.StatusOK, out)
}
}
// AttachNodePoolTaints godoc
//
// @ID AttachNodePoolTaints
// @Summary Attach taints 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)"
// @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
// @Security OrgSecretAuth
func AttachNodePoolTaints(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "node pool id required")
return
}
var np models.NodePool
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&np).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "node_pool_not_found", "node pool not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
var req dto.AttachTaintsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "bad request")
return
}
ids, err := parseUUIDs(req.TaintIDs)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "invalid taint_ids")
return
}
if len(ids) == 0 {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "nothing to attach")
return
}
// validate IDs belong to org
if err := ensureTaintsBelongToOrg(orgID, ids, db); err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "invalid taint_ids for this organization")
return
}
var taints []models.Taint
if err := db.Where("organization_id = ? AND id IN ?", orgID, ids).Find(&taints).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "attach db error")
return
}
if len(taints) == 0 {
w.WriteHeader(http.StatusNoContent)
return
}
if err := db.Model(&np).Association("Taints").Append(&taints); err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "attach db error")
return
}
w.WriteHeader(http.StatusNoContent)
}
}
// DetachNodePoolTaint godoc
//
// @ID DetachNodePoolTaint
// @Summary Detach one taint from a node pool (org scoped)
// @Tags NodePools
// @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"
// @Router /node-pools/{id}/taints/{taintId} [delete]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func DetachNodePoolTaint(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "node pool id required")
return
}
taintId, err := uuid.Parse(chi.URLParam(r, "taintId"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "taintId_required", "taint id required")
return
}
var np models.NodePool
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&np).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "node_pool_not_found", "node pool not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
var t models.Taint
if err := db.Where("id = ? AND organization_id = ?", taintId, orgID).First(&t).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "taint_not_found", "taint not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
if err := db.Model(&np).Association("Taints").Delete(&t); err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
w.WriteHeader(http.StatusNoContent)
}
}
// -- Node Pools Labels
// ListNodePoolLabels godoc
//
// @ID ListNodePoolLabels
// @Summary List labels attached to a node pool (org scoped)
// @Tags NodePools
// @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"
// @Router /node-pools/{id}/labels [get]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func ListNodePoolLabels(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "node pool id required")
return
}
var np models.NodePool
if err := db.Where("id = ? AND organization_id = ?", id, orgID).Preload("Labels").First(&np).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "node_pool_not_found", "node pool not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
out := make([]dto.LabelResponse, 0, len(np.Taints))
for _, label := range np.Labels {
out = append(out, dto.LabelResponse{
AuditFields: common.AuditFields{
ID: label.ID,
OrganizationID: label.OrganizationID,
CreatedAt: label.CreatedAt,
UpdatedAt: label.UpdatedAt,
},
Key: label.Key,
Value: label.Value,
})
}
utils.WriteJSON(w, http.StatusOK, out)
}
}
// AttachNodePoolLabels godoc
//
// @ID AttachNodePoolLabels
// @Summary Attach labels 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)"
// @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
// @Security OrgSecretAuth
func AttachNodePoolLabels(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "node pool id required")
return
}
var np models.NodePool
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&np).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "node_pool_not_found", "node pool not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
var req dto.AttachLabelsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "bad request")
return
}
ids, err := parseUUIDs(req.LabelIDs)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "invalid label_ids")
return
}
if len(ids) == 0 {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "nothing to attach")
return
}
if err := ensureLabelsBelongToOrg(orgID, ids, db); err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "invalid label_ids for this organization")
}
var labels []models.Label
if err := db.Where("organization_id = ? AND id IN ?", orgID, ids).Find(&labels).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "attach db error")
return
}
if len(labels) == 0 {
w.WriteHeader(http.StatusNoContent)
return
}
if err := db.Model(&np).Association("Labels").Append(&labels); err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "attach failed")
return
}
w.WriteHeader(http.StatusNoContent)
}
}
// DetachNodePoolLabel godoc
//
// @ID DetachNodePoolLabel
// @Summary Detach one label from a node pool (org scoped)
// @Tags NodePools
// @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"
// @Router /node-pools/{id}/labels/{labelId} [delete]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func DetachNodePoolLabel(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "node pool id required")
return
}
labelId, err := uuid.Parse(chi.URLParam(r, "labelId"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "labelId required")
return
}
var np models.NodePool
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&np).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "node_pool_not_found", "node pool not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
var l models.Label
if err := db.Where("id = ? AND organization_id = ?", labelId, orgID).First(&l).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "label_not_found", "label not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
if err := db.Model(&np).Association("Labels").Delete(&l); err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "detach error")
return
}
w.WriteHeader(http.StatusNoContent)
}
}
// -- Node Pools Annotations
// ListNodePoolAnnotations godoc
//
// @ID ListNodePoolAnnotations
// @Summary List annotations attached to a node pool (org scoped)
// @Tags NodePools
// @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"
// @Router /node-pools/{id}/annotations [get]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func ListNodePoolAnnotations(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "node pool id required")
return
}
var np models.NodePool
if err := db.Where("id = ? AND organization_id = ?", id, orgID).Preload("Labels").First(&np).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "node_pool_not_found", "node pool not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
out := make([]dto.AnnotationResponse, 0, len(np.Annotations))
for _, ann := range np.Annotations {
out = append(out, dto.AnnotationResponse{
AuditFields: common.AuditFields{
ID: ann.ID,
OrganizationID: ann.OrganizationID,
CreatedAt: ann.CreatedAt,
UpdatedAt: ann.UpdatedAt,
},
Key: ann.Key,
Value: ann.Value,
})
}
utils.WriteJSON(w, http.StatusOK, out)
}
}
// AttachNodePoolAnnotations godoc
//
// @ID AttachNodePoolAnnotations
// @Summary Attach annotation 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 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
// @Security OrgSecretAuth
func AttachNodePoolAnnotations(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "node pool id required")
return
}
var np models.NodePool
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&np).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "node_pool_not_found", "node pool not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
var req dto.AttachAnnotationsRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "bad request")
return
}
ids, err := parseUUIDs(req.AnnotationIDs)
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "invalid annotation ids")
return
}
if len(ids) == 0 {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "nothing to attach")
return
}
if err := ensureAnnotaionsBelongToOrg(orgID, ids, db); err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "invalid annotation ids for this organization")
return
}
var ann []models.Annotation
if err := db.Where("organization_id = ? AND id IN ?", orgID, ids).Find(&ann).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
if len(ann) == 0 {
w.WriteHeader(http.StatusNoContent)
return
}
if err := db.Model(&np).Association("Annotations").Append(&ann); err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "attach failed")
return
}
w.WriteHeader(http.StatusNoContent)
}
}
// DetachNodePoolAnnotation godoc
//
// @ID DetachNodePoolAnnotation
// @Summary Detach one annotation from a node pool (org scoped)
// @Tags NodePools
// @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"
// @Router /node-pools/{id}/annotations/{annotationId} [delete]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func DetachNodePoolAnnotation(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
orgID, ok := httpmiddleware.OrgIDFrom(r.Context())
if !ok {
utils.WriteError(w, http.StatusForbidden, "org_required", "specify X-Org-ID")
return
}
id, err := uuid.Parse(chi.URLParam(r, "id"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "node pool id required")
return
}
annotationId, err := uuid.Parse(chi.URLParam(r, "annotationId"))
if err != nil {
utils.WriteError(w, http.StatusBadRequest, "id_required", "node pool annotation id required")
return
}
var np models.NodePool
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&np).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "node_pool_not_found", "node pool not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
var ann []models.Annotation
if err := db.Where("id = ? AND organization_id = ?", annotationId, orgID).First(&ann).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "annotation_not_found", "annotation not found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
if err := db.Model(&np).Association("Annotations").Delete(&ann); err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
w.WriteHeader(http.StatusNoContent)
}
}
// -- Helpers
func parseUUIDs(ids []string) ([]uuid.UUID, error) {
out := make([]uuid.UUID, 0, len(ids))
for _, id := range ids {
u, err := uuid.Parse(id)
if err != nil {
return nil, err
}
out = append(out, u)
}
return out, nil
}
func ensureServersBelongToOrg(orgID uuid.UUID, ids []uuid.UUID, db *gorm.DB) error {
var count int64
if err := db.Model(&models.Server{}).Where("organization_id = ? AND id IN ?", orgID, ids).Count(&count).Error; err != nil {
return err
}
if count != int64(len(ids)) {
return errors.New("some servers do not belong to this org")
}
return nil
}
func ensureTaintsBelongToOrg(orgID uuid.UUID, ids []uuid.UUID, db *gorm.DB) error {
var count int64
if err := db.Model(&models.Taint{}).Where("organization_id = ? AND id IN ?", orgID, ids).Count(&count).Error; err != nil {
return err
}
if count != int64(len(ids)) {
return errors.New("some taints do not belong to this org")
}
return nil
}
func ensureLabelsBelongToOrg(orgID uuid.UUID, ids []uuid.UUID, db *gorm.DB) error {
var count int64
if err := db.Model(&models.Label{}).Where("organization_id = ? AND id IN ?", orgID, ids).Count(&count).Error; err != nil {
return err
}
if count != int64(len(ids)) {
return errors.New("some labels do not belong to this org")
}
return nil
}
func ensureAnnotaionsBelongToOrg(orgID uuid.UUID, ids []uuid.UUID, db *gorm.DB) error {
var count int64
if err := db.Model(&models.Annotation{}).Where("organization_id = ? AND id IN ?", orgID, ids).Count(&count).Error; err != nil {
return err
}
if count != int64(len(ids)) {
return errors.New("some annotations do not belong to this org")
}
return nil
}