Files
autoglue/internal/handlers/clusters/funcs.go
2025-09-16 22:26:53 +01:00

192 lines
5.5 KiB
Go

package clusters
import (
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"net/http"
"strings"
"github.com/glueops/autoglue/internal/db"
"github.com/glueops/autoglue/internal/db/models"
"github.com/google/uuid"
"gorm.io/gorm"
)
func ensureNodePoolsBelongToOrg(orgID uuid.UUID, ids []uuid.UUID) error {
if len(ids) == 0 {
return errors.New("empty ids")
}
var count int64
if err := db.DB.Model(&models.NodePool{}).
Where("organization_id = ? AND id IN ?", orgID, ids).
Count(&count).Error; err != nil {
return err
}
if count != int64(len(ids)) {
return errors.New("some node pools do not belong to org")
}
return nil
}
func ensureServerBelongsToOrgWithRole(orgID uuid.UUID, id uuid.UUID, role string) error {
var count int64
if err := db.DB.Model(&models.Server{}).
Where("organization_id = ? AND id = ? AND role = ?", orgID, id, role).
Count(&count).Error; err != nil {
return err
}
if count != 1 {
return errors.New("server not found in org or role mismatch")
}
return nil
}
func toResp(c models.Cluster, includePools, includeBastion bool) clusterResponse {
out := clusterResponse{
ID: c.ID,
Name: c.Name,
Provider: c.Provider,
Region: c.Region,
Status: c.Status,
ClusterLoadBalancer: c.ClusterLoadBalancer,
ControlLoadBalancer: c.ControlLoadBalancer,
}
if includePools {
out.NodePools = make([]nodePoolBrief, 0, len(c.NodePools))
for _, p := range c.NodePools {
nb := nodePoolBrief{ID: p.ID, Name: p.Name}
fmt.Printf("node pool %s\n", p.Name)
fmt.Printf("node pool labels %d\n", len(p.Labels))
if len(p.Labels) > 0 {
nb.Labels = make([]labelBrief, 0, len(p.Labels))
for _, l := range p.Labels {
nb.Labels = append(nb.Labels, labelBrief{ID: l.ID, Key: l.Key, Value: l.Value})
}
}
fmt.Printf("node pool annotations %d\n", len(p.Annotations))
if len(p.Annotations) > 0 {
nb.Annotations = make([]annotationBrief, 0, len(p.Annotations))
for _, a := range p.Annotations {
nb.Annotations = append(nb.Annotations, annotationBrief{ID: a.ID, Key: a.Key, Value: a.Value})
}
}
fmt.Printf("node pool taints %d\n", len(p.Taints))
if len(p.Taints) > 0 {
nb.Taints = make([]taintBrief, 0, len(p.Taints))
for _, t := range p.Taints {
nb.Taints = append(nb.Taints, taintBrief{ID: t.ID, Key: t.Key, Value: t.Value, Effect: t.Effect})
}
}
if len(p.Servers) > 0 {
nb.Servers = make([]serverBrief, 0, len(p.Servers))
for _, s := range p.Servers {
nb.Servers = append(nb.Servers, serverBrief{ID: s.ID, Hostname: s.Hostname, Role: s.Role, Status: s.Status, IP: s.IPAddress})
}
}
out.NodePools = append(out.NodePools, nb)
}
}
if includeBastion && c.BastionServer != nil {
out.BastionServer = &serverBrief{
ID: c.BastionServer.ID,
Hostname: c.BastionServer.Hostname,
IP: c.BastionServer.IPAddress,
Role: c.BastionServer.Role,
Status: c.BastionServer.Status,
}
}
return out
}
func contains(xs []string, want string) bool {
for _, x := range xs {
if strings.TrimSpace(x) == want {
return true
}
}
return false
}
func errorsIsNotFound(err error) bool { return err == gorm.ErrRecordNotFound }
func parseUUIDs(ids []string) ([]uuid.UUID, error) {
out := make([]uuid.UUID, 0, len(ids))
for _, s := range ids {
u, err := uuid.Parse(strings.TrimSpace(s))
if err != nil {
return nil, err
}
out = append(out, u)
}
return out, nil
}
const maxJSONBytes int64 = 1 << 20
func readJSON(w http.ResponseWriter, r *http.Request, dst any) bool {
if ct := r.Header.Get("Content-Type"); ct != "" {
mt, _, err := mime.ParseMediaType(ct)
if err != nil || mt != "application/json" {
http.Error(w, "Content-Type must be application/json", http.StatusUnsupportedMediaType)
return false
}
}
r.Body = http.MaxBytesReader(w, r.Body, maxJSONBytes)
defer r.Body.Close()
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(dst); err != nil {
var syntaxErr *json.SyntaxError
var typeErr *json.UnmarshalTypeError
var maxErr *http.MaxBytesError
switch {
case errors.As(err, &maxErr):
http.Error(w, fmt.Sprintf("request body too large (max %d bytes)", maxJSONBytes), http.StatusRequestEntityTooLarge)
case errors.Is(err, io.EOF):
http.Error(w, "request body must not be empty", http.StatusBadRequest)
case errors.As(err, &syntaxErr):
http.Error(w, fmt.Sprintf("malformed JSON at character %d", syntaxErr.Offset), http.StatusBadRequest)
case errors.Is(err, io.ErrUnexpectedEOF):
http.Error(w, "malformed JSON", http.StatusBadRequest)
case errors.As(err, &typeErr):
// Example: expected string but got number for field "name"
field := typeErr.Field
if field == "" && len(typeErr.Struct) > 0 {
field = typeErr.Struct
}
http.Error(w, fmt.Sprintf("invalid value for %q (expected %s)", field, typeErr.Type.String()), http.StatusBadRequest)
case strings.HasPrefix(err.Error(), "json: unknown field "):
// Extract the field name from the error message.
field := strings.Trim(strings.TrimPrefix(err.Error(), "json: unknown field "), "\"")
http.Error(w, fmt.Sprintf("unknown field %q", field), http.StatusBadRequest)
default:
http.Error(w, "invalid json", http.StatusBadRequest)
}
return false
}
if dec.More() {
// Try to read one more token/value; if not EOF, there was extra content.
var extra any
if err := dec.Decode(&extra); err != io.EOF {
http.Error(w, "body must contain only a single JSON object", http.StatusBadRequest)
return false
}
}
return true
}