feat: add credentials management

Signed-off-by: allanice001 <allanice001@gmail.com>
This commit is contained in:
allanice001
2025-11-09 21:46:31 +00:00
parent 56ea963b47
commit bc72df3c9a
43 changed files with 4671 additions and 15 deletions

View File

@@ -0,0 +1,186 @@
package handlers
import (
"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"
"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("NodePools").
Preload("NodePools.Labels").
Preload("NodePools.Annotations").
Preload("NodePools.Labels").
Preload("NodePools.Taints").
Preload("NodePools.Servers").
Preload("BastionServer").
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))
}
}
}
// CreateCluster godoc
//
// @ID CreateCluster
// @Summary Create cluster (org scoped)
// @Description Creates a cluster. If `kubeconfig` is provided, it will be encrypted per-organization and stored securely (never returned).
// @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) {
w.WriteHeader(http.StatusNoContent)
}
}
// -- Helpers
func clusterToDTO(c models.Cluster) dto.ClusterResponse {
var bastion *dto.ServerResponse
if c.BastionServer != nil {
b := serverToDTO(*c.BastionServer)
bastion = &b
}
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,
Provider: c.Provider,
Region: c.Region,
Status: c.Status,
CaptainDomain: c.CaptainDomain,
ClusterLoadBalancer: c.ClusterLoadBalancer,
RandomToken: c.RandomToken,
CertificateKey: c.CertificateKey,
ControlLoadBalancer: c.ControlLoadBalancer,
NodePools: nps,
BastionServer: bastion,
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),
}
}