From 9aa8b56922f64a7d11e641fa72a24d8951da0b7a Mon Sep 17 00:00:00 2001 From: starryskymeow Date: Thu, 17 Jul 2025 18:16:32 +0800 Subject: [PATCH] Redesign data structures and implement PVC management of create and delete --- cmd/manager/main.go | 3 -- internal/api/handlers.go | 80 +-------------------------------------- internal/api/pvc.go | 44 +++++++++++++++++++++ internal/api/server.go | 22 +++++++---- internal/api/workspace.go | 57 ++++++++++++++++++++++++++++ internal/k8s/define.go | 5 +++ internal/k8s/pvc.go | 45 ++++++++++++++++++++++ 7 files changed, 166 insertions(+), 90 deletions(-) create mode 100644 internal/api/pvc.go create mode 100644 internal/api/workspace.go create mode 100644 internal/k8s/define.go create mode 100644 internal/k8s/pvc.go diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 8a1c389..b909ca5 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -17,9 +17,6 @@ func main() { server := api.NewServer(clientset) - // TODO: Initialize controllers - // TODO: Start everything - fmt.Println("Starting API server on :8080") if err := server.Run(":8080"); err != nil { panic(fmt.Sprintf("Failed to start API server: %v", err)) diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 55eb608..21ff5d1 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -1,98 +1,20 @@ package api import ( - "net/http" "regexp" - "gitea.starryskymeow.cn/xkm/educode-controller/internal/k8s" - "github.com/gin-gonic/gin" "k8s.io/client-go/kubernetes" ) -const Namespace = "educode" - type Handler struct { clientset *kubernetes.Clientset } +// create handler with k8s clientset func NewHandler(clientset *kubernetes.Clientset) *Handler { return &Handler{clientset: clientset} } -func (h *Handler) createWorkspace(c *gin.Context) { - var req CreateWorkspaceRequest - if err := c.ShouldBindJSON(&req); err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - if !isValidDNS1035Label(req.WorkspaceID) { - c.JSON(http.StatusBadRequest, gin.H{"error": "a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123')"}) - return - } - image := k8s.DefaultImage - if req.Image != "" { - image = req.Image - } - - resourceLimits := k8s.DefaultResourceLimits - if req.ResourceLimits != nil { - if req.ResourceLimits.CPU != "" { - resourceLimits.CPU = req.ResourceLimits.CPU - } - if req.ResourceLimits.Memory != "" { - resourceLimits.Memory = req.ResourceLimits.Memory - } - if req.ResourceLimits.Storage != "" { - resourceLimits.Storage = req.ResourceLimits.Storage - } - } - - k8sReq := &k8s.WorkspaceRequest{ - Image: image, - Env: req.Env, - ResourceLimits: &resourceLimits, - WorkspaceID: req.WorkspaceID, - Clientset: h.clientset, - Namespace: Namespace, - } - - keyword, err := k8s.CreateWorkspace(k8sReq) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusAccepted, gin.H{"status": "creating", "workspaceId": req.WorkspaceID, "keyword": keyword}) -} - -func (h *Handler) deleteWorkspace(c *gin.Context) { - workspaceID := c.Param("workspaceID") - - err := k8s.DeleteWorkspace(h.clientset, Namespace, workspaceID) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) - return - } - - c.JSON(http.StatusAccepted, gin.H{"status": "deleting", "workspaceId": workspaceID}) -} - -func (h *Handler) getWorkspace(c *gin.Context) { - workspaceID := c.Param("workspaceID") - - // TODO: Call k8s manager to get resource status - - c.JSON(http.StatusOK, gin.H{"status": "ok", "workspaceId": workspaceID}) -} - -func (h *Handler) extendWorkspace(c *gin.Context) { - workspaceID := c.Param("workspaceID") - - // TODO: Call k8s manager to patch resource annotation - - c.JSON(http.StatusOK, gin.H{"status": "extended", "workspaceId": workspaceID}) -} - func isValidDNS1035Label(s string) bool { if len(s) > 63 { return false diff --git a/internal/api/pvc.go b/internal/api/pvc.go new file mode 100644 index 0000000..350f346 --- /dev/null +++ b/internal/api/pvc.go @@ -0,0 +1,44 @@ +package api + +import ( + "net/http" + + "gitea.starryskymeow.cn/xkm/educode-controller/internal/k8s" + "gitea.starryskymeow.cn/xkm/educode-controller/internal/types" + "github.com/gin-gonic/gin" +) + +func (h *Handler) createPvc(c *gin.Context) { + var req types.Pvc + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + if !isValidDNS1035Label(req.ID) { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123')"}) + return + } + + err := k8s.CreatePvc(req, h.clientset) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + } + c.JSON(http.StatusAccepted, gin.H{"success": true}) +} + +func (h *Handler) deletePvc(c *gin.Context) { + ID := c.Param("ID") + err := k8s.DeletePvc(ID, h.clientset) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + } + c.JSON(http.StatusAccepted, gin.H{"success": true}) +} + +func (h *Handler) getPvcByID(c *gin.Context) { + // TODO +} + +func (h *Handler) getPvc(c *gin.Context) { + // TODO +} diff --git a/internal/api/server.go b/internal/api/server.go index f06c6e4..413094f 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -18,15 +18,21 @@ func NewServer(clientset *kubernetes.Clientset) *gin.Engine { }) }) - v1 := r.Group("/api/v1") + workspaces := r.Group("/workspaces") { - workspaces := v1.Group("/workspaces") - { - workspaces.POST("", h.createWorkspace) - workspaces.DELETE("/:workspaceID", h.deleteWorkspace) - workspaces.GET("/:workspaceID", h.getWorkspace) - workspaces.PATCH("/:workspaceID", h.extendWorkspace) - } + workspaces.POST("", h.createWorkspace) + workspaces.DELETE("/:ID", h.deleteWorkspace) + workspaces.GET("/:ID", h.getWorkspaceByID) + workspaces.GET("", h.getWorkspace) + // workspaces.PATCH("/:workspaceID", h.extendWorkspace) + } + + pvcs := r.Group("/pvcs") + { + pvcs.POST("", h.createPvc) + pvcs.DELETE("/:ID", h.deletePvc) + pvcs.GET("/:ID", h.getPvcByID) + pvcs.GET("", h.getPvc) } return r diff --git a/internal/api/workspace.go b/internal/api/workspace.go new file mode 100644 index 0000000..3edb97d --- /dev/null +++ b/internal/api/workspace.go @@ -0,0 +1,57 @@ +package api + +import ( + "net/http" + + "gitea.starryskymeow.cn/xkm/educode-controller/internal/k8s" + "gitea.starryskymeow.cn/xkm/educode-controller/internal/types" + "github.com/gin-gonic/gin" +) + +func (h *Handler) createWorkspace(c *gin.Context) { + var req types.Workspace + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()}) + return + } + if !isValidDNS1035Label(req.ID) { + c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123')"}) + return + } + + err := k8s.CreateWorkspace(&req, h.clientset) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + + c.JSON(http.StatusAccepted, gin.H{"success": true}) +} + +func (h *Handler) deleteWorkspace(c *gin.Context) { + ID := c.Param("ID") + + err := k8s.DeleteWorkspace(ID, h.clientset) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()}) + return + } + + c.JSON(http.StatusAccepted, gin.H{"success": true}) +} + +func (h *Handler) getWorkspaceByID(c *gin.Context) { + // workspaceID := c.Param("workspaceID") + + // TODO: Call k8s manager to get resource status +} + +func (h *Handler) getWorkspace(c *gin.Context) { + // TODO: Call k8s manager to get resource status +} + +func (h *Handler) extendWorkspace(c *gin.Context) { + // workspaceID := c.Param("workspaceID") + + // TODO: Call k8s manager to patch resource annotation +} diff --git a/internal/k8s/define.go b/internal/k8s/define.go new file mode 100644 index 0000000..8260306 --- /dev/null +++ b/internal/k8s/define.go @@ -0,0 +1,5 @@ +package k8s + +const ( + namespace = "educode" +) \ No newline at end of file diff --git a/internal/k8s/pvc.go b/internal/k8s/pvc.go new file mode 100644 index 0000000..e62c2ee --- /dev/null +++ b/internal/k8s/pvc.go @@ -0,0 +1,45 @@ +package k8s + +import ( + "context" + + "gitea.starryskymeow.cn/xkm/educode-controller/internal/types" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +func CreatePvc(req types.Pvc, client *kubernetes.Clientset) error { + pvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: req.ID, + Namespace: namespace, + Labels: map[string]string{ + "educode/uid": req.UID, + }, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.PersistentVolumeAccessMode(req.AccessMode), + }, + StorageClassName: &req.StorageClassName, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse(req.Limit), + }, + }, + }, + } + + _, err := client.CoreV1().PersistentVolumeClaims(namespace).Create(context.TODO(), pvc, metav1.CreateOptions{}) + if err != nil { + return err + } + return nil +} + +func DeletePvc(ID string, client *kubernetes.Clientset) error { + err := client.CoreV1().PersistentVolumeClaims(namespace).Delete(context.TODO(), ID, metav1.DeleteOptions{}) + return err +}