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 (
"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")

View File

@@ -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)

View File

@@ -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{

View File

@@ -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"`
}

View File

@@ -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 }