Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d175a66394 | ||
|
|
9aa8b56922 |
@@ -17,9 +17,6 @@ func main() {
|
||||
|
||||
server := api.NewServer(clientset)
|
||||
|
||||
// TODO: Initialize controllers
|
||||
// TODO: Start everything
|
||||
|
||||
fmt.Println("Starting API server on :8080")
|
||||
if err := server.Run(":8080"); err != nil {
|
||||
panic(fmt.Sprintf("Failed to start API server: %v", err))
|
||||
|
||||
@@ -1,98 +1,20 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"gitea.starryskymeow.cn/xkm/educode-controller/internal/k8s"
|
||||
"github.com/gin-gonic/gin"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
)
|
||||
|
||||
const Namespace = "educode"
|
||||
|
||||
type Handler struct {
|
||||
clientset *kubernetes.Clientset
|
||||
}
|
||||
|
||||
// create handler with k8s clientset
|
||||
func NewHandler(clientset *kubernetes.Clientset) *Handler {
|
||||
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 {
|
||||
if len(s) > 63 {
|
||||
return false
|
||||
|
||||
46
internal/api/pvc.go
Normal file
46
internal/api/pvc.go
Normal 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
|
||||
}
|
||||
@@ -18,15 +18,21 @@ func NewServer(clientset *kubernetes.Clientset) *gin.Engine {
|
||||
})
|
||||
})
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
{
|
||||
workspaces := v1.Group("/workspaces")
|
||||
workspaces := r.Group("/workspaces")
|
||||
{
|
||||
workspaces.POST("", h.createWorkspace)
|
||||
workspaces.DELETE("/:workspaceID", h.deleteWorkspace)
|
||||
workspaces.GET("/:workspaceID", h.getWorkspace)
|
||||
workspaces.PATCH("/:workspaceID", h.extendWorkspace)
|
||||
workspaces.DELETE("/:ID", h.deleteWorkspace)
|
||||
workspaces.GET("/:ID", h.getWorkspaceByID)
|
||||
workspaces.GET("", h.getWorkspace)
|
||||
// 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
|
||||
|
||||
57
internal/api/workspace.go
Normal file
57
internal/api/workspace.go
Normal 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
5
internal/k8s/define.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package k8s
|
||||
|
||||
const (
|
||||
namespace = "educode"
|
||||
)
|
||||
45
internal/k8s/pvc.go
Normal file
45
internal/k8s/pvc.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user