From 49d90848d899fa9b1b4a7139ae5958cd52b2665a Mon Sep 17 00:00:00 2001 From: starryskymeow Date: Sat, 5 Jul 2025 06:04:40 +0800 Subject: [PATCH] little improve --- cmd/manager/main.go | 3 +- internal/api/handlers.go | 32 ++++++++------ internal/api/server.go | 1 - internal/api/types.go | 18 ++++---- internal/k8s/workspace.go | 87 ++++++++++++++++++++------------------- 5 files changed, 74 insertions(+), 67 deletions(-) diff --git a/cmd/manager/main.go b/cmd/manager/main.go index d92ad18..8a1c389 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "gitea.starryskymeow.cn/xkm/educode-controller/internal/api" "gitea.starryskymeow.cn/xkm/educode-controller/internal/k8s" ) @@ -16,7 +17,7 @@ func main() { server := api.NewServer(clientset) - // TODO: Initialize controllers + // TODO: Initialize controllers // TODO: Start everything fmt.Println("Starting API server on :8080") diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 62b0c45..08cf1c9 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -8,7 +8,7 @@ import ( "k8s.io/client-go/kubernetes" ) -const Namespace = "default" // Or get from config +const Namespace = "educode" type Handler struct { clientset *kubernetes.Clientset @@ -24,25 +24,31 @@ func (h *Handler) createWorkspace(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } - image := k8s.DefaultImage if req.Image != "" { image = req.Image } - k8sReq := &k8s.WorkspaceRequest{ - Image: image, - Env: req.Env, - WorkspaceID: req.WorkspaceID, - Clientset: h.clientset, - Namespace: Namespace, - } - + resourceLimits := k8s.DefaultResourceLimits if req.ResourceLimits != nil { - k8sReq.ResourceLimits = &k8s.ResourceLimits{ - CPU: req.ResourceLimits.CPU, - Memory: req.ResourceLimits.Memory, + 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, } err := k8s.CreateWorkspace(k8sReq) diff --git a/internal/api/server.go b/internal/api/server.go index fc2ad0a..310e3a8 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -11,7 +11,6 @@ func NewServer(clientset *kubernetes.Clientset) *gin.Engine { // Create a new handler with the clientset h := NewHandler(clientset) - // Health check endpoint r.GET("/healthz", func(c *gin.Context) { c.JSON(200, gin.H{ diff --git a/internal/api/types.go b/internal/api/types.go index 343292f..74d9749 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -1,15 +1,13 @@ package api +import ( + "gitea.starryskymeow.cn/xkm/educode-controller/internal/k8s" +) + // CreateWorkspaceRequest defines the request body for creating a new workspace. type CreateWorkspaceRequest struct { - Image string `json:"image"` - Env map[string]string `json:"env"` - ResourceLimits *ResourceLimits `json:"resourceLimits"` - WorkspaceID string `json:"workspaceId"` -} - -// ResourceLimits defines the CPU and memory resource limits. -type ResourceLimits struct { - CPU string `json:"cpu"` - Memory string `json:"memory"` + Image string `json:"image"` + Env map[string]string `json:"env"` + ResourceLimits *k8s.ResourceLimits `json:"resourceLimits"` + WorkspaceID string `json:"workspaceId"` } diff --git a/internal/k8s/workspace.go b/internal/k8s/workspace.go index bb47347..83cc28c 100644 --- a/internal/k8s/workspace.go +++ b/internal/k8s/workspace.go @@ -14,12 +14,15 @@ import ( ) const ( - DefaultImage = "ghcr.io/dreamstarsky/educode:cpu-latest" - DefaultCPU = "500m" - DefaultMemory = "512Mi" - DefaultStorage = "5Gi" + DefaultImage = "ghcr.io/dreamstarsky/educode:cpu-latest" ) +var DefaultResourceLimits = ResourceLimits{ + CPU: "500m", + Memory: "1Gi", + Storage: "5Gi", +} + type WorkspaceRequest struct { Image string Env map[string]string @@ -30,8 +33,9 @@ type WorkspaceRequest struct { } type ResourceLimits struct { - CPU string - Memory string + CPU string `json:"cpu"` + Memory string `json:"memory"` + Storage string `json:"storage"` } func CreateWorkspace(req *WorkspaceRequest) error { @@ -60,13 +64,13 @@ func CreateWorkspace(req *WorkspaceRequest) error { Name: "code-server", Image: req.Image, Ports: []corev1.ContainerPort{ - {ContainerPort: 8080}, // Default code-server port + {ContainerPort: 8080}, // code-server port {ContainerPort: 22}, // SSH port }, - Env: getEnvVars(req.Env), - Resources: getResourceRequirements(req.ResourceLimits), + Env: getEnvVars(req.Env), + Resources: getResourceRequirements(req.ResourceLimits), VolumeMounts: []corev1.VolumeMount{ - {Name: "workspace-storage", MountPath: "/home/coder"}, + {Name: "workspace-storage", MountPath: "/home/ubuntu/workspace"}, }, }, }, @@ -76,10 +80,10 @@ func CreateWorkspace(req *WorkspaceRequest) error { { ObjectMeta: metav1.ObjectMeta{Name: "workspace-storage"}, Spec: corev1.PersistentVolumeClaimSpec{ - AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteMany}, Resources: corev1.VolumeResourceRequirements{ Requests: corev1.ResourceList{ - corev1.ResourceStorage: resource.MustParse(DefaultStorage), + corev1.ResourceStorage: resource.MustParse(req.ResourceLimits.Storage), }, }, }, @@ -88,32 +92,44 @@ func CreateWorkspace(req *WorkspaceRequest) error { }, } + // Create resources + sts, err := req.Clientset.AppsV1().StatefulSets(req.Namespace).Create(context.TODO(), sts, metav1.CreateOptions{}) + if err != nil { + return fmt.Errorf("failed to create statefulset: %w", err) + } + // Define Service svc := &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: req.WorkspaceID, Namespace: req.Namespace, + OwnerReferences: []metav1.OwnerReference{ + { + APIVersion: "apps/v1", + Kind: "StatefulSet", + Name: req.WorkspaceID, + UID: sts.UID, + Controller: boolPrt(true), + }, + }, }, Spec: corev1.ServiceSpec{ Selector: map[string]string{"app": req.WorkspaceID}, Ports: []corev1.ServicePort{ - {Name: "http", Port: 8080, TargetPort: intstr.FromInt(8080)}, + {Name: "code-server", Port: 8080, TargetPort: intstr.FromInt(8080)}, {Name: "ssh", Port: 22, TargetPort: intstr.FromInt(22)}, }, Type: corev1.ServiceTypeClusterIP, }, } - // Create resources - _, err := req.Clientset.AppsV1().StatefulSets(req.Namespace).Create(context.TODO(), sts, metav1.CreateOptions{}) - if err != nil { - return fmt.Errorf("failed to create statefulset: %w", err) - } - _, err = req.Clientset.CoreV1().Services(req.Namespace).Create(context.TODO(), svc, metav1.CreateOptions{}) if err != nil { // Cleanup StatefulSet if Service creation fails - req.Clientset.AppsV1().StatefulSets(req.Namespace).Delete(context.TODO(), req.WorkspaceID, metav1.DeleteOptions{}) + deletePolicy := metav1.DeletePropagationBackground + req.Clientset.AppsV1().StatefulSets(req.Namespace).Delete(context.TODO(), req.WorkspaceID, metav1.DeleteOptions{ + PropagationPolicy: &deletePolicy, + }) return fmt.Errorf("failed to create service: %w", err) } @@ -122,20 +138,16 @@ func CreateWorkspace(req *WorkspaceRequest) error { func DeleteWorkspace(clientset *kubernetes.Clientset, namespace, workspaceID string) error { // Delete StatefulSet - err := clientset.AppsV1().StatefulSets(namespace).Delete(context.TODO(), workspaceID, metav1.DeleteOptions{}) + deletePolicy := metav1.DeletePropagationBackground + // TODO + // Maybe not delete pvc? + err := clientset.AppsV1().StatefulSets(namespace).Delete(context.TODO(), workspaceID, metav1.DeleteOptions{ + PropagationPolicy: &deletePolicy, + }) if err != nil { return fmt.Errorf("failed to delete statefulset: %w", err) } - // Delete Service - err = clientset.CoreV1().Services(namespace).Delete(context.TODO(), workspaceID, metav1.DeleteOptions{}) - if err != nil { - return fmt.Errorf("failed to delete service: %w", err) - } - - // PVC will be deleted automatically when the StatefulSet is deleted, if the reclaim policy is set to Delete. - // If not, it might need manual cleanup. For now, we assume it's handled. - return nil } @@ -150,22 +162,13 @@ func getEnvVars(env map[string]string) []corev1.EnvVar { func getResourceRequirements(limits *ResourceLimits) corev1.ResourceRequirements { req := corev1.ResourceRequirements{ Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse(DefaultCPU), - corev1.ResourceMemory: resource.MustParse(DefaultMemory), + corev1.ResourceCPU: resource.MustParse(limits.CPU), + corev1.ResourceMemory: resource.MustParse(limits.Memory), }, } - if limits != nil { - req.Limits = corev1.ResourceList{} - if limits.CPU != "" { - req.Limits[corev1.ResourceCPU] = resource.MustParse(limits.CPU) - } - if limits.Memory != "" { - req.Limits[corev1.ResourceMemory] = resource.MustParse(limits.Memory) - } - } - return req } func int32Ptr(i int32) *int32 { return &i } +func boolPrt(b bool) *bool { return &b }