Compare commits

2 Commits

Author SHA1 Message Date
starryskymeow
d175a66394 fix: return when failed to manage pvc 2025-07-17 18:55:34 +08:00
starryskymeow
9aa8b56922 Redesign data structures and implement PVC management of create and delete 2025-07-17 18:17:40 +08:00
7 changed files with 168 additions and 90 deletions

View File

@@ -17,9 +17,6 @@ func main() {
server := api.NewServer(clientset) server := api.NewServer(clientset)
// TODO: Initialize controllers
// TODO: Start everything
fmt.Println("Starting API server on :8080") fmt.Println("Starting API server on :8080")
if err := server.Run(":8080"); err != nil { if err := server.Run(":8080"); err != nil {
panic(fmt.Sprintf("Failed to start API server: %v", err)) panic(fmt.Sprintf("Failed to start API server: %v", err))

View File

@@ -1,98 +1,20 @@
package api package api
import ( import (
"net/http"
"regexp" "regexp"
"gitea.starryskymeow.cn/xkm/educode-controller/internal/k8s"
"github.com/gin-gonic/gin"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
) )
const Namespace = "educode"
type Handler struct { type Handler struct {
clientset *kubernetes.Clientset clientset *kubernetes.Clientset
} }
// create handler with k8s clientset
func NewHandler(clientset *kubernetes.Clientset) *Handler { func NewHandler(clientset *kubernetes.Clientset) *Handler {
return &Handler{clientset: clientset} return &Handler{clientset: clientset}
} }
func (h *Handler) createWorkspace(c *gin.Context) {
var req CreateWorkspaceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if !isValidDNS1035Label(req.WorkspaceID) {
c.JSON(http.StatusBadRequest, gin.H{"error": "a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123')"})
return
}
image := k8s.DefaultImage
if req.Image != "" {
image = req.Image
}
resourceLimits := k8s.DefaultResourceLimits
if req.ResourceLimits != nil {
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,
}
keyword, err := k8s.CreateWorkspace(k8sReq)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusAccepted, gin.H{"status": "creating", "workspaceId": req.WorkspaceID, "keyword": keyword})
}
func (h *Handler) deleteWorkspace(c *gin.Context) {
workspaceID := c.Param("workspaceID")
err := k8s.DeleteWorkspace(h.clientset, Namespace, workspaceID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusAccepted, gin.H{"status": "deleting", "workspaceId": workspaceID})
}
func (h *Handler) getWorkspace(c *gin.Context) {
workspaceID := c.Param("workspaceID")
// TODO: Call k8s manager to get resource status
c.JSON(http.StatusOK, gin.H{"status": "ok", "workspaceId": workspaceID})
}
func (h *Handler) extendWorkspace(c *gin.Context) {
workspaceID := c.Param("workspaceID")
// TODO: Call k8s manager to patch resource annotation
c.JSON(http.StatusOK, gin.H{"status": "extended", "workspaceId": workspaceID})
}
func isValidDNS1035Label(s string) bool { func isValidDNS1035Label(s string) bool {
if len(s) > 63 { if len(s) > 63 {
return false return false

46
internal/api/pvc.go Normal file
View File

@@ -0,0 +1,46 @@
package api
import (
"net/http"
"gitea.starryskymeow.cn/xkm/educode-controller/internal/k8s"
"gitea.starryskymeow.cn/xkm/educode-controller/internal/types"
"github.com/gin-gonic/gin"
)
func (h *Handler) createPvc(c *gin.Context) {
var req types.Pvc
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
return
}
if !isValidDNS1035Label(req.ID) {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123')"})
return
}
err := k8s.CreatePvc(req, h.clientset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
c.JSON(http.StatusAccepted, gin.H{"success": true})
}
func (h *Handler) deletePvc(c *gin.Context) {
ID := c.Param("ID")
err := k8s.DeletePvc(ID, h.clientset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
c.JSON(http.StatusAccepted, gin.H{"success": true})
}
func (h *Handler) getPvcByID(c *gin.Context) {
// TODO
}
func (h *Handler) getPvc(c *gin.Context) {
// TODO
}

View File

@@ -18,15 +18,21 @@ func NewServer(clientset *kubernetes.Clientset) *gin.Engine {
}) })
}) })
v1 := r.Group("/api/v1") workspaces := r.Group("/workspaces")
{ {
workspaces := v1.Group("/workspaces") workspaces.POST("", h.createWorkspace)
{ workspaces.DELETE("/:ID", h.deleteWorkspace)
workspaces.POST("", h.createWorkspace) workspaces.GET("/:ID", h.getWorkspaceByID)
workspaces.DELETE("/:workspaceID", h.deleteWorkspace) workspaces.GET("", h.getWorkspace)
workspaces.GET("/:workspaceID", h.getWorkspace) // workspaces.PATCH("/:workspaceID", h.extendWorkspace)
workspaces.PATCH("/:workspaceID", h.extendWorkspace) }
}
pvcs := r.Group("/pvcs")
{
pvcs.POST("", h.createPvc)
pvcs.DELETE("/:ID", h.deletePvc)
pvcs.GET("/:ID", h.getPvcByID)
pvcs.GET("", h.getPvc)
} }
return r return r

57
internal/api/workspace.go Normal file
View File

@@ -0,0 +1,57 @@
package api
import (
"net/http"
"gitea.starryskymeow.cn/xkm/educode-controller/internal/k8s"
"gitea.starryskymeow.cn/xkm/educode-controller/internal/types"
"github.com/gin-gonic/gin"
)
func (h *Handler) createWorkspace(c *gin.Context) {
var req types.Workspace
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": err.Error()})
return
}
if !isValidDNS1035Label(req.ID) {
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "a DNS-1035 label must consist of lower case alphanumeric characters or '-', start with an alphabetic character, and end with an alphanumeric character (e.g. 'my-name', or 'abc-123')"})
return
}
err := k8s.CreateWorkspace(&req, h.clientset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
c.JSON(http.StatusAccepted, gin.H{"success": true})
}
func (h *Handler) deleteWorkspace(c *gin.Context) {
ID := c.Param("ID")
err := k8s.DeleteWorkspace(ID, h.clientset)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": err.Error()})
return
}
c.JSON(http.StatusAccepted, gin.H{"success": true})
}
func (h *Handler) getWorkspaceByID(c *gin.Context) {
// workspaceID := c.Param("workspaceID")
// TODO: Call k8s manager to get resource status
}
func (h *Handler) getWorkspace(c *gin.Context) {
// TODO: Call k8s manager to get resource status
}
func (h *Handler) extendWorkspace(c *gin.Context) {
// workspaceID := c.Param("workspaceID")
// TODO: Call k8s manager to patch resource annotation
}

5
internal/k8s/define.go Normal file
View File

@@ -0,0 +1,5 @@
package k8s
const (
namespace = "educode"
)

45
internal/k8s/pvc.go Normal file
View File

@@ -0,0 +1,45 @@
package k8s
import (
"context"
"gitea.starryskymeow.cn/xkm/educode-controller/internal/types"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
func CreatePvc(req types.Pvc, client *kubernetes.Clientset) error {
pvc := &corev1.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{
Name: req.ID,
Namespace: namespace,
Labels: map[string]string{
"educode/uid": req.UID,
},
},
Spec: corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{
corev1.PersistentVolumeAccessMode(req.AccessMode),
},
StorageClassName: &req.StorageClassName,
Resources: corev1.VolumeResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceStorage: resource.MustParse(req.Limit),
},
},
},
}
_, err := client.CoreV1().PersistentVolumeClaims(namespace).Create(context.TODO(), pvc, metav1.CreateOptions{})
if err != nil {
return err
}
return nil
}
func DeletePvc(ID string, client *kubernetes.Clientset) error {
err := client.CoreV1().PersistentVolumeClaims(namespace).Delete(context.TODO(), ID, metav1.DeleteOptions{})
return err
}