fix: refactor labels to use common entries between model and dto

This commit is contained in:
allanice001
2025-11-04 09:23:31 +00:00
parent 3ca77c097d
commit 2170b9a945
26 changed files with 398 additions and 224 deletions

View File

@@ -36,6 +36,7 @@ func NewRuntime() *Runtime {
&models.Server{},
&models.Taint{},
&models.Label{},
&models.Annotation{},
)
if err != nil {
log.Fatalf("Error initializing database: %v", err)

14
internal/common/audit.go Normal file
View File

@@ -0,0 +1,14 @@
package common
import (
"time"
"github.com/google/uuid"
)
type AuditFields struct {
ID uuid.UUID `json:"id" gorm:"type:uuid;default:gen_random_uuid()"`
OrganizationID uuid.UUID `json:"organization_id" gorm:"type:uuid;index"`
CreatedAt time.Time `json:"created_at,omitempty" gorm:"column:created_at;not null;default:now()"`
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"autoUpdateTime;column:updated_at;not null;default:now()"`
}

View File

@@ -0,0 +1,21 @@
package handlers
// ListAnnotations godoc
// @ID ListAnnotations
// @Summary List annotations (org scoped)
// @Description Returns annotations for the organization in X-Org-ID. Filters: `name`, `value`, and `q` (name 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 name query string false "Exact name"
// @Param value query string false "Exact value"
// @Param q query string false "name contains (case-insensitive)"
// @Success 200 {array} annotationResponse
// @Failure 401 {string} string "Unauthorized"
// @Failure 403 {string} string "organization required"
// @Failure 500 {string} string "failed to list annotations"
// @Router /api/v1/annotations [get]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth

View File

@@ -0,0 +1,19 @@
package dto
import "github.com/glueops/autoglue/internal/common"
type AnnotationResponse struct {
common.AuditFields
Key string `json:"key"`
Value string `json:"value"`
}
type CreateAnnotationRequest struct {
Key string `json:"key"`
Value string `json:"value"`
}
type UpdateAnnotationRequest struct {
Key *string `json:"key,omitempty"`
Value *string `json:"value,omitempty"`
}

View File

@@ -1,14 +1,13 @@
package dto
import "github.com/google/uuid"
import (
"github.com/glueops/autoglue/internal/common"
)
type LabelResponse struct {
ID uuid.UUID `json:"id"`
OrganizationID uuid.UUID `json:"organization_id"`
Key string `json:"key"`
Value string `json:"value"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
common.AuditFields
Key string `json:"key"`
Value string `json:"value"`
}
type CreateLabelRequest struct {

View File

@@ -7,6 +7,7 @@ import (
"strings"
"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"
@@ -53,20 +54,12 @@ func ListLabels(db *gorm.DB) http.HandlerFunc {
if needle := strings.TrimSpace(r.URL.Query().Get("q")); needle != "" {
q = q.Where(`key ILIKE ?`, "%"+needle+"%")
}
var rows []models.Label
if err := q.Order("created_at DESC").Find(&rows).Error; err != nil {
var out []dto.LabelResponse
if err := q.Model(&models.Label{}).Order("created_at DESC").Scan(&out).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
out := make([]dto.LabelResponse, 0, len(rows))
for _, row := range rows {
out = append(out, dto.LabelResponse{
Key: row.Key,
Value: row.Value,
ID: row.ID,
})
}
utils.WriteJSON(w, http.StatusOK, out)
}
}
@@ -104,17 +97,14 @@ func GetLabel(db *gorm.DB) http.HandlerFunc {
return
}
var row models.Label
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&row).Error; err != nil {
utils.WriteError(w, http.StatusNotFound, "label_not_found", "label not found")
return
var out dto.LabelResponse
if err := db.Model(&models.Label{}).Where("id = ? AND organization_id = ?", id, orgID).Limit(1).Scan(&out).Error; err != nil {
if out.ID == uuid.Nil {
utils.WriteError(w, http.StatusNotFound, "label_not_found", "label not found")
return
}
}
out := dto.LabelResponse{
Key: row.Key,
Value: row.Value,
ID: row.ID,
}
utils.WriteJSON(w, http.StatusOK, out)
}
}
@@ -160,9 +150,9 @@ func CreateLabel(db *gorm.DB) http.HandlerFunc {
}
l := models.Label{
OrganizationID: orgID,
Key: req.Key,
Value: req.Value,
AuditFields: common.AuditFields{OrganizationID: orgID},
Key: req.Key,
Value: req.Value,
}
if err := db.Create(&l).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
@@ -170,14 +160,15 @@ func CreateLabel(db *gorm.DB) http.HandlerFunc {
}
out := dto.LabelResponse{
ID: l.ID,
Key: l.Key,
Value: l.Value,
AuditFields: l.AuditFields,
Key: l.Key,
Value: l.Value,
}
utils.WriteJSON(w, http.StatusCreated, out)
}
}
// UpdateLabel godoc
// UpdateLabel godoc
// @ID UpdateLabel
// @Summary Update label (org scoped)
@@ -228,22 +219,22 @@ func UpdateLabel(db *gorm.DB) http.HandlerFunc {
return
}
next := l
if req.Key != nil {
next.Key = strings.TrimSpace(*req.Key)
l.Key = strings.TrimSpace(*req.Key)
}
if req.Value != nil {
next.Value = strings.TrimSpace(*req.Value)
l.Value = strings.TrimSpace(*req.Value)
}
if err := db.Save(&next).Error; err != nil {
if err := db.Save(&l).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
out := dto.LabelResponse{
ID: next.ID,
Key: next.Key,
Value: next.Value,
AuditFields: l.AuditFields,
Key: l.Key,
Value: l.Value,
}
utils.WriteJSON(w, http.StatusOK, out)
}

View File

@@ -29,7 +29,7 @@ type userAPIKeyOut struct {
// ListUserAPIKeys godoc
// @ID ListUserAPIKeys
// @Summary List my API keys
// @Tags Me / API Keys
// @Tags MeAPIKeys
// @Produce json
// @Success 200 {array} userAPIKeyOut
// @Router /me/api-keys [get]
@@ -67,7 +67,7 @@ type createUserKeyRequest struct {
// @ID CreateUserAPIKey
// @Summary Create a new user API key
// @Description Returns the plaintext key once. Store it securely on the client side.
// @Tags Me / API Keys
// @Tags MeAPIKeys
// @Accept json
// @Produce json
// @Param body body createUserKeyRequest true "Key options"
@@ -120,7 +120,7 @@ func CreateUserAPIKey(db *gorm.DB) http.HandlerFunc {
// DeleteUserAPIKey godoc
// @ID DeleteUserAPIKey
// @Summary Delete a user API key
// @Tags Me / API Keys
// @Tags MeAPIKeys
// @Produce json
// @Param id path string true "Key ID (UUID)"
// @Success 204 "No Content"

View File

@@ -5,6 +5,7 @@ import (
"errors"
"net/http"
"strings"
"time"
"github.com/glueops/autoglue/internal/api/httpmiddleware"
"github.com/glueops/autoglue/internal/handlers/dto"
@@ -63,10 +64,13 @@ func ListTaints(db *gorm.DB) http.HandlerFunc {
out := make([]dto.TaintResponse, 0, len(rows))
for _, row := range rows {
out = append(out, dto.TaintResponse{
ID: row.ID,
Key: row.Key,
Value: row.Value,
Effect: row.Effect,
ID: row.ID,
Key: row.Key,
Value: row.Value,
Effect: row.Effect,
OrganizationID: row.OrganizationID,
CreatedAt: row.CreatedAt.UTC().Format(time.RFC3339),
UpdatedAt: row.UpdatedAt.UTC().Format(time.RFC3339),
})
}
utils.WriteJSON(w, http.StatusOK, out)
@@ -115,10 +119,13 @@ func GetTaint(db *gorm.DB) http.HandlerFunc {
return
}
out := dto.TaintResponse{
ID: row.ID,
Key: row.Key,
Value: row.Value,
Effect: row.Effect,
ID: row.ID,
Key: row.Key,
Value: row.Value,
Effect: row.Effect,
OrganizationID: row.OrganizationID,
CreatedAt: row.CreatedAt.UTC().Format(time.RFC3339),
UpdatedAt: row.UpdatedAt.UTC().Format(time.RFC3339),
}
utils.WriteJSON(w, http.StatusOK, out)
}
@@ -182,10 +189,13 @@ func CreateTaint(db *gorm.DB) http.HandlerFunc {
}
out := dto.TaintResponse{
ID: t.ID,
Key: t.Key,
Value: t.Value,
Effect: t.Effect,
ID: t.ID,
Key: t.Key,
Value: t.Value,
Effect: t.Effect,
OrganizationID: t.OrganizationID,
CreatedAt: t.CreatedAt.UTC().Format(time.RFC3339),
UpdatedAt: t.UpdatedAt.UTC().Format(time.RFC3339),
}
utils.WriteJSON(w, http.StatusCreated, out)
}
@@ -268,10 +278,13 @@ func UpdateTaint(db *gorm.DB) http.HandlerFunc {
}
out := dto.TaintResponse{
ID: next.ID,
Key: next.Key,
Value: next.Value,
Effect: next.Effect,
ID: next.ID,
Key: next.Key,
Value: next.Value,
Effect: next.Effect,
OrganizationID: next.OrganizationID,
CreatedAt: next.CreatedAt.UTC().Format(time.RFC3339),
UpdatedAt: next.UpdatedAt.UTC().Format(time.RFC3339),
}
utils.WriteJSON(w, http.StatusOK, out)
}

View File

@@ -0,0 +1,12 @@
package models
import (
"github.com/glueops/autoglue/internal/common"
)
type Annotation struct {
common.AuditFields `gorm:"embedded"`
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
Key string `gorm:"not null" json:"key"`
Value string `gorm:"not null" json:"value"`
}

View File

@@ -1,18 +1,13 @@
package models
import (
"time"
"github.com/google/uuid"
"github.com/glueops/autoglue/internal/common"
)
type Label 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"`
Key string `gorm:"not null" json:"key"`
Value string `gorm:"not null" json:"value"`
NodePools []NodePool `gorm:"many2many:node_labels;constraint:OnDelete:CASCADE" json:"servers,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;not null;default:now()" json:"created_at"`
UpdatedAt time.Time `gorm:"autoUpdateTime;column:updated_at;not null;default:now()" json:"updated_at"`
common.AuditFields
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
Key string `gorm:"not null" json:"key"`
Value string `gorm:"not null" json:"value"`
NodePools []NodePool `gorm:"many2many:node_labels;constraint:OnDelete:CASCADE" json:"servers,omitempty"`
}

View File

@@ -12,9 +12,9 @@ type NodePool struct {
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
Name string `gorm:"not null" json:"name"`
Servers []Server `gorm:"many2many:node_servers;constraint:OnDelete:CASCADE" json:"servers,omitempty"`
//Annotations []Annotation `gorm:"many2many:node_annotations;constraint:OnDelete:CASCADE" json:"annotations,omitempty"`
Labels []Label `gorm:"many2many:node_labels;constraint:OnDelete:CASCADE" json:"labels,omitempty"`
Taints []Taint `gorm:"many2many:node_taints;constraint:OnDelete:CASCADE" json:"taints,omitempty"`
Annotations []Annotation `gorm:"many2many:node_annotations;constraint:OnDelete:CASCADE" json:"annotations,omitempty"`
Labels []Label `gorm:"many2many:node_labels;constraint:OnDelete:CASCADE" json:"labels,omitempty"`
Taints []Taint `gorm:"many2many:node_taints;constraint:OnDelete:CASCADE" json:"taints,omitempty"`
//Clusters []Cluster `gorm:"many2many:cluster_node_pools;constraint:OnDelete:CASCADE" json:"clusters,omitempty"`
Topology string `gorm:"not null" json:"topology,omitempty"` // stacked or external
Role string `gorm:"not null" json:"role,omitempty"` // master, worker, or etcd (etcd only if topology = external