mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 12:50:05 +01:00
192 lines
5.5 KiB
Go
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
|
|
}
|