Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d175a66394 | ||
|
|
9aa8b56922 |
@@ -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))
|
||||||
|
|||||||
@@ -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
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 := 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
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