mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 21:00:06 +01:00
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>
1487 lines
48 KiB
Go
1487 lines
48 KiB
Go
package handlers
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"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"
|
|
)
|
|
|
|
// ListClusters godoc
|
|
//
|
|
// @ID ListClusters
|
|
// @Summary List clusters (org scoped)
|
|
// @Description Returns clusters for the organization in X-Org-ID. Filter by `q` (name contains).
|
|
// @Tags Clusters
|
|
// @Produce json
|
|
// @Param X-Org-ID header string false "Organization UUID"
|
|
// @Param q query string false "Name contains (case-insensitive)"
|
|
// @Success 200 {array} dto.ClusterResponse
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 500 {string} string "failed to list clusters"
|
|
// @Router /clusters [get]
|
|
// @Security BearerAuth
|
|
// @Security OrgKeyAuth
|
|
// @Security OrgSecretAuth
|
|
func ListClusters(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 needle := strings.TrimSpace(r.URL.Query().Get("q")); needle != "" {
|
|
q = q.Where(`name ILIKE ?`, "%"+needle+"%")
|
|
}
|
|
|
|
var rows []models.Cluster
|
|
if err := q.
|
|
Preload("CaptainDomain").
|
|
Preload("ControlPlaneRecordSet").
|
|
Preload("AppsLoadBalancer").
|
|
Preload("GlueOpsLoadBalancer").
|
|
Preload("BastionServer").
|
|
Preload("NodePools").
|
|
Preload("NodePools.Labels").
|
|
Preload("NodePools.Annotations").
|
|
Preload("NodePools.Taints").
|
|
Preload("NodePools.Servers").
|
|
Find(&rows).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
out := make([]dto.ClusterResponse, 0, len(rows))
|
|
for _, row := range rows {
|
|
out = append(out, clusterToDTO(row))
|
|
}
|
|
utils.WriteJSON(w, http.StatusOK, out)
|
|
}
|
|
}
|
|
|
|
// GetCluster godoc
|
|
//
|
|
// @ID GetCluster
|
|
// @Summary Get a single cluster by ID (org scoped)
|
|
// @Description Returns a cluster with all related resources (domain, record set, load balancers, bastion, node pools).
|
|
// @Tags Clusters
|
|
// @Produce json
|
|
// @Param X-Org-ID header string false "Organization UUID"
|
|
// @Param clusterID path string true "Cluster ID"
|
|
// @Success 200 {object} dto.ClusterResponse
|
|
// @Failure 400 {string} string "bad request"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 404 {string} string "cluster not found"
|
|
// @Failure 500 {string} string "db error"
|
|
// @Router /clusters/{clusterID} [get]
|
|
// @Security BearerAuth
|
|
// @Security OrgKeyAuth
|
|
// @Security OrgSecretAuth
|
|
func GetCluster(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
|
|
}
|
|
|
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
|
return
|
|
}
|
|
|
|
var cluster models.Cluster
|
|
if err := db.
|
|
Where("id = ? AND organization_id = ?", clusterID, orgID).
|
|
Preload("CaptainDomain").
|
|
Preload("ControlPlaneRecordSet").
|
|
Preload("AppsLoadBalancer").
|
|
Preload("GlueOpsLoadBalancer").
|
|
Preload("BastionServer").
|
|
Preload("NodePools").
|
|
Preload("NodePools.Labels").
|
|
Preload("NodePools.Annotations").
|
|
Preload("NodePools.Taints").
|
|
Preload("NodePools.Servers").
|
|
First(&cluster).Error; err != nil {
|
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
|
|
}
|
|
}
|
|
|
|
// CreateCluster godoc
|
|
//
|
|
// @ID CreateCluster
|
|
// @Summary Create cluster (org scoped)
|
|
// @Description Creates a cluster. Status is managed by the system and starts as `pre_pending` for validation.
|
|
// @Tags Clusters
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param X-Org-ID header string false "Organization UUID"
|
|
// @Param body body dto.CreateClusterRequest true "payload"
|
|
// @Success 201 {object} dto.ClusterResponse
|
|
// @Failure 400 {string} string "invalid json"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 500 {string} string "create failed"
|
|
// @Router /clusters [post]
|
|
// @Security BearerAuth
|
|
// @Security OrgKeyAuth
|
|
// @Security OrgSecretAuth
|
|
func CreateCluster(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 in dto.CreateClusterRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
|
return
|
|
}
|
|
|
|
certificateKey, err := GenerateSecureHex(32)
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "internal_error", "failed to generate certificate key")
|
|
return
|
|
}
|
|
|
|
randomToken, err := GenerateFormattedToken()
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "internal_error", "failed to generate random token")
|
|
return
|
|
}
|
|
|
|
c := models.Cluster{
|
|
OrganizationID: orgID,
|
|
Name: in.Name,
|
|
Provider: in.Provider,
|
|
Region: in.Region,
|
|
Status: models.ClusterStatusPrePending,
|
|
LastError: "",
|
|
CertificateKey: certificateKey,
|
|
RandomToken: randomToken,
|
|
}
|
|
|
|
if err := db.Create(&c).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusCreated, clusterToDTO(c))
|
|
}
|
|
}
|
|
|
|
// UpdateCluster godoc
|
|
//
|
|
// @ID UpdateCluster
|
|
// @Summary Update basic cluster details (org scoped)
|
|
// @Description Updates the cluster name, provider, and/or region. Status is managed by the system.
|
|
// @Tags Clusters
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param X-Org-ID header string false "Organization UUID"
|
|
// @Param clusterID path string true "Cluster ID"
|
|
// @Param body body dto.UpdateClusterRequest true "payload"
|
|
// @Success 200 {object} dto.ClusterResponse
|
|
// @Failure 400 {string} string "bad request"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 404 {string} string "cluster not found"
|
|
// @Failure 500 {string} string "db error"
|
|
// @Router /clusters/{clusterID} [patch]
|
|
// @Security BearerAuth
|
|
// @Security OrgKeyAuth
|
|
// @Security OrgSecretAuth
|
|
func UpdateCluster(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
|
|
}
|
|
|
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
|
return
|
|
}
|
|
|
|
var in dto.UpdateClusterRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
|
return
|
|
}
|
|
|
|
var cluster models.Cluster
|
|
if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
// Apply only provided fields
|
|
if in.Name != nil {
|
|
cluster.Name = *in.Name
|
|
}
|
|
if in.Provider != nil {
|
|
cluster.Provider = *in.Provider
|
|
}
|
|
if in.Region != nil {
|
|
cluster.Region = *in.Region
|
|
}
|
|
|
|
if err := db.Save(&cluster).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
// Any change to the cluster config may require re-validation.
|
|
_ = markClusterNeedsValidation(db, cluster.ID)
|
|
|
|
// Preload for a rich response
|
|
if err := db.Preload("CaptainDomain").
|
|
Preload("ControlPlaneRecordSet").
|
|
Preload("AppsLoadBalancer").
|
|
Preload("GlueOpsLoadBalancer").
|
|
Preload("BastionServer").
|
|
Preload("NodePools").
|
|
Preload("NodePools.Labels").
|
|
Preload("NodePools.Annotations").
|
|
Preload("NodePools.Taints").
|
|
Preload("NodePools.Servers").
|
|
First(&cluster, "id = ?", cluster.ID).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
|
|
}
|
|
}
|
|
|
|
// DeleteCluster godoc
|
|
//
|
|
// @ID DeleteCluster
|
|
// @Summary Delete a cluster (org scoped)
|
|
// @Description Deletes the cluster. Related resources are cleaned up via DB constraints (e.g. CASCADE).
|
|
// @Tags Clusters
|
|
// @Produce json
|
|
// @Param X-Org-ID header string false "Organization UUID"
|
|
// @Param clusterID path string true "Cluster ID"
|
|
// @Success 204 {string} string "deleted"
|
|
// @Failure 400 {string} string "bad request"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 404 {string} string "cluster not found"
|
|
// @Failure 500 {string} string "db error"
|
|
// @Router /clusters/{clusterID} [delete]
|
|
// @Security BearerAuth
|
|
// @Security OrgKeyAuth
|
|
// @Security OrgSecretAuth
|
|
func DeleteCluster(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
|
|
}
|
|
|
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
|
return
|
|
}
|
|
|
|
tx := db.Where("id = ? AND organization_id = ?", clusterID, orgID).Delete(&models.Cluster{})
|
|
if tx.Error != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
if tx.RowsAffected == 0 {
|
|
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// AttachCaptainDomain godoc
|
|
//
|
|
// @ID AttachCaptainDomain
|
|
// @Summary Attach a captain domain to a cluster
|
|
// @Description Sets captain_domain_id on the cluster. Validation of shape happens asynchronously.
|
|
// @Tags Clusters
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param X-Org-ID header string false "Organization UUID"
|
|
// @Param clusterID path string true "Cluster ID"
|
|
// @Param body body dto.AttachCaptainDomainRequest true "payload"
|
|
// @Success 200 {object} dto.ClusterResponse
|
|
// @Failure 400 {string} string "bad request"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 404 {string} string "cluster or domain not found"
|
|
// @Failure 500 {string} string "db error"
|
|
// @Router /clusters/{clusterID}/captain-domain [post]
|
|
// @Security BearerAuth
|
|
// @Security OrgKeyAuth
|
|
// @Security OrgSecretAuth
|
|
func AttachCaptainDomain(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
|
|
}
|
|
|
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
|
return
|
|
}
|
|
|
|
var in dto.AttachCaptainDomainRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
|
return
|
|
}
|
|
|
|
var cluster models.Cluster
|
|
if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
// Ensure domain exists and belongs to the org
|
|
var domain models.Domain
|
|
if err := db.Where("id = ? AND organization_id = ?", in.DomainID, orgID).First(&domain).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "domain_not_found", "domain not found for organization")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
cluster.CaptainDomainID = &domain.ID
|
|
if err := db.Save(&cluster).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
if err := markClusterNeedsValidation(db, cluster.ID); err != nil {
|
|
// Don't fail the request, just log if you have logging.
|
|
}
|
|
|
|
// Preload domain for response
|
|
if err := db.Preload("CaptainDomain").
|
|
Preload("ControlPlaneRecordSet").
|
|
Preload("AppsLoadBalancer").
|
|
Preload("GlueOpsLoadBalancer").
|
|
Preload("BastionServer").
|
|
Preload("NodePools").
|
|
Preload("NodePools.Labels").
|
|
Preload("NodePools.Annotations").
|
|
Preload("NodePools.Taints").
|
|
Preload("NodePools.Servers").
|
|
First(&cluster, "id = ?", cluster.ID).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
|
|
}
|
|
}
|
|
|
|
// DetachCaptainDomain godoc
|
|
//
|
|
// @ID DetachCaptainDomain
|
|
// @Summary Detach the captain domain from a cluster
|
|
// @Description Clears captain_domain_id on the cluster. This will likely cause the cluster to become incomplete.
|
|
// @Tags Clusters
|
|
// @Produce json
|
|
// @Param X-Org-ID header string false "Organization UUID"
|
|
// @Param clusterID path string true "Cluster ID"
|
|
// @Success 200 {object} dto.ClusterResponse
|
|
// @Failure 400 {string} string "bad request"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 404 {string} string "cluster not found"
|
|
// @Failure 500 {string} string "db error"
|
|
// @Router /clusters/{clusterID}/captain-domain [delete]
|
|
// @Security BearerAuth
|
|
// @Security OrgKeyAuth
|
|
// @Security OrgSecretAuth
|
|
func DetachCaptainDomain(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
|
|
}
|
|
|
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
|
return
|
|
}
|
|
|
|
var cluster models.Cluster
|
|
if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
cluster.CaptainDomainID = nil
|
|
if err := db.Save(&cluster).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
_ = markClusterNeedsValidation(db, cluster.ID)
|
|
|
|
if err := db.Preload("CaptainDomain").
|
|
Preload("ControlPlaneRecordSet").
|
|
Preload("AppsLoadBalancer").
|
|
Preload("GlueOpsLoadBalancer").
|
|
Preload("BastionServer").
|
|
Preload("NodePools").
|
|
Preload("NodePools.Labels").
|
|
Preload("NodePools.Annotations").
|
|
Preload("NodePools.Taints").
|
|
Preload("NodePools.Servers").
|
|
First(&cluster, "id = ?", cluster.ID).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
|
|
}
|
|
}
|
|
|
|
// AttachControlPlaneRecordSet godoc
|
|
//
|
|
// @ID AttachControlPlaneRecordSet
|
|
// @Summary Attach a control plane record set to a cluster
|
|
// @Description Sets control_plane_record_set_id on the cluster.
|
|
// @Tags Clusters
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param X-Org-ID header string false "Organization UUID"
|
|
// @Param clusterID path string true "Cluster ID"
|
|
// @Param body body dto.AttachRecordSetRequest true "payload"
|
|
// @Success 200 {object} dto.ClusterResponse
|
|
// @Failure 400 {string} string "bad request"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 404 {string} string "cluster or record set not found"
|
|
// @Failure 500 {string} string "db error"
|
|
// @Router /clusters/{clusterID}/control-plane-record-set [post]
|
|
// @Security BearerAuth
|
|
// @Security OrgKeyAuth
|
|
// @Security OrgSecretAuth
|
|
func AttachControlPlaneRecordSet(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
|
|
}
|
|
|
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
|
return
|
|
}
|
|
|
|
var in dto.AttachRecordSetRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
|
return
|
|
}
|
|
|
|
var cluster models.Cluster
|
|
if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
// record sets are indirectly org-scoped via their domain
|
|
var rs models.RecordSet
|
|
if err := db.
|
|
Joins("JOIN domains d ON d.id = record_sets.domain_id").
|
|
Where("record_sets.id = ? AND d.organization_id = ?", in.RecordSetID, orgID).
|
|
First(&rs).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "recordset_not_found", "record set not found for organization")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
cluster.ControlPlaneRecordSetID = &rs.ID
|
|
if err := db.Save(&cluster).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
_ = markClusterNeedsValidation(db, cluster.ID)
|
|
|
|
if err := db.Preload("CaptainDomain").
|
|
Preload("ControlPlaneRecordSet").
|
|
Preload("AppsLoadBalancer").
|
|
Preload("GlueOpsLoadBalancer").
|
|
Preload("BastionServer").
|
|
Preload("NodePools").
|
|
Preload("NodePools.Labels").
|
|
Preload("NodePools.Annotations").
|
|
Preload("NodePools.Taints").
|
|
Preload("NodePools.Servers").
|
|
First(&cluster, "id = ?", cluster.ID).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
|
|
}
|
|
}
|
|
|
|
// DetachControlPlaneRecordSet godoc
|
|
//
|
|
// @ID DetachControlPlaneRecordSet
|
|
// @Summary Detach the control plane record set from a cluster
|
|
// @Description Clears control_plane_record_set_id on the cluster.
|
|
// @Tags Clusters
|
|
// @Produce json
|
|
// @Param X-Org-ID header string false "Organization UUID"
|
|
// @Param clusterID path string true "Cluster ID"
|
|
// @Success 200 {object} dto.ClusterResponse
|
|
// @Failure 400 {string} string "bad request"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 404 {string} string "cluster not found"
|
|
// @Failure 500 {string} string "db error"
|
|
// @Router /clusters/{clusterID}/control-plane-record-set [delete]
|
|
// @Security BearerAuth
|
|
// @Security OrgKeyAuth
|
|
// @Security OrgSecretAuth
|
|
func DetachControlPlaneRecordSet(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
|
|
}
|
|
|
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
|
return
|
|
}
|
|
|
|
var cluster models.Cluster
|
|
if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
cluster.ControlPlaneRecordSetID = nil
|
|
if err := db.Save(&cluster).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
_ = markClusterNeedsValidation(db, cluster.ID)
|
|
|
|
if err := db.Preload("CaptainDomain").
|
|
Preload("ControlPlaneRecordSet").
|
|
Preload("AppsLoadBalancer").
|
|
Preload("GlueOpsLoadBalancer").
|
|
Preload("BastionServer").
|
|
Preload("NodePools").
|
|
Preload("NodePools.Labels").
|
|
Preload("NodePools.Annotations").
|
|
Preload("NodePools.Taints").
|
|
Preload("NodePools.Servers").
|
|
First(&cluster, "id = ?", cluster.ID).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
|
|
}
|
|
}
|
|
|
|
// AttachAppsLoadBalancer godoc
|
|
//
|
|
// @ID AttachAppsLoadBalancer
|
|
// @Summary Attach an apps load balancer to a cluster
|
|
// @Description Sets apps_load_balancer_id on the cluster.
|
|
// @Tags Clusters
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param X-Org-ID header string false "Organization UUID"
|
|
// @Param clusterID path string true "Cluster ID"
|
|
// @Param body body dto.AttachLoadBalancerRequest true "payload"
|
|
// @Success 200 {object} dto.ClusterResponse
|
|
// @Failure 400 {string} string "bad request"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 404 {string} string "cluster or load balancer not found"
|
|
// @Failure 500 {string} string "db error"
|
|
// @Router /clusters/{clusterID}/apps-load-balancer [post]
|
|
// @Security BearerAuth
|
|
// @Security OrgKeyAuth
|
|
// @Security OrgSecretAuth
|
|
func AttachAppsLoadBalancer(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
|
|
}
|
|
|
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
|
return
|
|
}
|
|
|
|
var in dto.AttachLoadBalancerRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
|
return
|
|
}
|
|
|
|
var cluster models.Cluster
|
|
if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
var lb models.LoadBalancer
|
|
if err := db.Where("id = ? AND organization_id = ?", in.LoadBalancerID, orgID).First(&lb).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "lb_not_found", "load balancer not found for organization")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
cluster.AppsLoadBalancerID = &lb.ID
|
|
if err := db.Save(&cluster).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
_ = markClusterNeedsValidation(db, cluster.ID)
|
|
|
|
if err := db.Preload("CaptainDomain").
|
|
Preload("ControlPlaneRecordSet").
|
|
Preload("AppsLoadBalancer").
|
|
Preload("GlueOpsLoadBalancer").
|
|
Preload("BastionServer").
|
|
Preload("NodePools").
|
|
Preload("NodePools.Labels").
|
|
Preload("NodePools.Annotations").
|
|
Preload("NodePools.Taints").
|
|
Preload("NodePools.Servers").
|
|
First(&cluster, "id = ?", cluster.ID).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
|
|
}
|
|
}
|
|
|
|
// DetachAppsLoadBalancer godoc
|
|
//
|
|
// @ID DetachAppsLoadBalancer
|
|
// @Summary Detach the apps load balancer from a cluster
|
|
// @Description Clears apps_load_balancer_id on the cluster.
|
|
// @Tags Clusters
|
|
// @Produce json
|
|
// @Param X-Org-ID header string false "Organization UUID"
|
|
// @Param clusterID path string true "Cluster ID"
|
|
// @Success 200 {object} dto.ClusterResponse
|
|
// @Failure 400 {string} string "bad request"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 404 {string} string "cluster not found"
|
|
// @Failure 500 {string} string "db error"
|
|
// @Router /clusters/{clusterID}/apps-load-balancer [delete]
|
|
// @Security BearerAuth
|
|
// @Security OrgKeyAuth
|
|
// @Security OrgSecretAuth
|
|
func DetachAppsLoadBalancer(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
|
|
}
|
|
|
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
|
return
|
|
}
|
|
|
|
var cluster models.Cluster
|
|
if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
cluster.AppsLoadBalancerID = nil
|
|
if err := db.Save(&cluster).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
_ = markClusterNeedsValidation(db, cluster.ID)
|
|
|
|
if err := db.Preload("CaptainDomain").
|
|
Preload("ControlPlaneRecordSet").
|
|
Preload("AppsLoadBalancer").
|
|
Preload("GlueOpsLoadBalancer").
|
|
Preload("BastionServer").
|
|
Preload("NodePools").
|
|
Preload("NodePools.Labels").
|
|
Preload("NodePools.Annotations").
|
|
Preload("NodePools.Taints").
|
|
Preload("NodePools.Servers").
|
|
First(&cluster, "id = ?", cluster.ID).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
|
|
}
|
|
}
|
|
|
|
// AttachGlueOpsLoadBalancer godoc
|
|
//
|
|
// @ID AttachGlueOpsLoadBalancer
|
|
// @Summary Attach a GlueOps/control-plane load balancer to a cluster
|
|
// @Description Sets glueops_load_balancer_id on the cluster.
|
|
// @Tags Clusters
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param X-Org-ID header string false "Organization UUID"
|
|
// @Param clusterID path string true "Cluster ID"
|
|
// @Param body body dto.AttachLoadBalancerRequest true "payload"
|
|
// @Success 200 {object} dto.ClusterResponse
|
|
// @Failure 400 {string} string "bad request"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 404 {string} string "cluster or load balancer not found"
|
|
// @Failure 500 {string} string "db error"
|
|
// @Router /clusters/{clusterID}/glueops-load-balancer [post]
|
|
// @Security BearerAuth
|
|
// @Security OrgKeyAuth
|
|
// @Security OrgSecretAuth
|
|
func AttachGlueOpsLoadBalancer(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
|
|
}
|
|
|
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
|
return
|
|
}
|
|
|
|
var in dto.AttachLoadBalancerRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
|
return
|
|
}
|
|
|
|
var cluster models.Cluster
|
|
if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
var lb models.LoadBalancer
|
|
if err := db.Where("id = ? AND organization_id = ?", in.LoadBalancerID, orgID).First(&lb).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "lb_not_found", "load balancer not found for organization")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
cluster.GlueOpsLoadBalancerID = &lb.ID
|
|
if err := db.Save(&cluster).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
_ = markClusterNeedsValidation(db, cluster.ID)
|
|
|
|
if err := db.Preload("CaptainDomain").
|
|
Preload("ControlPlaneRecordSet").
|
|
Preload("AppsLoadBalancer").
|
|
Preload("GlueOpsLoadBalancer").
|
|
Preload("BastionServer").
|
|
Preload("NodePools").
|
|
Preload("NodePools.Labels").
|
|
Preload("NodePools.Annotations").
|
|
Preload("NodePools.Taints").
|
|
Preload("NodePools.Servers").
|
|
First(&cluster, "id = ?", cluster.ID).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
|
|
}
|
|
}
|
|
|
|
// DetachGlueOpsLoadBalancer godoc
|
|
//
|
|
// @ID DetachGlueOpsLoadBalancer
|
|
// @Summary Detach the GlueOps/control-plane load balancer from a cluster
|
|
// @Description Clears glueops_load_balancer_id on the cluster.
|
|
// @Tags Clusters
|
|
// @Produce json
|
|
// @Param X-Org-ID header string false "Organization UUID"
|
|
// @Param clusterID path string true "Cluster ID"
|
|
// @Success 200 {object} dto.ClusterResponse
|
|
// @Failure 400 {string} string "bad request"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 404 {string} string "cluster not found"
|
|
// @Failure 500 {string} string "db error"
|
|
// @Router /clusters/{clusterID}/glueops-load-balancer [delete]
|
|
// @Security BearerAuth
|
|
// @Security OrgKeyAuth
|
|
// @Security OrgSecretAuth
|
|
func DetachGlueOpsLoadBalancer(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
|
|
}
|
|
|
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
|
return
|
|
}
|
|
|
|
var cluster models.Cluster
|
|
if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
cluster.GlueOpsLoadBalancerID = nil
|
|
if err := db.Save(&cluster).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
_ = markClusterNeedsValidation(db, cluster.ID)
|
|
|
|
if err := db.Preload("CaptainDomain").
|
|
Preload("ControlPlaneRecordSet").
|
|
Preload("AppsLoadBalancer").
|
|
Preload("GlueOpsLoadBalancer").
|
|
Preload("BastionServer").
|
|
Preload("NodePools").
|
|
Preload("NodePools.Labels").
|
|
Preload("NodePools.Annotations").
|
|
Preload("NodePools.Taints").
|
|
Preload("NodePools.Servers").
|
|
First(&cluster, "id = ?", cluster.ID).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
|
|
}
|
|
}
|
|
|
|
// AttachBastionServer godoc
|
|
//
|
|
// @ID AttachBastionServer
|
|
// @Summary Attach a bastion server to a cluster
|
|
// @Description Sets bastion_server_id on the cluster.
|
|
// @Tags Clusters
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param X-Org-ID header string false "Organization UUID"
|
|
// @Param clusterID path string true "Cluster ID"
|
|
// @Param body body dto.AttachBastionRequest true "payload"
|
|
// @Success 200 {object} dto.ClusterResponse
|
|
// @Failure 400 {string} string "bad request"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 404 {string} string "cluster or server not found"
|
|
// @Failure 500 {string} string "db error"
|
|
// @Router /clusters/{clusterID}/bastion [post]
|
|
// @Security BearerAuth
|
|
// @Security OrgKeyAuth
|
|
// @Security OrgSecretAuth
|
|
func AttachBastionServer(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
|
|
}
|
|
|
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
|
return
|
|
}
|
|
|
|
var in dto.AttachBastionRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
|
return
|
|
}
|
|
|
|
var cluster models.Cluster
|
|
if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
var server models.Server
|
|
if err := db.Where("id = ? AND organization_id = ?", in.ServerID, orgID).First(&server).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "server_not_found", "server not found for organization")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
cluster.BastionServerID = &server.ID
|
|
if err := db.Save(&cluster).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
_ = markClusterNeedsValidation(db, cluster.ID)
|
|
|
|
if err := db.Preload("CaptainDomain").
|
|
Preload("ControlPlaneRecordSet").
|
|
Preload("AppsLoadBalancer").
|
|
Preload("GlueOpsLoadBalancer").
|
|
Preload("BastionServer").
|
|
Preload("NodePools").
|
|
Preload("NodePools.Labels").
|
|
Preload("NodePools.Annotations").
|
|
Preload("NodePools.Taints").
|
|
Preload("NodePools.Servers").
|
|
First(&cluster, "id = ?", cluster.ID).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
|
|
}
|
|
}
|
|
|
|
// DetachBastionServer godoc
|
|
//
|
|
// @ID DetachBastionServer
|
|
// @Summary Detach the bastion server from a cluster
|
|
// @Description Clears bastion_server_id on the cluster.
|
|
// @Tags Clusters
|
|
// @Produce json
|
|
// @Param X-Org-ID header string false "Organization UUID"
|
|
// @Param clusterID path string true "Cluster ID"
|
|
// @Success 200 {object} dto.ClusterResponse
|
|
// @Failure 400 {string} string "bad request"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 404 {string} string "cluster not found"
|
|
// @Failure 500 {string} string "db error"
|
|
// @Router /clusters/{clusterID}/bastion [delete]
|
|
// @Security BearerAuth
|
|
// @Security OrgKeyAuth
|
|
// @Security OrgSecretAuth
|
|
func DetachBastionServer(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
|
|
}
|
|
|
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
|
return
|
|
}
|
|
|
|
var cluster models.Cluster
|
|
if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
cluster.BastionServerID = nil
|
|
if err := db.Save(&cluster).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
_ = markClusterNeedsValidation(db, cluster.ID)
|
|
|
|
if err := db.Preload("CaptainDomain").
|
|
Preload("ControlPlaneRecordSet").
|
|
Preload("AppsLoadBalancer").
|
|
Preload("GlueOpsLoadBalancer").
|
|
Preload("BastionServer").
|
|
Preload("NodePools").
|
|
Preload("NodePools.Labels").
|
|
Preload("NodePools.Annotations").
|
|
Preload("NodePools.Taints").
|
|
Preload("NodePools.Servers").
|
|
First(&cluster, "id = ?", cluster.ID).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
|
|
}
|
|
}
|
|
|
|
// SetClusterKubeconfig godoc
|
|
//
|
|
// @ID SetClusterKubeconfig
|
|
// @Summary Set (or replace) the kubeconfig for a cluster
|
|
// @Description Stores the kubeconfig encrypted per organization. The kubeconfig is never returned in responses.
|
|
// @Tags Clusters
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param X-Org-ID header string false "Organization UUID"
|
|
// @Param clusterID path string true "Cluster ID"
|
|
// @Param body body dto.SetKubeconfigRequest true "payload"
|
|
// @Success 200 {object} dto.ClusterResponse
|
|
// @Failure 400 {string} string "bad request"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 404 {string} string "cluster not found"
|
|
// @Failure 500 {string} string "db error"
|
|
// @Router /clusters/{clusterID}/kubeconfig [post]
|
|
// @Security BearerAuth
|
|
// @Security OrgKeyAuth
|
|
// @Security OrgSecretAuth
|
|
func SetClusterKubeconfig(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
|
|
}
|
|
|
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
|
return
|
|
}
|
|
|
|
var in dto.SetKubeconfigRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
|
return
|
|
}
|
|
|
|
var cluster models.Cluster
|
|
if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
ct, iv, tag, err := utils.EncryptForOrg(orgID, []byte(in.Kubeconfig), db)
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "encryption_error", "failed to encrypt kubeconfig")
|
|
return
|
|
}
|
|
|
|
cluster.EncryptedKubeconfig = ct
|
|
cluster.KubeIV = iv
|
|
cluster.KubeTag = tag
|
|
|
|
if err := db.Save(&cluster).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
_ = markClusterNeedsValidation(db, cluster.ID)
|
|
|
|
if err := db.Preload("CaptainDomain").
|
|
Preload("ControlPlaneRecordSet").
|
|
Preload("AppsLoadBalancer").
|
|
Preload("GlueOpsLoadBalancer").
|
|
Preload("BastionServer").
|
|
Preload("NodePools").
|
|
Preload("NodePools.Labels").
|
|
Preload("NodePools.Annotations").
|
|
Preload("NodePools.Taints").
|
|
Preload("NodePools.Servers").
|
|
First(&cluster, "id = ?", cluster.ID).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
|
|
}
|
|
}
|
|
|
|
// ClearClusterKubeconfig godoc
|
|
//
|
|
// @ID ClearClusterKubeconfig
|
|
// @Summary Clear the kubeconfig for a cluster
|
|
// @Description Removes the encrypted kubeconfig, IV, and tag from the cluster record.
|
|
// @Tags Clusters
|
|
// @Produce json
|
|
// @Param X-Org-ID header string false "Organization UUID"
|
|
// @Param clusterID path string true "Cluster ID"
|
|
// @Success 200 {object} dto.ClusterResponse
|
|
// @Failure 400 {string} string "bad request"
|
|
// @Failure 401 {string} string "Unauthorized"
|
|
// @Failure 403 {string} string "organization required"
|
|
// @Failure 404 {string} string "cluster not found"
|
|
// @Failure 500 {string} string "db error"
|
|
// @Router /clusters/{clusterID}/kubeconfig [delete]
|
|
// @Security BearerAuth
|
|
// @Security OrgKeyAuth
|
|
// @Security OrgSecretAuth
|
|
func ClearClusterKubeconfig(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
|
|
}
|
|
|
|
clusterID, err := uuid.Parse(chi.URLParam(r, "clusterID"))
|
|
if err != nil {
|
|
utils.WriteError(w, http.StatusBadRequest, "bad_cluster_id", "invalid cluster id")
|
|
return
|
|
}
|
|
|
|
var cluster models.Cluster
|
|
if err := db.Where("id = ? AND organization_id = ?", clusterID, orgID).First(&cluster).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
utils.WriteError(w, http.StatusNotFound, "not_found", "cluster not found")
|
|
return
|
|
}
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
cluster.EncryptedKubeconfig = ""
|
|
cluster.KubeIV = ""
|
|
cluster.KubeTag = ""
|
|
|
|
if err := db.Save(&cluster).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
_ = markClusterNeedsValidation(db, cluster.ID)
|
|
|
|
if err := db.Preload("CaptainDomain").
|
|
Preload("ControlPlaneRecordSet").
|
|
Preload("AppsLoadBalancer").
|
|
Preload("GlueOpsLoadBalancer").
|
|
Preload("BastionServer").
|
|
Preload("NodePools").
|
|
Preload("NodePools.Labels").
|
|
Preload("NodePools.Annotations").
|
|
Preload("NodePools.Taints").
|
|
Preload("NodePools.Servers").
|
|
First(&cluster, "id = ?", cluster.ID).Error; err != nil {
|
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
|
return
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusOK, clusterToDTO(cluster))
|
|
}
|
|
}
|
|
|
|
// -- Helpers
|
|
|
|
func clusterToDTO(c models.Cluster) dto.ClusterResponse {
|
|
var bastion *dto.ServerResponse
|
|
if c.BastionServer != nil {
|
|
b := serverToDTO(*c.BastionServer)
|
|
bastion = &b
|
|
}
|
|
|
|
var captainDomain *dto.DomainResponse
|
|
if c.CaptainDomainID != nil && c.CaptainDomain.ID != uuid.Nil {
|
|
dr := domainToDTO(c.CaptainDomain)
|
|
captainDomain = &dr
|
|
}
|
|
|
|
var controlPlane *dto.RecordSetResponse
|
|
if c.ControlPlaneRecordSet != nil {
|
|
rr := recordSetToDTO(*c.ControlPlaneRecordSet)
|
|
controlPlane = &rr
|
|
}
|
|
|
|
var appsLB *dto.LoadBalancerResponse
|
|
if c.AppsLoadBalancer != nil {
|
|
lr := loadBalancerToDTO(*c.AppsLoadBalancer)
|
|
appsLB = &lr
|
|
}
|
|
|
|
var glueOpsLB *dto.LoadBalancerResponse
|
|
if c.GlueOpsLoadBalancer != nil {
|
|
lr := loadBalancerToDTO(*c.GlueOpsLoadBalancer)
|
|
glueOpsLB = &lr
|
|
}
|
|
|
|
nps := make([]dto.NodePoolResponse, 0, len(c.NodePools))
|
|
for _, np := range c.NodePools {
|
|
nps = append(nps, nodePoolToDTO(np))
|
|
}
|
|
|
|
return dto.ClusterResponse{
|
|
ID: c.ID,
|
|
Name: c.Name,
|
|
CaptainDomain: captainDomain,
|
|
ControlPlaneRecordSet: controlPlane,
|
|
AppsLoadBalancer: appsLB,
|
|
GlueOpsLoadBalancer: glueOpsLB,
|
|
BastionServer: bastion,
|
|
Provider: c.Provider,
|
|
Region: c.Region,
|
|
Status: c.Status,
|
|
LastError: c.LastError,
|
|
RandomToken: c.RandomToken,
|
|
CertificateKey: c.CertificateKey,
|
|
NodePools: nps,
|
|
CreatedAt: c.CreatedAt,
|
|
UpdatedAt: c.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func nodePoolToDTO(np models.NodePool) dto.NodePoolResponse {
|
|
labels := make([]dto.LabelResponse, 0, len(np.Labels))
|
|
for _, l := range np.Labels {
|
|
labels = append(labels, dto.LabelResponse{
|
|
Key: l.Key,
|
|
Value: l.Value,
|
|
})
|
|
}
|
|
|
|
annotations := make([]dto.AnnotationResponse, 0, len(np.Annotations))
|
|
for _, a := range np.Annotations {
|
|
annotations = append(annotations, dto.AnnotationResponse{
|
|
Key: a.Key,
|
|
Value: a.Value,
|
|
})
|
|
}
|
|
|
|
taints := make([]dto.TaintResponse, 0, len(np.Taints))
|
|
for _, t := range np.Taints {
|
|
taints = append(taints, dto.TaintResponse{
|
|
Key: t.Key,
|
|
Value: t.Value,
|
|
Effect: t.Effect,
|
|
})
|
|
}
|
|
|
|
servers := make([]dto.ServerResponse, 0, len(np.Servers))
|
|
for _, s := range np.Servers {
|
|
servers = append(servers, serverToDTO(s))
|
|
}
|
|
|
|
return dto.NodePoolResponse{
|
|
AuditFields: common.AuditFields{
|
|
ID: np.ID,
|
|
OrganizationID: np.OrganizationID,
|
|
CreatedAt: np.CreatedAt,
|
|
UpdatedAt: np.UpdatedAt,
|
|
},
|
|
Name: np.Name,
|
|
Role: dto.NodeRole(np.Role),
|
|
Labels: labels,
|
|
Annotations: annotations,
|
|
Taints: taints,
|
|
Servers: servers,
|
|
}
|
|
}
|
|
|
|
func serverToDTO(s models.Server) dto.ServerResponse {
|
|
return dto.ServerResponse{
|
|
ID: s.ID,
|
|
Hostname: s.Hostname,
|
|
PrivateIPAddress: s.PrivateIPAddress,
|
|
PublicIPAddress: s.PublicIPAddress,
|
|
Role: s.Role,
|
|
Status: s.Status,
|
|
SSHUser: s.SSHUser,
|
|
SshKeyID: s.SshKeyID,
|
|
CreatedAt: s.CreatedAt.UTC().Format(time.RFC3339),
|
|
UpdatedAt: s.UpdatedAt.UTC().Format(time.RFC3339),
|
|
}
|
|
}
|
|
|
|
func domainToDTO(d models.Domain) dto.DomainResponse {
|
|
return dto.DomainResponse{
|
|
ID: d.ID.String(),
|
|
OrganizationID: d.OrganizationID.String(),
|
|
DomainName: d.DomainName,
|
|
ZoneID: d.ZoneID,
|
|
Status: d.Status,
|
|
LastError: d.LastError,
|
|
CredentialID: d.CredentialID.String(),
|
|
CreatedAt: d.CreatedAt.UTC().Format(time.RFC3339),
|
|
UpdatedAt: d.UpdatedAt.UTC().Format(time.RFC3339),
|
|
}
|
|
}
|
|
|
|
func recordSetToDTO(rs models.RecordSet) dto.RecordSetResponse {
|
|
return dto.RecordSetResponse{
|
|
ID: rs.ID.String(),
|
|
DomainID: rs.DomainID.String(),
|
|
Name: rs.Name,
|
|
Type: rs.Type,
|
|
TTL: rs.TTL,
|
|
Values: []byte(rs.Values),
|
|
Fingerprint: rs.Fingerprint,
|
|
Status: rs.Status,
|
|
Owner: rs.Owner,
|
|
LastError: rs.LastError,
|
|
CreatedAt: rs.CreatedAt.UTC().Format(time.RFC3339),
|
|
UpdatedAt: rs.UpdatedAt.UTC().Format(time.RFC3339),
|
|
}
|
|
}
|
|
|
|
func loadBalancerToDTO(lb models.LoadBalancer) dto.LoadBalancerResponse {
|
|
return dto.LoadBalancerResponse{
|
|
ID: lb.ID,
|
|
OrganizationID: lb.OrganizationID,
|
|
Name: lb.Name,
|
|
Kind: lb.Kind,
|
|
PublicIPAddress: lb.PublicIPAddress,
|
|
PrivateIPAddress: lb.PrivateIPAddress,
|
|
CreatedAt: lb.CreatedAt,
|
|
UpdatedAt: lb.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func GenerateSecureHex(n int) (string, error) {
|
|
bytes := make([]byte, n)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", fmt.Errorf("failed to generate random bytes: %w", err)
|
|
}
|
|
return hex.EncodeToString(bytes), nil
|
|
}
|
|
|
|
func GenerateFormattedToken() (string, error) {
|
|
part1, err := GenerateSecureHex(3)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to generate token part 1: %w", err)
|
|
}
|
|
part2, err := GenerateSecureHex(8)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to generate token part 2: %w", err)
|
|
}
|
|
return fmt.Sprintf("%s.%s", part1, part2), nil
|
|
}
|
|
|
|
func markClusterNeedsValidation(db *gorm.DB, clusterID uuid.UUID) error {
|
|
return db.Model(&models.Cluster{}).Where("id = ?", clusterID).Updates(map[string]any{
|
|
"status": models.ClusterStatusPrePending,
|
|
"last_error": "",
|
|
}).Error
|
|
}
|