diff --git a/API.md b/API.md index 51fdea6..122cf8a 100644 --- a/API.md +++ b/API.md @@ -64,6 +64,7 @@ type Model struct { ``` 数据结构中包含的Comment字段为该数据的简单描述,Doc字段为该数据的详细文档,文档应支持Markdown格式保存和渲染。 +在所有创建对象的请求(POST)中,ID字段将被忽略,创建成功后自动分配ID。 ## 镜像管理 @@ -90,7 +91,7 @@ CURD: + GET /images?UserName=cg_wmj # 查询某用户的镜像 + GET /images?IsPublish=1 # 查询所有公开镜像 -## 数据集管理 (尚未实现) +## 数据集管理 训练数据,数据结构: ```go @@ -118,7 +119,7 @@ CURD: + GET /data?UserName=cg_wmj + GET /data?IsPublish=1 -## 项目管理 (尚未实现) +## 项目管理 数据结构 ```go @@ -132,9 +133,8 @@ type ProjectModel struct { Doc string IsPublic bool - ImageID int - Image ImageModel - TrainData []*DataModel `gorm:"many2many:projects_data"` + Image ImageModel + TrainData []*ProjectTrainDataModel `gorm:"foreignKey:ProjectID"` UserPath string // 用户目录挂载位置 @@ -143,6 +143,14 @@ type ProjectModel struct { Running bool `gorm:"-:all"` } + + +type ProjectTrainDataModel struct { + BaseModel[ProjectTrainDataModel] + Project *ProjectModel + Data *DataModel + MountPath string +} ``` 其中`Type`字段为项目的类型,可选值为`jupyter`、`jupyterlab`、`jupyternotebook`、`novnc`、`vscode`、`http`。 @@ -161,6 +169,7 @@ CURD: 数据结构 ```go type RunningProject struct { + ID int Project *ProjectModel StartTime *time.Time UserName string @@ -177,9 +186,7 @@ type RunningProject struct { 运行一个项目,前端发送POST请求时URL可留空,若该项目成功启动,则POST的返回内容中将包含正确的URL。 通过此URL即可访问项目服务。ControlURL为容器服务客户端连接地址,可通过本地客户端提供更多功能。 -此数据结构中没有ID字段,删除时使用其对应的ProjectModel的ID。 - CURD: + POST:/running_project -+ DELETE: /running_project?ProjectID=1 ++ DELETE: /running_project?ID=1 + GET /running_projects?UserName=cg_wmj diff --git a/src/controller/running_project_controller.go b/src/controller/running_project_controller.go new file mode 100644 index 0000000..8a7a7fc --- /dev/null +++ b/src/controller/running_project_controller.go @@ -0,0 +1,8 @@ +package controller + +import "k8s-manager/src/model" + +func CreateRunningProject(r *model.RunningProject) (*model.RunningProject, error) { + + return r, nil +} diff --git a/src/model/app_model.go b/src/model/app_model.go deleted file mode 100644 index 91a954a..0000000 --- a/src/model/app_model.go +++ /dev/null @@ -1,197 +0,0 @@ -package model - -import ( - "fmt" - "k8s-manager/src" - v1 "k8s.io/api/core/v1" - "strings" - "time" - "unicode" -) - -type AppModel struct { - Id int - AppName string - UserName string - Image string - Command string - Comment string - Env string - SvcName string - SvcRemovePrefix bool - Ports string - PersistentPath string - OriginId int - CreateTime time.Time - UpdateTime time.Time - IsPublished bool - AppType string -} - -func (*AppModel) TableName() string { - return "app" -} - -func GetApps(username string) []AppModel { - db := k8s_manager.DB() - ans := make([]AppModel, 0) - db.Where("user_name = ?", username).Find(&ans) - return ans -} - -func (a *AppModel) Fill() error { - db := k8s_manager.DB() - res := db.First(a, a.Id) - return res.Error -} - -func (a *AppModel) fillDefaults() { - if a.SvcName == "" { - a.SvcName = "svc" - } - a.SvcName = strings.TrimPrefix(a.SvcName, a.NamePrefix()+"-") - if a.PersistentPath == "" { - a.PersistentPath = "/data/" - } -} - -func (a *AppModel) Update() error { - a.fillDefaults() - a.UpdateTime = time.Now() - db := k8s_manager.DB() - db.Save(a) - return nil -} - -func (a *AppModel) Create() error { - a.fillDefaults() - a.CreateTime = time.Now() - a.UpdateTime = time.Now() - db := k8s_manager.DB() - db.Create(a) - return nil -} - -func (a *AppModel) Delete() error { - db := k8s_manager.DB() - db.Delete(a, a.Id) - return nil -} - -func (a *AppModel) NamePrefix() string { - t := "app" - if a.OriginId > 0 { - t = "fork" - } - prefix := fmt.Sprintf("%s-%s-%s-%d", t, a.UserName, a.AppName, a.Id) - ans := strings.Builder{} - for _, c := range prefix { - if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' { - ans.WriteRune(c) - } else { - ans.WriteString(fmt.Sprintf("-%d-", c)) - } - } - return ans.String() -} - -func (a *AppModel) PodName() string { - return fmt.Sprintf("%s-pod", a.NamePrefix()) -} - -func (a *AppModel) ContainerName() string { - return fmt.Sprintf("%s-container", a.NamePrefix()) -} - -func (a *AppModel) ServiceName() string { - if len(a.GetPorts()) == 0 { - return "" - } - if strings.TrimSpace(a.SvcName) != "" { - return fmt.Sprintf("%s-%s", a.NamePrefix(), a.SvcName) - } - return a.DefaultServiceName() -} - -func (a *AppModel) DefaultServiceName() string { - return fmt.Sprintf("%s-svc", a.NamePrefix()) -} - -func (a *AppModel) DeploymentName() string { - return fmt.Sprintf("%s-deployment", a.NamePrefix()) -} - -func (a *AppModel) GetCommandOrBlack() []string { - if strings.TrimSpace(a.Command) != "" { - t := a.Command - t = strings.ReplaceAll(t, "$URL_PREFIX", "svc/"+a.URLPrefix()) - return []string{"/bin/sh", "-c", t} - } else { - return nil - } -} - -func (a *AppModel) GetLabel() (string, string) { - return "cg-label", fmt.Sprintf("%s-label", a.NamePrefix()) -} - -func (a *AppModel) GetEnv() []v1.EnvVar { - if strings.TrimSpace(a.Env) == "" { - return nil - } - sep1 := strings.Split(a.Env, " ") - ans := make([]v1.EnvVar, len(sep1)) - for i, v := range sep1 { - kv := strings.SplitN(v, "=", 2) - ans[i].Name = kv[0] - if len(kv) == 2 { - ans[i].Value = kv[1] - } - } - return ans -} - -func (a *AppModel) GetPorts() []int32 { - ans := make([]int32, 0) - var cur int32 = 0 - for _, c := range a.Ports { - if unicode.IsNumber(c) { - cur = cur*10 + (c - '0') - } else if cur > 0 { - ans = append(ans, cur) - cur = 0 - } - } - if cur > 0 { - ans = append(ans, cur) - } - return ans -} - -func (a *AppModel) IngressName() string { - return fmt.Sprintf("%s-ingress", a.NamePrefix()) -} - -func (a *AppModel) URLPrefix() string { - return a.ServiceName() -} - -func (a *AppModel) PersistentVolumeName() string { - return a.UserName + "-pv" -} - -func (a *AppModel) NFSPath() string { - return k8s_manager.GetConfig().NFSPath + "/" + a.UserName + "/" + a.AppName -} - -func (a *AppModel) PersistentVolumeClaimName() string { - return a.NamePrefix() + "-pvc" -} - -func (a *AppModel) StorageClassName() string { - return a.NamePrefix() + "-storage-class" -} - -func (a *AppModel) PersistentVolumeMountName() string { - return a.NamePrefix() + "-pvm" -} diff --git a/src/model/project_model.go b/src/model/project_model.go index e91d990..c921b10 100644 --- a/src/model/project_model.go +++ b/src/model/project_model.go @@ -1,9 +1,13 @@ package model import ( + "fmt" k8s_manager "k8s-manager/src" "k8s-manager/src/utils" + v1 "k8s.io/api/core/v1" + "strings" "time" + "unicode" ) type ProjectModel struct { @@ -105,4 +109,164 @@ type RunningProject struct { GpuMemory int URL string ControlURL string + + ID int + SvcRemovePrefix bool +} + +func (a *RunningProject) NamePrefix() string { + t := "app" + prefix := fmt.Sprintf("%s-%s-%s-%d", t, a.UserName, a.Project.ProjectName, a.ID) + ans := strings.Builder{} + for _, c := range prefix { + if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' { + ans.WriteRune(c) + } else { + ans.WriteString(fmt.Sprintf("-%d-", c)) + } + } + return ans.String() +} + +func (a *RunningProject) PodName() string { + return fmt.Sprintf("%s-pod", a.NamePrefix()) +} + +func (a *RunningProject) ContainerName() string { + return fmt.Sprintf("%s-container", a.NamePrefix()) +} + +func (a *RunningProject) ServiceName() string { + if len(a.GetPorts()) == 0 { + return "" + } + return a.DefaultServiceName() +} + +func (a *RunningProject) DefaultServiceName() string { + return fmt.Sprintf("%s-svc", a.NamePrefix()) +} + +func (a *RunningProject) DeploymentName() string { + return fmt.Sprintf("%s-deployment", a.NamePrefix()) +} + +func (a *RunningProject) GetCommandOrBlack() []string { + if strings.TrimSpace(a.Project.Image.Command) != "" { + t := a.Project.Image.Command + t = strings.ReplaceAll(t, "$URL_PREFIX", "svc/"+a.URLPrefix()) + return []string{"/bin/sh", "-c", t} + } else { + return nil + } +} + +func (a *RunningProject) GetLabel() (string, string) { + return "cg-label", fmt.Sprintf("%s-label", a.NamePrefix()) +} + +func (a *RunningProject) GetEnv() []v1.EnvVar { + if strings.TrimSpace(a.Project.Image.Env) == "" { + return nil + } + sep1 := strings.Split(a.Project.Image.Env, " ") + ans := make([]v1.EnvVar, len(sep1)) + for i, v := range sep1 { + kv := strings.SplitN(v, "=", 2) + ans[i].Name = kv[0] + if len(kv) == 2 { + ans[i].Value = kv[1] + } + } + return ans +} + +func (a *RunningProject) GetPorts() []int32 { + ans := make([]int32, 0) + var cur int32 = 0 + for _, c := range a.Project.Image.Port { + if unicode.IsNumber(c) { + cur = cur*10 + (c - '0') + } else if cur > 0 { + ans = append(ans, cur) + cur = 0 + } + } + if cur > 0 { + ans = append(ans, cur) + } + return ans +} + +func (a *RunningProject) IngressName() string { + return fmt.Sprintf("%s-ingress", a.NamePrefix()) +} + +func (a *RunningProject) URLPrefix() string { + return a.ServiceName() +} + +func (a *RunningProject) PersistentVolumeName() string { + return a.UserName + "-pv" +} + +func (a *RunningProject) NFSPath() string { + return k8s_manager.GetConfig().NFSPath + "/" + a.UserName + "/" + a.Project.ProjectName +} + +func (a *RunningProject) PersistentVolumeClaimName() string { + return a.NamePrefix() + "-pvc" +} + +func (a *RunningProject) StorageClassName() string { + return a.NamePrefix() + "-storage-class" +} + +func (a *RunningProject) PersistentVolumeMountName() string { + return a.NamePrefix() + "-pvm" +} + +type NFSMount struct { + Name string + From string + To string + ReadOnly bool +} + +func (a *RunningProject) createNFSMount(name string, from string, to string, readonly bool) NFSMount { + return NFSMount{ + Name: fmt.Sprintf("%s-nfs-%s", a.NamePrefix(), name), + From: from, + To: to, + ReadOnly: readonly, + } +} + +func (a *RunningProject) GetNFSMounts() []NFSMount { + var ans []NFSMount + if a.Project.UserPath != "" { + ans = append(ans, a.createNFSMount("user", + fmt.Sprintf("/users/%s/home", a.UserName), a.Project.UserPath, false)) + } + if a.Project.CodePath != "" { + ans = append(ans, a.createNFSMount("code", + fmt.Sprintf("/users/%s/code/%s", a.UserName, a.Project.ProjectName), a.Project.CodePath, false)) + } + if a.Project.CodePath != "" { + ans = append(ans, a.createNFSMount("model", + fmt.Sprintf("/users/%s/model/%s", a.UserName, a.Project.ProjectName), a.Project.ModelPath, false)) + } + for i, m := range a.Project.TrainData { + if m.Data.Type != "dir" { + continue + } + ans = append(ans, a.createNFSMount( + fmt.Sprintf("%s-%d", m.Data.Name, i), + m.Data.AccessInfo, + m.MountPath, + true, + ), + ) + } + return ans } diff --git a/src/service/k8s_service.go b/src/service/k8s_service.go index 5792afa..cdaa845 100644 --- a/src/service/k8s_service.go +++ b/src/service/k8s_service.go @@ -73,24 +73,53 @@ func (c *K8sClient) DeploymentExists(deploymentName string) bool { return false } -func (c *K8sClient) UpdateDeployment(app *model.AppModel) { - labelKey, labelValue := app.GetLabel() - l := map[string]string{labelKey: labelValue} - if _, err := os.Stat(app.NFSPath()); os.IsNotExist(err) { - err := os.MkdirAll(app.NFSPath(), 0777) - if err != nil { - log.WithError(err).Error("Make nfs dir error") +func checkDirs(mounts []model.NFSMount) { + for _, n := range mounts { + if _, err := os.Stat(n.From); os.IsNotExist(err) { + err := os.MkdirAll(n.From, 0777) + if err != nil { + log.WithError(err).Error("Make nfs dir error") + } } } +} + +func (c *K8sClient) UpdateDeployment(app *model.RunningProject) { + labelKey, labelValue := app.GetLabel() + l := map[string]string{labelKey: labelValue} + nfsDirs := app.GetNFSMounts() + checkDirs(nfsDirs) + var volumeMounts []v1.VolumeMount + var volumes []v1.Volume + for _, n := range nfsDirs { + volumeMounts = append(volumeMounts, v1.VolumeMount{ + Name: n.Name, + MountPath: n.To, + ReadOnly: n.ReadOnly, + }) + volumes = append(volumes, v1.Volume{ + Name: n.Name, + VolumeSource: v1.VolumeSource{ + NFS: &v1.NFSVolumeSource{ + Server: k8s_manager.GetConfig().NFSPath, + Path: n.From, + ReadOnly: n.ReadOnly, + }, + }, + }) + } + podLabels := map[string]string{ labelKey: labelValue, "cg-deployment": app.DeploymentName(), } + containerPorts := funk.Map(app.GetPorts(), func(port int32) v1.ContainerPort { return v1.ContainerPort{ ContainerPort: port, } }).([]v1.ContainerPort) + deployment := &appv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: app.DeploymentName(), @@ -108,36 +137,19 @@ func (c *K8sClient) UpdateDeployment(app *model.AppModel) { Spec: v1.PodSpec{ Containers: []v1.Container{{ Name: app.ContainerName(), - Image: app.Image, + Image: app.Project.Image.Image, Command: app.GetCommandOrBlack(), Env: append(app.GetEnv(), v1.EnvVar{ Name: "URL_PREFIX", Value: app.URLPrefix(), }), - VolumeMounts: []v1.VolumeMount{ - { - Name: app.PersistentVolumeMountName(), - ReadOnly: false, - MountPath: app.PersistentPath, - }, - }, - Ports: containerPorts, + VolumeMounts: volumeMounts, + Ports: containerPorts, SecurityContext: &v1.SecurityContext{ RunAsUser: utils.UnnamedPointer(int64(0)), }, }}, - Volumes: []v1.Volume{ - { - Name: app.PersistentVolumeMountName(), - VolumeSource: v1.VolumeSource{ - NFS: &v1.NFSVolumeSource{ - Server: k8s_manager.GetConfig().NFSHost, - Path: app.NFSPath(), - ReadOnly: false, - }, - }, - }, - }, + Volumes: volumes, }, }, }, @@ -166,7 +178,7 @@ func (c *K8sClient) ServiceExists(servername string) bool { return false } -func (c *K8sClient) UpdateService(app *model.AppModel) { +func (c *K8sClient) UpdateService(app *model.RunningProject) { ports := app.GetPorts() if len(ports) == 0 { return @@ -210,19 +222,19 @@ func (c *K8sClient) UpdateService(app *model.AppModel) { } -func (c *K8sClient) CreateApp(app *model.AppModel) { +func (c *K8sClient) CreateApp(app *model.RunningProject) { c.UpdateDeployment(app) c.UpdateService(app) c.UpdateIngress(app) } -func (c *K8sClient) DeleteApp(app *model.AppModel) { +func (c *K8sClient) DeleteApp(app *model.RunningProject) { c.DeleteDeployment(app.DeploymentName()) c.DeleteAppService(app) c.DeleteIngress(app.IngressName()) } -func (c *K8sClient) DeleteAppService(app *model.AppModel) { +func (c *K8sClient) DeleteAppService(app *model.RunningProject) { k, v := app.GetLabel() label := labels.FormatLabels(map[string]string{k: v}) svcs, err := c.CoreV1().Services(c.Namespace).List(context.Background(), metav1.ListOptions{ @@ -294,7 +306,7 @@ func (c *K8sClient) IngressExists(name string) bool { return false } -func (c *K8sClient) UpdateIngress(a *model.AppModel) { +func (c *K8sClient) UpdateIngress(a *model.RunningProject) { ports := a.GetPorts() if len(ports) == 0 { return @@ -363,7 +375,7 @@ func (c *K8sClient) DeleteIngress(name string) { } /* -func (c *K8sClient) PersistentVolumeExists(app *AppModel) bool { +func (c *K8sClient) PersistentVolumeExists(app *RunningProject) bool { _, err := c.CoreV1().PersistentVolumes().Get(context.Background(), app.PersistentVolumeName(), metav1.GetOptions{}) if err == nil { return true @@ -374,7 +386,7 @@ func (c *K8sClient) PersistentVolumeExists(app *AppModel) bool { return false } -func (c *K8sClient) CreatePersistentVolume(app *AppModel) { +func (c *K8sClient) CreatePersistentVolume(app *RunningProject) { cap1, _ := resource.ParseQuantity("1Gi") pv := &v1.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{ @@ -404,7 +416,7 @@ func (c *K8sClient) CreatePersistentVolume(app *AppModel) { } } -func (c *K8sClient) PersistentVolumeClaimExists(app *AppModel) bool { +func (c *K8sClient) PersistentVolumeClaimExists(app *RunningProject) bool { _, err := c.CoreV1().PersistentVolumeClaims(c.Namespace).Get(context.Background(), app.PersistentVolumeClaimName(), metav1.GetOptions{}) if err == nil { return true @@ -415,7 +427,7 @@ func (c *K8sClient) PersistentVolumeClaimExists(app *AppModel) bool { return false } -func (c *K8sClient) CreatePersistentVolumeClaim(app *AppModel) { +func (c *K8sClient) CreatePersistentVolumeClaim(app *RunningProject) { pvc := &v1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: app.PersistentVolumeClaimName(),