mirror of
https://github.com/GlueOps/autoglue.git
synced 2026-02-13 04:40:05 +01:00
feat: cluster page ui
Signed-off-by: allanice001 <allanice001@gmail.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -46,6 +46,11 @@ components:
|
|||||||
load_balancer_id:
|
load_balancer_id:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
dto.AttachNodePoolRequest:
|
||||||
|
properties:
|
||||||
|
node_pool_id:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
dto.AttachRecordSetRequest:
|
dto.AttachRecordSetRequest:
|
||||||
properties:
|
properties:
|
||||||
record_set_id:
|
record_set_id:
|
||||||
@@ -1154,7 +1159,7 @@ info:
|
|||||||
name: GlueOps
|
name: GlueOps
|
||||||
description: API for managing K3s clusters across cloud providers
|
description: API for managing K3s clusters across cloud providers
|
||||||
title: AutoGlue API
|
title: AutoGlue API
|
||||||
version: "1.0"
|
version: ""
|
||||||
openapi: 3.1.0
|
openapi: 3.1.0
|
||||||
paths:
|
paths:
|
||||||
/.well-known/jwks.json:
|
/.well-known/jwks.json:
|
||||||
@@ -2853,6 +2858,139 @@ paths:
|
|||||||
summary: Set (or replace) the kubeconfig for a cluster
|
summary: Set (or replace) the kubeconfig for a cluster
|
||||||
tags:
|
tags:
|
||||||
- Clusters
|
- Clusters
|
||||||
|
/clusters/{clusterID}/node-pools:
|
||||||
|
post:
|
||||||
|
description: Adds an entry in the cluster_node_pools join table.
|
||||||
|
operationId: AttachNodePool
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- description: Cluster ID
|
||||||
|
in: path
|
||||||
|
name: clusterID
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/dto.AttachNodePoolRequest'
|
||||||
|
description: payload
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/dto.ClusterResponse'
|
||||||
|
description: OK
|
||||||
|
"400":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: bad request
|
||||||
|
"401":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Unauthorized
|
||||||
|
"403":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: organization required
|
||||||
|
"404":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: cluster or node pool not found
|
||||||
|
"500":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: db error
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
- OrgKeyAuth: []
|
||||||
|
- OrgSecretAuth: []
|
||||||
|
summary: Attach a node pool to a cluster
|
||||||
|
tags:
|
||||||
|
- Clusters
|
||||||
|
/clusters/{clusterID}/node-pools/{nodePoolID}:
|
||||||
|
delete:
|
||||||
|
description: Removes an entry from the cluster_node_pools join table.
|
||||||
|
operationId: DetachNodePool
|
||||||
|
parameters:
|
||||||
|
- description: Organization UUID
|
||||||
|
in: header
|
||||||
|
name: X-Org-ID
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- description: Cluster ID
|
||||||
|
in: path
|
||||||
|
name: clusterID
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- description: Node Pool ID
|
||||||
|
in: path
|
||||||
|
name: nodePoolID
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/dto.ClusterResponse'
|
||||||
|
description: OK
|
||||||
|
"400":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: bad request
|
||||||
|
"401":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Unauthorized
|
||||||
|
"403":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: organization required
|
||||||
|
"404":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: cluster or node pool not found
|
||||||
|
"500":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: db error
|
||||||
|
security:
|
||||||
|
- BearerAuth: []
|
||||||
|
- OrgKeyAuth: []
|
||||||
|
- OrgSecretAuth: []
|
||||||
|
summary: Detach a node pool from a cluster
|
||||||
|
tags:
|
||||||
|
- Clusters
|
||||||
/credentials:
|
/credentials:
|
||||||
get:
|
get:
|
||||||
description: Returns credential metadata for the current org. Secrets are never
|
description: Returns credential metadata for the current org. Secrets are never
|
||||||
|
|||||||
@@ -35,5 +35,7 @@ func mountClusterRoutes(r chi.Router, db *gorm.DB, authOrg func(http.Handler) ht
|
|||||||
c.Post("/{clusterID}/kubeconfig", handlers.SetClusterKubeconfig(db))
|
c.Post("/{clusterID}/kubeconfig", handlers.SetClusterKubeconfig(db))
|
||||||
c.Delete("/{clusterID}/kubeconfig", handlers.ClearClusterKubeconfig(db))
|
c.Delete("/{clusterID}/kubeconfig", handlers.ClearClusterKubeconfig(db))
|
||||||
|
|
||||||
|
c.Post("/{clusterID}/node-pools", handlers.AttachNodePool(db))
|
||||||
|
c.Delete("/{clusterID}/node-pools/{nodePoolID}", handlers.DeleteNodePool(db))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1294,6 +1294,199 @@ func ClearClusterKubeconfig(db *gorm.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AttachNodePool godoc
|
||||||
|
//
|
||||||
|
// @ID AttachNodePool
|
||||||
|
// @Summary Attach a node pool to a cluster
|
||||||
|
// @Description Adds an entry in the cluster_node_pools join table.
|
||||||
|
// @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.AttachNodePoolRequest 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 node pool not found"
|
||||||
|
// @Failure 500 {string} string "db error"
|
||||||
|
// @Router /clusters/{clusterID}/node-pools [post]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func AttachNodePool(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.AttachNodePoolRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_json", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load cluster (org scoped)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load node pool (org scoped)
|
||||||
|
var np models.NodePool
|
||||||
|
if err := db.
|
||||||
|
Where("id = ? AND organization_id = ?", in.NodePoolID, orgID).
|
||||||
|
First(&np).Error; err != nil {
|
||||||
|
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "nodepool_not_found", "node pool not found for organization")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create association in join table
|
||||||
|
if err := db.Model(&cluster).Association("NodePools").Append(&np); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to attach node pool")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = markClusterNeedsValidation(db, cluster.ID)
|
||||||
|
|
||||||
|
// Reload for 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetachNodePool godoc
|
||||||
|
//
|
||||||
|
// @ID DetachNodePool
|
||||||
|
// @Summary Detach a node pool from a cluster
|
||||||
|
// @Description Removes an entry from the cluster_node_pools join table.
|
||||||
|
// @Tags Clusters
|
||||||
|
// @Produce json
|
||||||
|
// @Param X-Org-ID header string false "Organization UUID"
|
||||||
|
// @Param clusterID path string true "Cluster ID"
|
||||||
|
// @Param nodePoolID path string true "Node Pool 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 or node pool not found"
|
||||||
|
// @Failure 500 {string} string "db error"
|
||||||
|
// @Router /clusters/{clusterID}/node-pools/{nodePoolID} [delete]
|
||||||
|
// @Security BearerAuth
|
||||||
|
// @Security OrgKeyAuth
|
||||||
|
// @Security OrgSecretAuth
|
||||||
|
func DetachNodePool(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
|
||||||
|
}
|
||||||
|
|
||||||
|
nodePoolID, err := uuid.Parse(chi.URLParam(r, "nodePoolID"))
|
||||||
|
if err != nil {
|
||||||
|
utils.WriteError(w, http.StatusBadRequest, "bad_nodepool_id", "invalid node pool 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
|
||||||
|
}
|
||||||
|
|
||||||
|
var np models.NodePool
|
||||||
|
if err := db.
|
||||||
|
Where("id = ? AND organization_id = ?", nodePoolID, orgID).
|
||||||
|
First(&np).Error; err != nil {
|
||||||
|
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
utils.WriteError(w, http.StatusNotFound, "nodepool_not_found", "node pool not found for organization")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "db error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Model(&cluster).Association("NodePools").Delete(&np); err != nil {
|
||||||
|
utils.WriteError(w, http.StatusInternalServerError, "db_error", "failed to detach node pool")
|
||||||
|
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
|
// -- Helpers
|
||||||
|
|
||||||
func clusterToDTO(c models.Cluster) dto.ClusterResponse {
|
func clusterToDTO(c models.Cluster) dto.ClusterResponse {
|
||||||
|
|||||||
@@ -56,3 +56,7 @@ type AttachBastionRequest struct {
|
|||||||
type SetKubeconfigRequest struct {
|
type SetKubeconfigRequest struct {
|
||||||
Kubeconfig string `json:"kubeconfig"`
|
Kubeconfig string `json:"kubeconfig"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AttachNodePoolRequest struct {
|
||||||
|
NodePoolID uuid.UUID `json:"node_pool_id"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,17 +16,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Cluster struct {
|
type Cluster struct {
|
||||||
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
|
ID uuid.UUID `gorm:"type:uuid;default:gen_random_uuid();primaryKey" json:"id"`
|
||||||
OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"`
|
OrganizationID uuid.UUID `gorm:"type:uuid;not null" json:"organization_id"`
|
||||||
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
Organization Organization `gorm:"foreignKey:OrganizationID;constraint:OnDelete:CASCADE" json:"organization"`
|
||||||
|
Name string `gorm:"not null" json:"name"`
|
||||||
Name string `gorm:"not null" json:"name"`
|
Provider string `json:"provider"`
|
||||||
Provider string `json:"provider"`
|
Region string `json:"region"`
|
||||||
Region string `json:"region"`
|
Status string `gorm:"type:varchar(20);not null;default:'pre_pending'" json:"status"`
|
||||||
|
LastError string `gorm:"type:text;not null;default:''" json:"last_error"`
|
||||||
Status string `gorm:"type:varchar(20);not null;default:'pre_pending'" json:"status"`
|
|
||||||
LastError string `gorm:"type:text;not null;default:''" json:"last_error"`
|
|
||||||
|
|
||||||
CaptainDomainID *uuid.UUID `gorm:"type:uuid" json:"captain_domain_id"`
|
CaptainDomainID *uuid.UUID `gorm:"type:uuid" json:"captain_domain_id"`
|
||||||
CaptainDomain Domain `gorm:"foreignKey:CaptainDomainID" json:"captain_domain"`
|
CaptainDomain Domain `gorm:"foreignKey:CaptainDomainID" json:"captain_domain"`
|
||||||
ControlPlaneRecordSetID *uuid.UUID `gorm:"type:uuid" json:"control_plane_record_set_id,omitempty"`
|
ControlPlaneRecordSetID *uuid.UUID `gorm:"type:uuid" json:"control_plane_record_set_id,omitempty"`
|
||||||
@@ -37,16 +34,12 @@ type Cluster struct {
|
|||||||
GlueOpsLoadBalancer *LoadBalancer `gorm:"foreignKey:GlueOpsLoadBalancerID" json:"glueops_load_balancer,omitempty"`
|
GlueOpsLoadBalancer *LoadBalancer `gorm:"foreignKey:GlueOpsLoadBalancerID" json:"glueops_load_balancer,omitempty"`
|
||||||
BastionServerID *uuid.UUID `gorm:"type:uuid" json:"bastion_server_id,omitempty"`
|
BastionServerID *uuid.UUID `gorm:"type:uuid" json:"bastion_server_id,omitempty"`
|
||||||
BastionServer *Server `gorm:"foreignKey:BastionServerID" json:"bastion_server,omitempty"`
|
BastionServer *Server `gorm:"foreignKey:BastionServerID" json:"bastion_server,omitempty"`
|
||||||
|
NodePools []NodePool `gorm:"many2many:cluster_node_pools;constraint:OnDelete:CASCADE" json:"node_pools,omitempty"`
|
||||||
NodePools []NodePool `gorm:"many2many:cluster_node_pools;constraint:OnDelete:CASCADE" json:"node_pools,omitempty"`
|
RandomToken string `json:"random_token"`
|
||||||
|
CertificateKey string `json:"certificate_key"`
|
||||||
RandomToken string `json:"random_token"`
|
EncryptedKubeconfig string `gorm:"type:text" json:"-"`
|
||||||
CertificateKey string `json:"certificate_key"`
|
KubeIV string `json:"-"`
|
||||||
|
KubeTag string `json:"-"`
|
||||||
EncryptedKubeconfig string `gorm:"type:text" json:"-"`
|
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"`
|
||||||
KubeIV string `json:"-"`
|
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"`
|
||||||
KubeTag string `json:"-"`
|
|
||||||
|
|
||||||
CreatedAt time.Time `json:"created_at,omitempty" gorm:"type:timestamptz;column:created_at;not null;default:now()"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at,omitempty" gorm:"type:timestamptz;autoUpdateTime;column:updated_at;not null;default:now()"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
5
main.go
5
main.go
@@ -2,10 +2,12 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/glueops/autoglue/cmd"
|
"github.com/glueops/autoglue/cmd"
|
||||||
|
"github.com/glueops/autoglue/docs"
|
||||||
|
"github.com/glueops/autoglue/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @title AutoGlue API
|
// @title AutoGlue API
|
||||||
// @version 1.0
|
// @version dev
|
||||||
// @description API for managing K3s clusters across cloud providers
|
// @description API for managing K3s clusters across cloud providers
|
||||||
// @contact.name GlueOps
|
// @contact.name GlueOps
|
||||||
|
|
||||||
@@ -37,5 +39,6 @@ import (
|
|||||||
// @description Org-level secret
|
// @description Org-level secret
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
docs.SwaggerInfo.Version = version.Version
|
||||||
cmd.Execute()
|
cmd.Execute()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,23 @@ import { AppShell } from "@/layouts/app-shell.tsx"
|
|||||||
import { Route, Routes } from "react-router-dom"
|
import { Route, Routes } from "react-router-dom"
|
||||||
|
|
||||||
import { ProtectedRoute } from "@/components/protected-route.tsx"
|
import { ProtectedRoute } from "@/components/protected-route.tsx"
|
||||||
import { AnnotationPage } from "@/pages/annotations/annotation-page.tsx"
|
import { AnnotationPage } from "@/pages/annotation-page.tsx"
|
||||||
import { Login } from "@/pages/auth/login.tsx"
|
import { ClustersPage } from "@/pages/cluster-page"
|
||||||
import { CredentialPage } from "@/pages/credentials/credential-page.tsx"
|
import { CredentialPage } from "@/pages/credential-page.tsx"
|
||||||
import { DnsPage } from "@/pages/dns/dns-page.tsx"
|
import { DnsPage } from "@/pages/dns-page.tsx"
|
||||||
import { DocsPage } from "@/pages/docs/docs-page.tsx"
|
import { DocsPage } from "@/pages/docs-page.tsx"
|
||||||
import { JobsPage } from "@/pages/jobs/jobs-page.tsx"
|
import { JobsPage } from "@/pages/jobs-page.tsx"
|
||||||
import { LabelsPage } from "@/pages/labels/labels-page.tsx"
|
import { LabelsPage } from "@/pages/labels-page.tsx"
|
||||||
import { LoadBalancersPage } from "@/pages/loadbalancers/load-balancers-page"
|
import { LoadBalancersPage } from "@/pages/load-balancers-page"
|
||||||
import { MePage } from "@/pages/me/me-page.tsx"
|
import { Login } from "@/pages/login.tsx"
|
||||||
import { NodePoolsPage } from "@/pages/nodepools/node-pools-page.tsx"
|
import { MePage } from "@/pages/me-page.tsx"
|
||||||
|
import { NodePoolsPage } from "@/pages/node-pools-page.tsx"
|
||||||
import { OrgApiKeys } from "@/pages/org/api-keys.tsx"
|
import { OrgApiKeys } from "@/pages/org/api-keys.tsx"
|
||||||
import { OrgMembers } from "@/pages/org/members.tsx"
|
import { OrgMembers } from "@/pages/org/members.tsx"
|
||||||
import { OrgSettings } from "@/pages/org/settings.tsx"
|
import { OrgSettings } from "@/pages/org/settings.tsx"
|
||||||
import { ServerPage } from "@/pages/servers/server-page.tsx"
|
import { ServerPage } from "@/pages/server-page.tsx"
|
||||||
import { SshPage } from "@/pages/ssh/ssh-page.tsx"
|
import { SshPage } from "@/pages/ssh-page.tsx"
|
||||||
import { TaintsPage } from "@/pages/taints/taints-page.tsx"
|
import { TaintsPage } from "@/pages/taints-page.tsx"
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@@ -42,6 +43,7 @@ export default function App() {
|
|||||||
<Route path="/credentials" element={<CredentialPage />} />
|
<Route path="/credentials" element={<CredentialPage />} />
|
||||||
<Route path="/dns" element={<DnsPage />} />
|
<Route path="/dns" element={<DnsPage />} />
|
||||||
<Route path="/load-balancers" element={<LoadBalancersPage />} />
|
<Route path="/load-balancers" element={<LoadBalancersPage />} />
|
||||||
|
<Route path="/clusters" element={<ClustersPage />} />
|
||||||
|
|
||||||
<Route path="/admin/jobs" element={<JobsPage />} />
|
<Route path="/admin/jobs" element={<JobsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
150
ui/src/api/clusters.ts
Normal file
150
ui/src/api/clusters.ts
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import { withRefresh } from "@/api/with-refresh"
|
||||||
|
import type {
|
||||||
|
DtoAttachBastionRequest,
|
||||||
|
DtoAttachCaptainDomainRequest,
|
||||||
|
DtoAttachLoadBalancerRequest,
|
||||||
|
DtoAttachRecordSetRequest,
|
||||||
|
DtoCreateClusterRequest,
|
||||||
|
DtoSetKubeconfigRequest,
|
||||||
|
DtoUpdateClusterRequest,
|
||||||
|
} from "@/sdk"
|
||||||
|
import { makeClusterApi } from "@/sdkClient"
|
||||||
|
|
||||||
|
const clusters = makeClusterApi()
|
||||||
|
|
||||||
|
export const clustersApi = {
|
||||||
|
// --- basic CRUD ---
|
||||||
|
|
||||||
|
listClusters: (q?: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.listClusters(q ? { q } : {})
|
||||||
|
}),
|
||||||
|
|
||||||
|
getCluster: (id: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.getCluster({ clusterID: id })
|
||||||
|
}),
|
||||||
|
|
||||||
|
createCluster: (body: DtoCreateClusterRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.createCluster({
|
||||||
|
dtoCreateClusterRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateCluster: (id: string, body: DtoUpdateClusterRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.updateCluster({
|
||||||
|
clusterID: id,
|
||||||
|
dtoUpdateClusterRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
deleteCluster: (id: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.deleteCluster({ clusterID: id })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// --- kubeconfig ---
|
||||||
|
|
||||||
|
setKubeconfig: (clusterID: string, body: DtoSetKubeconfigRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.setClusterKubeconfig({
|
||||||
|
clusterID,
|
||||||
|
dtoSetKubeconfigRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
clearKubeconfig: (clusterID: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.clearClusterKubeconfig({ clusterID })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// --- captain domain ---
|
||||||
|
|
||||||
|
attachCaptainDomain: (clusterID: string, body: DtoAttachCaptainDomainRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.attachCaptainDomain({
|
||||||
|
clusterID,
|
||||||
|
dtoAttachCaptainDomainRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
detachCaptainDomain: (clusterID: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.detachCaptainDomain({ clusterID })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// --- control plane record set ---
|
||||||
|
|
||||||
|
attachControlPlaneRecordSet: (clusterID: string, body: DtoAttachRecordSetRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.attachControlPlaneRecordSet({
|
||||||
|
clusterID,
|
||||||
|
dtoAttachRecordSetRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
detachControlPlaneRecordSet: (clusterID: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.detachControlPlaneRecordSet({ clusterID })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// --- load balancers ---
|
||||||
|
|
||||||
|
attachAppsLoadBalancer: (clusterID: string, body: DtoAttachLoadBalancerRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.attachAppsLoadBalancer({
|
||||||
|
clusterID,
|
||||||
|
dtoAttachLoadBalancerRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
detachAppsLoadBalancer: (clusterID: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.detachAppsLoadBalancer({ clusterID })
|
||||||
|
}),
|
||||||
|
|
||||||
|
attachGlueOpsLoadBalancer: (clusterID: string, body: DtoAttachLoadBalancerRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.attachGlueOpsLoadBalancer({
|
||||||
|
clusterID,
|
||||||
|
dtoAttachLoadBalancerRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
detachGlueOpsLoadBalancer: (clusterID: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.detachGlueOpsLoadBalancer({ clusterID })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// --- bastion ---
|
||||||
|
|
||||||
|
attachBastion: (clusterID: string, body: DtoAttachBastionRequest) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.attachBastionServer({
|
||||||
|
clusterID,
|
||||||
|
dtoAttachBastionRequest: body,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
detachBastion: (clusterID: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.detachBastionServer({ clusterID })
|
||||||
|
}),
|
||||||
|
|
||||||
|
// -- node-pools
|
||||||
|
|
||||||
|
attachNodePool: (clusterID: string, nodePoolID: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.attachNodePool({
|
||||||
|
clusterID,
|
||||||
|
dtoAttachNodePoolRequest: { node_pool_id: nodePoolID },
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
detachNodePool: (clusterID: string, nodePoolID: string) =>
|
||||||
|
withRefresh(async () => {
|
||||||
|
return await clusters.detachNodePool({ clusterID, nodePoolID })
|
||||||
|
}),
|
||||||
|
}
|
||||||
1273
ui/src/pages/cluster-page.tsx
Normal file
1273
ui/src/pages/cluster-page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,28 +1,66 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react"
|
||||||
import { credentialsApi } from "@/api/credentials";
|
import { credentialsApi } from "@/api/credentials"
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||||
import { AlertTriangle, Eye, Loader2, MoreHorizontal, Pencil, Plus, Search, Trash2 } from "lucide-react";
|
import {
|
||||||
import { Controller, useForm } from "react-hook-form";
|
AlertTriangle,
|
||||||
import { toast } from "sonner";
|
Eye,
|
||||||
import { z } from "zod";
|
Loader2,
|
||||||
|
MoreHorizontal,
|
||||||
|
Pencil,
|
||||||
|
Plus,
|
||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@/components/ui/alert-dialog";
|
Search,
|
||||||
import { Badge } from "@/components/ui/badge";
|
Trash2,
|
||||||
import { Button } from "@/components/ui/button";
|
} from "lucide-react"
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
import { Controller, useForm } from "react-hook-form"
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
import { toast } from "sonner"
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
|
import { z } from "zod"
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
} from "@/components/ui/alert-dialog"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
|
import { Switch } from "@/components/ui/switch"
|
||||||
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
|
|
||||||
// -------------------- Constants --------------------
|
// -------------------- Constants --------------------
|
||||||
|
|
||||||
Reference in New Issue
Block a user