Files
autoglue/internal/handlers/annotations.go
allanice001 7985b310c5 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>
2025-11-17 04:59:39 +00:00

289 lines
9.1 KiB
Go

package handlers
import (
"encoding/json"
"errors"
"net/http"
"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"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
"gorm.io/gorm"
)
// ListAnnotations godoc
//
// @ID ListAnnotations
// @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
// @Produce json
// @Param X-Org-ID header string false "Organization UUID"
// @Param key query string false "Exact key"
// @Param value query string false "Exact value"
// @Param q query string false "key contains (case-insensitive)"
// @Success 200 {array} dto.AnnotationResponse
// @Failure 401 {string} string "Unauthorized"
// @Failure 403 {string} string "organization required"
// @Failure 500 {string} string "failed to list annotations"
// @Router /annotations [get]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func ListAnnotations(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 key := strings.TrimSpace(r.URL.Query().Get("key")); key != "" {
q = q.Where(`key = ?`, key)
}
if val := strings.TrimSpace(r.URL.Query().Get("value")); val != "" {
q = q.Where(`value = ?`, val)
}
if needle := strings.TrimSpace(r.URL.Query().Get("q")); needle != "" {
q = q.Where(`key ILIKE ?`, "%"+needle+"%")
}
var out []dto.AnnotationResponse
if err := q.Model(&models.Annotation{}).Order("created_at DESC").Scan(&out).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
if out == nil {
out = []dto.AnnotationResponse{}
}
utils.WriteJSON(w, http.StatusOK, out)
}
}
// GetAnnotation godoc
//
// @ID GetAnnotation
// @Summary Get annotation by ID (org scoped)
// @Description Returns one annotation. Add `include=node_pools` to include node pools.
// @Tags Annotations
// @Produce json
// @Param X-Org-ID header string false "Organization UUID"
// @Param id path string true "Annotation ID (UUID)"
// @Success 200 {object} 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 /annotations/{id} [get]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func GetAnnotation(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_request", "bad request")
return
}
var out dto.AnnotationResponse
if err := db.Model(&models.Annotation{}).Where("id = ? AND organization_id = ?", id, orgID).First(&out).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "not_found", "not_found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
utils.WriteJSON(w, http.StatusOK, out)
}
}
// CreateAnnotation godoc
//
// @ID CreateAnnotation
// @Summary Create annotation (org scoped)
// @Description Creates an annotation.
// @Tags Annotations
// @Accept json
// @Produce json
// @Param X-Org-ID header string false "Organization UUID"
// @Param body body dto.CreateAnnotationRequest true "Annotation payload"
// @Success 201 {object} dto.AnnotationResponse
// @Failure 400 {string} string "invalid json / missing fields"
// @Failure 401 {string} string "Unauthorized"
// @Failure 403 {string} string "organization required"
// @Failure 500 {string} string "create failed"
// @Router /annotations [post]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func CreateAnnotation(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.CreateAnnotationRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "bad request")
return
}
req.Key = strings.TrimSpace(req.Key)
req.Value = strings.TrimSpace(req.Value)
if req.Key == "" || req.Value == "" {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "missing key/value")
return
}
a := models.Annotation{
AuditFields: common.AuditFields{OrganizationID: orgID},
Key: req.Key,
Value: req.Value,
}
if err := db.Create(&a).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
out := dto.AnnotationResponse{
AuditFields: a.AuditFields,
Key: a.Key,
Value: a.Value,
}
utils.WriteJSON(w, http.StatusCreated, out)
}
}
// UpdateAnnotation godoc
//
// @ID UpdateAnnotation
// @Summary Update annotation (org scoped)
// @Description Partially update annotation fields.
// @Tags Annotations
// @Accept json
// @Produce json
// @Param X-Org-ID header string false "Organization UUID"
// @Param id path string true "Annotation ID (UUID)"
// @Param body body dto.UpdateAnnotationRequest true "Fields to update"
// @Success 200 {object} dto.AnnotationResponse
// @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 /annotations/{id} [patch]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func UpdateAnnotation(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_request", "bad request")
return
}
var a models.Annotation
if err := db.Where("id = ? AND organization_id = ?", id, orgID).First(&a).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
utils.WriteError(w, http.StatusNotFound, "not_found", "not_found")
return
}
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
var req dto.UpdateAnnotationRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
utils.WriteError(w, http.StatusBadRequest, "bad_request", "bad request")
return
}
if req.Key != nil {
a.Key = strings.TrimSpace(*req.Key)
}
if req.Value != nil {
a.Value = strings.TrimSpace(*req.Value)
}
if err := db.Save(&a).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
out := dto.AnnotationResponse{
AuditFields: a.AuditFields,
Key: a.Key,
Value: a.Value,
}
utils.WriteJSON(w, http.StatusOK, out)
}
}
// DeleteAnnotation godoc
//
// @ID DeleteAnnotation
// @Summary Delete annotation (org scoped)
// @Description Permanently deletes the annotation.
// @Tags Annotations
// @Produce json
// @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"
// @Failure 500 {string} string "delete failed"
// @Router /annotations/{id} [delete]
// @Security BearerAuth
// @Security OrgKeyAuth
// @Security OrgSecretAuth
func DeleteAnnotation(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_request", "bad request")
return
}
if err := db.Where("id = ? AND organization_id = ?", id, orgID).Delete(&models.Annotation{}).Error; err != nil {
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
return
}
w.WriteHeader(http.StatusNoContent)
}
}