little improve
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
Reference in New Issue
Block a user