mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 21:00:06 +01:00
297 lines
9.3 KiB
Go
297 lines
9.3 KiB
Go
package servers
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/glueops/autoglue/internal/db"
|
|
"github.com/glueops/autoglue/internal/db/models"
|
|
"github.com/glueops/autoglue/internal/middleware"
|
|
"github.com/glueops/autoglue/internal/response"
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// ListServers godoc
|
|
// @Summary List servers (org scoped)
|
|
// @Description Returns servers for the organization in X-Org-ID. Optional filters: status, role.
|
|
// @Tags servers
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param X-Org-ID header string true "Organization UUID"
|
|
// @Param status query string false "Filter by status (pending|provisioning|ready|failed)"
|
|
// @Param role query string false "Filter by role"
|
|
// @Security BearerAuth
|
|
// @Success 200 {array} serverResponse
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 500 {string} string "failed to list servers"
|
|
// @Router /api/v1/servers [get]
|
|
func ListServers(w http.ResponseWriter, r *http.Request) {
|
|
ac := middleware.GetAuthContext(r)
|
|
if ac == nil || ac.OrganizationID == uuid.Nil {
|
|
http.Error(w, "organization required", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
q := db.DB.Where("organization_id = ?", ac.OrganizationID)
|
|
if s := strings.TrimSpace(r.URL.Query().Get("status")); s != "" {
|
|
if !validStatus(s) {
|
|
http.Error(w, "invalid status", http.StatusBadRequest)
|
|
return
|
|
}
|
|
q = q.Where("status = ?", strings.ToLower(s))
|
|
}
|
|
if role := strings.TrimSpace(r.URL.Query().Get("role")); role != "" {
|
|
q = q.Where("role = ?", role)
|
|
}
|
|
|
|
var rows []models.Server
|
|
if err := q.Order("created_at DESC").Find(&rows).Error; err != nil {
|
|
http.Error(w, "failed to list servers", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
out := make([]serverResponse, 0, len(rows))
|
|
for _, s := range rows {
|
|
out = append(out, toResponse(s))
|
|
}
|
|
|
|
_ = response.JSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
// GetServer godoc
|
|
// @Summary Get server by ID (org scoped)
|
|
// @Description Returns one server in the given organization.
|
|
// @Tags servers
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param X-Org-ID header string true "Organization UUID"
|
|
// @Param id path string true "Server ID (UUID)"
|
|
// @Security BearerAuth
|
|
// @Success 200 {object} 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 /api/v1/servers/{id} [get]
|
|
func GetServer(w http.ResponseWriter, r *http.Request) {
|
|
ac := middleware.GetAuthContext(r)
|
|
if ac == nil || ac.OrganizationID == uuid.Nil {
|
|
http.Error(w, "organization required", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var s models.Server
|
|
if err := db.DB.Where("id = ? AND organization_id = ?", id, ac.OrganizationID).First(&s).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
http.Error(w, "fetch failed", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
_ = response.JSON(w, http.StatusOK, toResponse(s))
|
|
}
|
|
|
|
// CreateServer godoc
|
|
// @Summary Create server (org scoped)
|
|
// @Description Creates a server bound to the org in X-Org-ID. Validates that ssh_key_id belongs to the org.
|
|
// @Tags servers
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param X-Org-ID header string true "Organization UUID"
|
|
// @Param body body createServerRequest true "Server payload"
|
|
// @Security BearerAuth
|
|
// @Success 201 {object} serverResponse
|
|
// @Failure 400 {string} string "invalid json / missing fields / invalid status / invalid ssh_key_id"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 500 {string} string "create failed"
|
|
// @Router /api/v1/servers [post]
|
|
func CreateServer(w http.ResponseWriter, r *http.Request) {
|
|
ac := middleware.GetAuthContext(r)
|
|
if ac == nil || ac.OrganizationID == uuid.Nil {
|
|
http.Error(w, "organization required", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
var req createServerRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.IPAddress == "" || req.SSHUser == "" || req.SshKeyID == "" || req.Role == "" {
|
|
http.Error(w, "ip_address, ssh_user, ssh_key_id and role are required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if req.Status != "" && !validStatus(req.Status) {
|
|
http.Error(w, "invalid status", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
keyID, err := uuid.Parse(req.SshKeyID)
|
|
if err != nil {
|
|
http.Error(w, "invalid ssh_key_id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := ensureKeyBelongsToOrg(ac.OrganizationID, keyID); err != nil {
|
|
http.Error(w, "invalid or unauthorized ssh_key_id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
s := models.Server{
|
|
OrganizationID: ac.OrganizationID,
|
|
Hostname: req.Hostname,
|
|
IPAddress: req.IPAddress,
|
|
SSHUser: req.SSHUser,
|
|
SshKeyID: keyID,
|
|
Role: req.Role,
|
|
Status: "pending",
|
|
}
|
|
if req.Status != "" {
|
|
s.Status = strings.ToLower(req.Status)
|
|
}
|
|
|
|
if err := db.DB.Create(&s).Error; err != nil {
|
|
http.Error(w, "create failed", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
_ = response.JSON(w, http.StatusCreated, toResponse(s))
|
|
}
|
|
|
|
// UpdateServer godoc
|
|
// @Summary Update server (org scoped)
|
|
// @Description Partially update fields; changing ssh_key_id validates ownership.
|
|
// @Tags servers
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param X-Org-ID header string true "Organization UUID"
|
|
// @Param id path string true "Server ID (UUID)"
|
|
// @Param body body updateServerRequest true "Fields to update"
|
|
// @Security BearerAuth
|
|
// @Success 200 {object} serverResponse
|
|
// @Failure 400 {string} string "invalid id / invalid json / invalid status / invalid ssh_key_id"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 404 {string} string "not found"
|
|
// @Failure 500 {string} string "update failed"
|
|
// @Router /api/v1/servers/{id} [patch]
|
|
func UpdateServer(w http.ResponseWriter, r *http.Request) {
|
|
ac := middleware.GetAuthContext(r)
|
|
if ac == nil || ac.OrganizationID == uuid.Nil {
|
|
http.Error(w, "organization required", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var s models.Server
|
|
if err := db.DB.Where("id = ? AND organization_id = ?", id, ac.OrganizationID).First(&s).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
http.Error(w, "not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
http.Error(w, "fetch failed", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
var req updateServerRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if req.Hostname != nil {
|
|
s.Hostname = *req.Hostname
|
|
}
|
|
if req.IPAddress != nil {
|
|
s.IPAddress = *req.IPAddress
|
|
}
|
|
if req.SSHUser != nil {
|
|
s.SSHUser = *req.SSHUser
|
|
}
|
|
if req.Role != nil {
|
|
s.Role = *req.Role
|
|
}
|
|
if req.Status != nil {
|
|
if !validStatus(*req.Status) {
|
|
http.Error(w, "invalid status", http.StatusBadRequest)
|
|
return
|
|
}
|
|
s.Status = strings.ToLower(*req.Status)
|
|
}
|
|
if req.SshKeyID != nil {
|
|
keyID, err := uuid.Parse(*req.SshKeyID)
|
|
if err != nil {
|
|
http.Error(w, "invalid ssh_key_id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if err := ensureKeyBelongsToOrg(ac.OrganizationID, keyID); err != nil {
|
|
http.Error(w, "invalid or unauthorized ssh_key_id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
s.SshKeyID = keyID
|
|
}
|
|
|
|
if err := db.DB.Save(&s).Error; err != nil {
|
|
http.Error(w, "update failed", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
_ = response.JSON(w, http.StatusOK, toResponse(s))
|
|
}
|
|
|
|
// DeleteServer godoc
|
|
// @Summary Delete server (org scoped)
|
|
// @Description Permanently deletes the server.
|
|
// @Tags servers
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param X-Org-ID header string true "Organization UUID"
|
|
// @Param id path string true "Server ID (UUID)"
|
|
// @Security BearerAuth
|
|
// @Success 204 {string} string "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 /api/v1/servers/{id} [delete]
|
|
func DeleteServer(w http.ResponseWriter, r *http.Request) {
|
|
ac := middleware.GetAuthContext(r)
|
|
if ac == nil || ac.OrganizationID == uuid.Nil {
|
|
http.Error(w, "organization required", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
id, err := uuid.Parse(chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
http.Error(w, "invalid id", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if err := db.DB.Where("id = ? AND organization_id = ?", id, ac.OrganizationID).
|
|
Delete(&models.Server{}).Error; err != nil {
|
|
http.Error(w, "delete failed", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
response.NoContent(w)
|
|
}
|