Add workspace creation feature
This commit is contained in:
17
internal/k8s/client.go
Normal file
17
internal/k8s/client.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package k8s
|
||||
|
||||
import (
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
|
||||
// NewClient creates a new Kubernetes clientset.
|
||||
func NewClient() (*kubernetes.Clientset, error) {
|
||||
// Try to load in-cluster config
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return kubernetes.NewForConfig(config)
|
||||
}
|
||||
171
internal/k8s/workspace.go
Normal file
171
internal/k8s/workspace.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package k8s
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultImage = "ghcr.io/dreamstarsky/educode:cpu-latest"
|
||||
DefaultCPU = "500m"
|
||||
DefaultMemory = "512Mi"
|
||||
DefaultStorage = "5Gi"
|
||||
)
|
||||
|
||||
type WorkspaceRequest struct {
|
||||
Image string
|
||||
Env map[string]string
|
||||
ResourceLimits *ResourceLimits
|
||||
WorkspaceID string
|
||||
Clientset *kubernetes.Clientset
|
||||
Namespace string
|
||||
}
|
||||
|
||||
type ResourceLimits struct {
|
||||
CPU string
|
||||
Memory string
|
||||
}
|
||||
|
||||
func CreateWorkspace(req *WorkspaceRequest) error {
|
||||
// Define StatefulSet
|
||||
sts := &appsv1.StatefulSet{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: req.WorkspaceID,
|
||||
Namespace: req.Namespace,
|
||||
Annotations: map[string]string{
|
||||
"educode/expires-at": time.Now().Add(1 * time.Hour).Format(time.RFC3339),
|
||||
},
|
||||
},
|
||||
Spec: appsv1.StatefulSetSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{"app": req.WorkspaceID},
|
||||
},
|
||||
ServiceName: req.WorkspaceID,
|
||||
Replicas: int32Ptr(1),
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"app": req.WorkspaceID},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "code-server",
|
||||
Image: req.Image,
|
||||
Ports: []corev1.ContainerPort{
|
||||
{ContainerPort: 8080}, // Default code-server port
|
||||
{ContainerPort: 22}, // SSH port
|
||||
},
|
||||
Env: getEnvVars(req.Env),
|
||||
Resources: getResourceRequirements(req.ResourceLimits),
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{Name: "workspace-storage", MountPath: "/home/coder"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
VolumeClaimTemplates: []corev1.PersistentVolumeClaim{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "workspace-storage"},
|
||||
Spec: corev1.PersistentVolumeClaimSpec{
|
||||
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
|
||||
Resources: corev1.VolumeResourceRequirements{
|
||||
Requests: corev1.ResourceList{
|
||||
corev1.ResourceStorage: resource.MustParse(DefaultStorage),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Define Service
|
||||
svc := &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: req.WorkspaceID,
|
||||
Namespace: req.Namespace,
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Selector: map[string]string{"app": req.WorkspaceID},
|
||||
Ports: []corev1.ServicePort{
|
||||
{Name: "http", 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{})
|
||||
return fmt.Errorf("failed to create service: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteWorkspace(clientset *kubernetes.Clientset, namespace, workspaceID string) error {
|
||||
// Delete StatefulSet
|
||||
err := clientset.AppsV1().StatefulSets(namespace).Delete(context.TODO(), workspaceID, metav1.DeleteOptions{})
|
||||
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
|
||||
}
|
||||
|
||||
func getEnvVars(env map[string]string) []corev1.EnvVar {
|
||||
var envVars []corev1.EnvVar
|
||||
for k, v := range env {
|
||||
envVars = append(envVars, corev1.EnvVar{Name: k, Value: v})
|
||||
}
|
||||
return envVars
|
||||
}
|
||||
|
||||
func getResourceRequirements(limits *ResourceLimits) corev1.ResourceRequirements {
|
||||
req := corev1.ResourceRequirements{
|
||||
Requests: corev1.ResourceList{
|
||||
corev1.ResourceCPU: resource.MustParse(DefaultCPU),
|
||||
corev1.ResourceMemory: resource.MustParse(DefaultMemory),
|
||||
},
|
||||
}
|
||||
|
||||
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 }
|
||||
Reference in New Issue
Block a user