little improve

This commit is contained in:
starryskymeow
2025-07-05 06:04:40 +08:00
parent fa18edc20f
commit 49d90848d8
5 changed files with 74 additions and 67 deletions

View File

@@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"gitea.starryskymeow.cn/xkm/educode-controller/internal/api" "gitea.starryskymeow.cn/xkm/educode-controller/internal/api"
"gitea.starryskymeow.cn/xkm/educode-controller/internal/k8s" "gitea.starryskymeow.cn/xkm/educode-controller/internal/k8s"
) )
@@ -16,7 +17,7 @@ func main() {
server := api.NewServer(clientset) server := api.NewServer(clientset)
// TODO: Initialize controllers // TODO: Initialize controllers
// TODO: Start everything // TODO: Start everything
fmt.Println("Starting API server on :8080") fmt.Println("Starting API server on :8080")

View File

@@ -8,7 +8,7 @@ import (
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
) )
const Namespace = "default" // Or get from config const Namespace = "educode"
type Handler struct { type Handler struct {
clientset *kubernetes.Clientset clientset *kubernetes.Clientset
@@ -24,25 +24,31 @@ func (h *Handler) createWorkspace(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
image := k8s.DefaultImage image := k8s.DefaultImage
if req.Image != "" { if req.Image != "" {
image = req.Image image = req.Image
} }
k8sReq := &k8s.WorkspaceRequest{ resourceLimits := k8s.DefaultResourceLimits
Image: image,
Env: req.Env,
WorkspaceID: req.WorkspaceID,
Clientset: h.clientset,
Namespace: Namespace,
}
if req.ResourceLimits != nil { if req.ResourceLimits != nil {
k8sReq.ResourceLimits = &k8s.ResourceLimits{ if req.ResourceLimits.CPU != "" {
CPU: req.ResourceLimits.CPU, resourceLimits.CPU = req.ResourceLimits.CPU
Memory: req.ResourceLimits.Memory,
} }
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) err := k8s.CreateWorkspace(k8sReq)

View File

@@ -11,7 +11,6 @@ func NewServer(clientset *kubernetes.Clientset) *gin.Engine {
// Create a new handler with the clientset // Create a new handler with the clientset
h := NewHandler(clientset) h := NewHandler(clientset)
// Health check endpoint // Health check endpoint
r.GET("/healthz", func(c *gin.Context) { r.GET("/healthz", func(c *gin.Context) {
c.JSON(200, gin.H{ c.JSON(200, gin.H{

View File

@@ -1,15 +1,13 @@
package api package api
import (
"gitea.starryskymeow.cn/xkm/educode-controller/internal/k8s"
)
// CreateWorkspaceRequest defines the request body for creating a new workspace. // CreateWorkspaceRequest defines the request body for creating a new workspace.
type CreateWorkspaceRequest struct { type CreateWorkspaceRequest struct {
Image string `json:"image"` Image string `json:"image"`
Env map[string]string `json:"env"` Env map[string]string `json:"env"`
ResourceLimits *ResourceLimits `json:"resourceLimits"` ResourceLimits *k8s.ResourceLimits `json:"resourceLimits"`
WorkspaceID string `json:"workspaceId"` WorkspaceID string `json:"workspaceId"`
}
// ResourceLimits defines the CPU and memory resource limits.
type ResourceLimits struct {
CPU string `json:"cpu"`
Memory string `json:"memory"`
} }

View File

@@ -14,12 +14,15 @@ import (
) )
const ( const (
DefaultImage = "ghcr.io/dreamstarsky/educode:cpu-latest" DefaultImage = "ghcr.io/dreamstarsky/educode:cpu-latest"
DefaultCPU = "500m"
DefaultMemory = "512Mi"
DefaultStorage = "5Gi"
) )
var DefaultResourceLimits = ResourceLimits{
CPU: "500m",
Memory: "1Gi",
Storage: "5Gi",
}
type WorkspaceRequest struct { type WorkspaceRequest struct {
Image string Image string
Env map[string]string Env map[string]string
@@ -30,8 +33,9 @@ type WorkspaceRequest struct {
} }
type ResourceLimits struct { type ResourceLimits struct {
CPU string CPU string `json:"cpu"`
Memory string Memory string `json:"memory"`
Storage string `json:"storage"`
} }
func CreateWorkspace(req *WorkspaceRequest) error { func CreateWorkspace(req *WorkspaceRequest) error {
@@ -60,13 +64,13 @@ func CreateWorkspace(req *WorkspaceRequest) error {
Name: "code-server", Name: "code-server",
Image: req.Image, Image: req.Image,
Ports: []corev1.ContainerPort{ Ports: []corev1.ContainerPort{
{ContainerPort: 8080}, // Default code-server port {ContainerPort: 8080}, // code-server port
{ContainerPort: 22}, // SSH port {ContainerPort: 22}, // SSH port
}, },
Env: getEnvVars(req.Env), Env: getEnvVars(req.Env),
Resources: getResourceRequirements(req.ResourceLimits), Resources: getResourceRequirements(req.ResourceLimits),
VolumeMounts: []corev1.VolumeMount{ 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"}, ObjectMeta: metav1.ObjectMeta{Name: "workspace-storage"},
Spec: corev1.PersistentVolumeClaimSpec{ Spec: corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteMany},
Resources: corev1.VolumeResourceRequirements{ Resources: corev1.VolumeResourceRequirements{
Requests: corev1.ResourceList{ 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 // Define Service
svc := &corev1.Service{ svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: req.WorkspaceID, Name: req.WorkspaceID,
Namespace: req.Namespace, Namespace: req.Namespace,
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: "apps/v1",
Kind: "StatefulSet",
Name: req.WorkspaceID,
UID: sts.UID,
Controller: boolPrt(true),
},
},
}, },
Spec: corev1.ServiceSpec{ Spec: corev1.ServiceSpec{
Selector: map[string]string{"app": req.WorkspaceID}, Selector: map[string]string{"app": req.WorkspaceID},
Ports: []corev1.ServicePort{ 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)}, {Name: "ssh", Port: 22, TargetPort: intstr.FromInt(22)},
}, },
Type: corev1.ServiceTypeClusterIP, 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{}) _, err = req.Clientset.CoreV1().Services(req.Namespace).Create(context.TODO(), svc, metav1.CreateOptions{})
if err != nil { if err != nil {
// Cleanup StatefulSet if Service creation fails // 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) 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 { func DeleteWorkspace(clientset *kubernetes.Clientset, namespace, workspaceID string) error {
// Delete StatefulSet // 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 { if err != nil {
return fmt.Errorf("failed to delete statefulset: %w", err) 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 return nil
} }
@@ -150,22 +162,13 @@ func getEnvVars(env map[string]string) []corev1.EnvVar {
func getResourceRequirements(limits *ResourceLimits) corev1.ResourceRequirements { func getResourceRequirements(limits *ResourceLimits) corev1.ResourceRequirements {
req := corev1.ResourceRequirements{ req := corev1.ResourceRequirements{
Requests: corev1.ResourceList{ Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse(DefaultCPU), corev1.ResourceCPU: resource.MustParse(limits.CPU),
corev1.ResourceMemory: resource.MustParse(DefaultMemory), 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 return req
} }
func int32Ptr(i int32) *int32 { return &i } func int32Ptr(i int32) *int32 { return &i }
func boolPrt(b bool) *bool { return &b }