Compare commits

..

10 Commits

Author SHA1 Message Date
w-mj b45ddb291f
running_project_model 2023-04-04 22:08:55 +08:00
w-mj b1ab2df214
curd on project 2023-04-04 21:10:33 +08:00
w-mj e4ddd0b445
project controller 2023-04-03 22:39:43 +08:00
w-mj a36de499c3
data curd 2023-04-03 21:10:40 +08:00
w-mj b50bd2c776
gin handler wrapper 2023-04-03 16:48:31 +08:00
w-mj 5e02216d65
base controller and base model 2023-04-02 22:51:34 +08:00
w-mj 66e9a60bf4
support webdav 2023-04-02 12:48:37 +08:00
w-mj 9e3f47b149
API Document 2023-04-01 21:30:52 +08:00
w-mj c40e273538
image curd 2023-04-01 17:10:14 +08:00
w-mj f22294056e
webdav image 2023-03-31 22:07:03 +08:00
28 changed files with 1195 additions and 312 deletions

192
API.md Normal file
View File

@ -0,0 +1,192 @@
# API 文档
## 总体设计
### 基本HTTP请求
本系统后端API设计遵循RESTful规范对于一般数据类型的增删改查使用同一URL下的POST、DELETE、PUT和GET方法进行访问。
对于POST和PUT方法待传输数据放在请求体中以Json格式进行传输。DELETE和GET方法使用URL参数传递必要数据。POST为上传新数据使用POST时数据中ID字段将被忽略。
后端返回数据也全部为Json格式使用HTTP状态码标识请求状态。当状态码为200时可以认为该请求成功达到了预期的结果。若请求失败则状态码不为200并且返回数据中包含`msg`
字段对发生的错误进行了简要说明。
一次失败的请求返回数据如下所示其状态码为401StatusUnauthorized
```json
{
"ok": false,
"msg": "username error"
}
```
一般数据类型包含`ID`字段类型为整数。DELETE方法可用此字段表示将要删除的对象。删除一个镜像的请求如下命令所示。
```bash
curl -X DELETE https://ms.educg.net/admin/image?ID=1
```
### 资源查询请求
对单个资源查询的方式与删除类似可使用该数据结构中的某些字段对数据进行查询返回结果为Json格式。
同时对该类型的多个资源进行查询时URL一般为该资源类型的复数形式如镜像的查询URL为`image`
那么同时获得多个镜像的查询的URL为`images`。特殊情况另行说明。
对多个数据进行查询时URL中除了必要的过滤条件外还可包含分页参数`page`和`pageSize`,均为整数类型。返回结果中`data`
字段为查询结果数据,`page`、`pageCount`和`pageSize`用于控制分页。
```json
{
"page": 1,
"pageCount": 100,
"pageSize": 2,
"data": [
{
"ID": 2,
"Other fields": "Other Values"
},
{
"ID": 3
}
]
}
```
### 数据结构
数据结构使用Golang语言格式进行定义其中`gorm.Model`为公用字段,定义如下:
```go
type Model struct {
ID uint `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt DeletedAt `gorm:"index"`
}
```
数据结构中包含的Comment字段为该数据的简单描述Doc字段为该数据的详细文档文档应支持Markdown格式保存和渲染。
在所有创建对象的请求POSTID字段将被忽略创建成功后自动分配ID。
## 镜像管理
数据结构:
```go
type ImageModel struct {
gorm.Model
Name string
Image string
Comment string
Command string
Env string
Port string
UserName string
IsPublished bool
Doc string
}
```
CURD:
+ POST/image
+ PUT: /image
+ DELETE: /image?ID=1
+ GET /images?UserName=cg_wmj # 查询某用户的镜像
+ GET /images?IsPublish=1 # 查询所有公开镜像
## 数据集管理
训练数据,数据结构:
```go
type DataModel struct {
gorm.Model
Type string
Name string
Comment string
Doc string
UserName string
AccessInfo string
IsPublished bool
}
```
其中`Type`字段为数据集的类型,表示该数据集的访问方式,目前仅支持`Type=="dir"`访问目录格式数据后续可能支持mysql等数据库。
本系统中对本地文件的访问使用Webdav协议。
`AccessInfo`字段为该数据集的访问方式,在`Type=="dir"`时为该目录的路径。
例如某数据集对象中,`AccessInfo=='/some/data/dir/'`,同时文件服务器地址为`https://ms.educg.net/files/ `那么通过WebDav协议访问`https://ms.educg.net/files/some/data/dir/ `即可访问该目录。
CURD:
+ POST/data
+ PUT: /data
+ DELETE: /data?ID=1
+ GET /data?UserName=cg_wmj
+ GET /data?IsPublish=1
## 项目管理
数据结构
```go
type ProjectModel struct {
gorm.Model
ProjectName string
UserName string
GroupName string
Type string
Comment string
Doc string
IsPublic bool
Image ImageModel
TrainData []*ProjectTrainDataModel `gorm:"foreignKey:ProjectID"`
UserPath string // 用户目录挂载位置
CodePath string // 代码目录挂载位置
ModelPath string // 模型目录挂载位置
Running bool `gorm:"-:all"`
}
type ProjectTrainDataModel struct {
BaseModel[ProjectTrainDataModel]
Project *ProjectModel
Data *DataModel
MountPath string
}
```
其中`Type`字段为项目的类型,可选值为`jupyter`、`jupyterlab`、`jupyternotebook`、`novnc`、`vscode`、`http`。
在创建或更新项目时,`TrainData`字段应该为DataModel结构的列表其中每一个DataModel仅有`ID`即可。
CURD:
+ POST/project
+ PUT: /project
+ DELETE: /project?ID=1
+ GET /projects?UserName=cg_wmj
+ GET /projects?IsPublish=1
## 运行项目 (尚未实现)
数据结构
```go
type RunningProject struct {
ID int
Project *ProjectModel
StartTime *time.Time
UserName string
Nodes int
Cpus int
Memory int
Gpus int
GpuMemory int
URL string
ControlURL string
}
```
运行一个项目前端发送POST请求时URL可留空若该项目成功启动则POST的返回内容中将包含正确的URL。
通过此URL即可访问项目服务。ControlURL为容器服务客户端连接地址可通过本地客户端提供更多功能。
CURD:
+ POST/running_project
+ DELETE: /running_project?ID=1
+ GET /running_projects?UserName=cg_wmj

View File

@ -23,6 +23,7 @@ restart: docker
install-dev: push
scp -i C:${HOMEPATH}/.ssh/id_rsa install-dev.yaml root@192.168.131.195:/root/install-backend-dev.yaml
ssh -i C:${HOMEPATH}/.ssh/id_rsa root@192.168.131.195 kubectl delete -f /root/install-backend-dev.yaml
ssh -i C:${HOMEPATH}/.ssh/id_rsa root@192.168.131.195 kubectl apply -f /root/install-backend-dev.yaml
push: docker

View File

@ -18,3 +18,10 @@ EOF
kubectl get secret dev-secret -o yaml
```
## 持续查看Log
```bash
while true; do bash -c "kubectl get pods | grep k8s-manager-deployment | awk '{print \$1}' | xargs kubectl logs -f"; sleep 1; done
```

View File

@ -22,7 +22,7 @@ spec:
- containerPort: 8080
env:
- name: GIN_MODE
value: release
value: debug
- name: URL_PREFIX
value: admin
- name: DB_HOST
@ -70,6 +70,7 @@ metadata:
name: manager-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/proxy-body-size: 10g
spec:
ingressClassName: nginx
rules:

10
main.go
View File

@ -73,6 +73,16 @@ func main() {
r.POST("/fork", controller.ForkApp)
// controller.AddWebdavHandler(r)
r.POST("/image", controller.AddImage)
r.PUT("/image", controller.UpdateImage)
r.DELETE("/image", controller.DeleteImage)
r.GET("/images", controller.GetImages)
controller.AddDataController(r)
controller.AddProjectController(r)
err := r.Run(":8000")
if err != nil {
log.Fatal(err)

View File

@ -0,0 +1,65 @@
package controller
import (
"errors"
"github.com/gin-gonic/gin"
"k8s-manager/src/model"
"k8s-manager/src/utils"
"net/http"
)
type BaseController[T model.ModalType, PT model.ModalInterface[T]] struct {
}
func (BaseController[T, PT]) Add(t *T) (utils.OKType, error) {
PT(t).Create()
return utils.OK, nil
}
func (BaseController[T, PT]) Delete(ID int) (utils.OKType, error) {
data := PT(new(T))
data.SetID(uint(ID))
data.Delete()
return utils.OK, nil
}
func (BaseController[T, PT]) Update(username string, t *T) (utils.OKType, error) {
data := PT(t)
oldData := PT(new(T))
oldData.SetID(data.GetID())
oldData.Fill()
if oldData.GetUserName() != username {
return nil, errors.New("wrong username")
}
data.Save()
return utils.OK, nil
}
func (BaseController[T, PT]) Gets(c *gin.Context) {
username := c.MustGet("username").(string)
var ans []any
query := map[string]interface{}{}
if c.Query("UserName") != "" {
if username != c.Query("UserName") {
utils.GinError(c, http.StatusUnauthorized, "username error")
return
}
query["UserName"] = username
} else if c.Query("IsPublic") == "1" {
query["IsPublic"] = true
} else {
utils.GinError(c, http.StatusBadRequest, "param error.")
return
}
ans = PT(new(T)).Gets(query)
c.JSON(http.StatusOK, gin.H{"data": ans})
}
//func AddCRUDRoute[T model.ModalType](r *gin.Engine, path string) {
// t := BaseController[T, *T]{}
// typeName := reflect.TypeOf((*T)(nil)).Name()
// utils.RouteWrapper(r, "POST", path, t.Add, []string{typeName})
// utils.RouteWrapper(r, "PUT", path, t.Update, []string{"username", typeName})
// utils.RouteWrapper(r, "DELETE", path, t.Delete, []string{"ID"})
// r.Handle("GET", path, t.Gets)
//}

View File

@ -0,0 +1,57 @@
package controller
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"k8s-manager/src/model"
"k8s-manager/src/utils"
"math/rand"
)
func AddData(username string, data *model.DataModel) (utils.OKType, error) {
data.UserName = username
data.AccessInfo = fmt.Sprintf("/data/%s-%s-%d", data.UserName, data.Name, rand.Int())
data.Create()
return utils.OK, nil
}
func DeleteData(username string, ID uint) (utils.OKType, error) {
oldData := model.DataModel{}
oldData.SetID(ID)
oldData.Fill()
if username != oldData.GetUserName() {
return nil, errors.New("wrong username")
}
oldData.Delete()
return utils.OK, nil
}
func UpdateData(username string, data *model.DataModel) (utils.OKType, error) {
oldData := &model.DataModel{}
oldData.SetID(data.ID)
oldData.Fill()
if username != oldData.GetUserName() {
return nil, errors.New("wrong username")
}
data.Save(oldData)
return utils.OK, nil
}
func GetData(UserName string, IsPublish bool) (gin.H, error) {
data := model.DataModel{}
data.UserName = UserName
data.IsPublished = IsPublish
if UserName == "" && !IsPublish {
return gin.H{"data": []string{}}, nil
}
ans := data.Gets()
return gin.H{"data": ans}, nil
}
func AddDataController(r *gin.Engine) {
utils.RouteWrapper(r, "POST", "/data", AddData, []string{"username", "1"})
utils.RouteWrapper(r, "DELETE", "/data", DeleteData, []string{"username", "ID"})
utils.RouteWrapper(r, "PUT", "/data", UpdateData, []string{"username", "1"})
utils.RouteWrapper(r, "GET", "/data", GetData, []string{"UserName", "IsPublished"})
}

View File

@ -6,6 +6,7 @@ import (
"k8s-manager/src/model"
utils "k8s-manager/src/utils"
"net/http"
"strconv"
)
func AddImage(c *gin.Context) {
@ -20,18 +21,23 @@ func AddImage(c *gin.Context) {
}
func DeleteImage(c *gin.Context) {
image := &model.ImageModel{}
if utils.GinErrorCheck(c, c.BindJSON(image)) {
id := c.Query("ID")
if id == "" {
utils.GinError(c, http.StatusBadRequest, "no id")
return
}
username := c.MustGet("username").(string)
oldImage := &model.ImageModel{Model: gorm.Model{ID: image.ID}}
idi, err := strconv.Atoi(id)
if utils.GinErrorCheck(c, err) {
return
}
oldImage := &model.ImageModel{Model: gorm.Model{ID: uint(idi)}}
oldImage.Fill()
if oldImage.UserName != username {
utils.GinError(c, http.StatusUnauthorized, "username error")
return
}
image.Delete()
oldImage.Delete()
}
func UpdateImage(c *gin.Context) {
@ -49,13 +55,20 @@ func UpdateImage(c *gin.Context) {
image.Save()
}
func MyImages(c *gin.Context) {
func GetImages(c *gin.Context) {
username := c.MustGet("username").(string)
myImages := model.GetMyImages(username)
c.JSON(http.StatusOK, gin.H{"images": myImages})
}
func PublicImages(c *gin.Context) {
myImages := model.GetPublicImages()
c.JSON(http.StatusOK, gin.H{"images": myImages})
var Images []model.ImageModel
if c.Query("UserName") != "" {
if username != c.Query("UserName") {
utils.GinError(c, http.StatusUnauthorized, "username error")
return
}
Images = model.GetMyImages(username)
} else if c.Query("IsPublic") != "" {
Images = model.GetPublicImages()
} else {
utils.GinError(c, http.StatusBadRequest, "param error.")
return
}
c.JSON(http.StatusOK, gin.H{"data": Images})
}

View File

@ -0,0 +1,54 @@
package controller
import (
"errors"
"github.com/gin-gonic/gin"
"k8s-manager/src/model"
"k8s-manager/src/utils"
)
func AddProject(username string, proj *model.ProjectModel) (utils.OKType, error) {
proj.UserName = username
proj.Create()
return utils.OK, nil
}
func DeleteProject(username string, ID uint) (utils.OKType, error) {
old := &model.ProjectModel{}
old.ID = ID
old.Fill()
if username != old.UserName {
return nil, errors.New("wrong username")
}
old.Delete()
return utils.OK, nil
}
func UpdateProject(username string, proj *model.ProjectModel) (utils.OKType, error) {
old := &model.ProjectModel{}
old.ID = proj.ID
old.Fill()
if username != old.GetUserName() {
return nil, errors.New("wrong username")
}
proj.Save(old)
return utils.OK, nil
}
func GetProjects(UserName string, IsPublished bool) (gin.H, error) {
proj := &model.ProjectModel{}
proj.UserName = UserName
proj.IsPublished = IsPublished
if UserName == "" && !IsPublished {
return gin.H{"data": []string{}}, nil
}
ans := proj.Gets()
return gin.H{"data": ans}, nil
}
func AddProjectController(r *gin.Engine) {
utils.RouteWrapper(r, "POST", "/project", AddProject, []string{"username", "1"})
utils.RouteWrapper(r, "DELETE", "/project", DeleteProject, []string{"username", "ID"})
utils.RouteWrapper(r, "PUT", "/project", UpdateProject, []string{"username", "1"})
utils.RouteWrapper(r, "GET", "/projects", GetProjects, []string{"UserName", "IsPublished"})
}

View File

@ -0,0 +1,8 @@
package controller
import "k8s-manager/src/model"
func CreateRunningProject(r *model.RunningProject) (*model.RunningProject, error) {
return r, nil
}

View File

@ -0,0 +1,52 @@
package controller
import (
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"golang.org/x/net/webdav"
"net/http"
"sync"
)
var fs *webdav.Handler
var fsLock sync.Mutex
func WebDav(c *gin.Context) {
if fs == nil {
fsLock.Lock()
if fs == nil {
fs = &webdav.Handler{
Prefix: "/webdav",
FileSystem: webdav.Dir("/mnt/CGdata/ms"),
LockSystem: webdav.NewMemLS(),
Logger: func(h *http.Request, e error) {
log.Error(e)
},
}
}
fsLock.Unlock()
}
log.Info("Webdav ", c.Request.Method, c.Request.URL.Path)
fs.ServeHTTP(c.Writer, c.Request)
}
func AddWebdavHandler(r *gin.Engine) {
methods := []string{
"OPTIONS",
"GET", "HEAD", "POST",
"DELETE",
"PUT",
"MKCOL",
"COPY", "MOVE",
"LOCK",
"UNLOCK",
"PROPFIND",
"PROPPATCH",
}
for _, m := range methods {
r.Handle(m, "/webdav", WebDav)
r.Handle(m, "/webdav/*path", WebDav)
}
}

View File

@ -48,6 +48,10 @@ func JWTAuthMiddleware(c *gin.Context) {
// 登录过程,不验证身份
return
}
if strings.HasPrefix(c.Request.URL.Path, "/webdav") {
// Webdav暂时不验证身份
return
}
authHeader := c.Request.Header.Get("Authorization")
if authHeader == "" {
var err error

View File

@ -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"
}

94
src/model/base_model.go Normal file
View File

@ -0,0 +1,94 @@
package model
import (
"gorm.io/gorm"
"gorm.io/gorm/clause"
k8s_manager "k8s-manager/src"
"reflect"
"unsafe"
)
type ModalType interface {
DataModel | ImageModel
}
type ModalInterface[T ModalType] interface {
*T
Create()
Fill()
Delete()
Save()
Gets(map[string]interface{}) []any
GetUserName() string
SetUserName(string)
SetID(uint)
GetID() uint
}
type BaseModel[T any] struct {
gorm.Model
}
func (b *BaseModel[T]) Fill() {
db := k8s_manager.DB()
p := (*T)(unsafe.Pointer(b))
db.Preload(clause.Associations).First(p, b.ID)
}
func (b *BaseModel[T]) Create() {
db := k8s_manager.DB()
p := (*T)(unsafe.Pointer(b))
db.Create(p)
}
func (b *BaseModel[T]) Save(old *T) {
db := k8s_manager.DB()
p := (*T)(unsafe.Pointer(b))
db.Save(p)
}
func (b *BaseModel[T]) Delete() {
db := k8s_manager.DB()
p := (*T)(unsafe.Pointer(b))
db.Delete(p)
}
func (b *BaseModel[T]) Gets() []T {
db := k8s_manager.DB()
ans := make([]T, 0)
p := (*T)(unsafe.Pointer(b))
db.Preload(clause.Associations).Where(p).Find(&ans)
return ans
}
//func (b *BaseModel[T]) Get(query map[string]interface{}) *T {
// db := k8s_manager.DB()
// p := (*T)(unsafe.Pointer(b))
// db.Where(query).First(p)
// return p
//}
func (b *BaseModel[T]) SetUserName(name string) {
p := (*T)(unsafe.Pointer(b))
a := reflect.ValueOf(p)
reflect.Indirect(a).FieldByName("UserName").SetString(name)
}
func (b *BaseModel[T]) SetID(ID uint) {
p := (*T)(unsafe.Pointer(b))
a := reflect.ValueOf(p)
reflect.Indirect(a).FieldByName("ID").SetUint(uint64(ID))
}
func (b *BaseModel[T]) GetUserName() string {
p := (*T)(unsafe.Pointer(b))
a := reflect.ValueOf(p)
return reflect.Indirect(a).FieldByName("UserName").String()
}
func (b *BaseModel[T]) GetID() uint {
p := (*T)(unsafe.Pointer(b))
a := reflect.ValueOf(p)
return uint(reflect.Indirect(a).FieldByName("ID").Uint())
}

View File

@ -1,23 +1,16 @@
package model
import (
"gorm.io/gorm"
k8s_manager "k8s-manager/src"
)
type DataModel struct {
gorm.Model
Type string
Name string
Comment string
AccessInfo string
BaseModel[DataModel]
Type string
Name string
Comment string
Doc string
UserName string
IsPublished bool
AccessInfo string
}
func (*DataModel) TableName() string {
return "data"
}
func (d *DataModel) Fill() {
db := k8s_manager.DB()
db.First(d, d.ID)
}

View File

@ -7,6 +7,7 @@ import (
type ImageModel struct {
gorm.Model
Name string
Image string
Comment string
Command string
@ -14,6 +15,7 @@ type ImageModel struct {
Port string
UserName string
IsPublished bool
Doc string
}
func (*ImageModel) TableName() string {
@ -40,6 +42,13 @@ func (i *ImageModel) Delete() {
db.Delete(i)
}
func GetImages(query map[string]interface{}) []ImageModel {
db := k8s_manager.DB()
ans := make([]ImageModel, 0)
db.Where(query).Find(&ans)
return ans
}
func GetMyImages(username string) []ImageModel {
db := k8s_manager.DB()
ans := make([]ImageModel, 0)

View File

@ -1,34 +1,272 @@
package model
import (
"gorm.io/gorm"
"fmt"
k8s_manager "k8s-manager/src"
"k8s-manager/src/utils"
v1 "k8s.io/api/core/v1"
"strings"
"time"
"unicode"
)
type ProjectModel struct {
gorm.Model
BaseModel[ProjectModel]
ProjectName string
UserName string
GroupName string
Type string
Comment string
Doc string
IsPublished bool
ImageID int
Image ImageModel
TrainData []*DataModel `gorm:"many2many:projects_data"`
TrainData []*ProjectTrainDataModel `gorm:"foreignKey:ProjectID"`
UserPath string
CodePath string
ModelPath string
Running bool `gorm:"-:all"`
}
func (*ProjectModel) TableName() string {
return "projects"
}
type RunningProject struct {
type ProjectTrainDataModel struct {
BaseModel[ProjectTrainDataModel]
ProjectID int
Project *ProjectModel
StartTime *time.Time
UserName string
Nodes int
Cpus int
Memory int
Gpus int
GpuMemory int
DataID int
Data *DataModel
MountPath string
}
func (*ProjectTrainDataModel) TableName() string {
return "project_train_data"
}
//func (p *ProjectModel) Fill() {
// db := k8s_manager.DB()
// db.Preload(clause.Associations).First(p, p.ID)
//}
//
//func (p *ProjectModel) Create() {
// db := k8s_manager.DB()
// db.Create(p)
//}
func (p *ProjectModel) Delete() {
db := k8s_manager.DB()
for _, d := range p.TrainData {
db.Delete(d)
}
db.Delete(p)
}
func (p *ProjectModel) Save(oldProject *ProjectModel) {
db := k8s_manager.DB()
//oldProject := &ProjectModel{}
//oldProject.ID = p.ID
//oldProject.Fill()
//toAdd := utils.Difference(p.TrainData, oldProject.TrainData, func(a, b ProjectTrainDataModel) bool { return a.ID == b.ID })
//for t := range toAdd {
// db.Create(t)
//}
//
toDel := utils.Difference(oldProject.TrainData, p.TrainData, func(a, b *ProjectTrainDataModel) bool { return a.ID == b.ID })
for _, t := range toDel {
db.Delete(t)
}
toSave := utils.Intersect(p.TrainData, oldProject.TrainData, func(a, b *ProjectTrainDataModel) bool { return a.ID == b.ID })
for _, t := range toSave {
t.Project = p
db.Save(t)
}
db.Save(p)
}
//func (p *ProjectModel) Gets() []ProjectModel {
// db := k8s_manager.DB()
// var ans []ProjectModel
// db.Model(&ProjectModel{}).Preload(clause.Associations).Where(p).Find(&ans)
// return ans
//}
type RunningProject struct {
Project *ProjectModel
StartTime *time.Time
UserName string
Nodes int
Cpus int
Memory int
Gpus int
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
}

View File

@ -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(),

View File

@ -0,0 +1,31 @@
package utils
func Difference[T any](a, b []T, com func(T, T) bool) []T {
var ans []T
for _, x := range a {
unique := true
for _, y := range b {
if com(x, y) {
unique = false
break
}
}
if unique {
ans = append(ans, x)
}
}
return ans
}
func Intersect[T any](a, b []T, com func(T, T) bool) []T {
var ans []T
for _, x := range a {
for _, y := range b {
if com(x, y) {
ans = append(ans, x)
break
}
}
}
return ans
}

146
src/utils/gin_utils.go Normal file
View File

@ -0,0 +1,146 @@
package utils
import (
"encoding/json"
"errors"
"fmt"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"net/http"
"reflect"
"strconv"
)
func UnnamedPointer[T any](v T) *T {
return &v
}
func GinError(c *gin.Context, code int, msg string) {
c.JSON(code, gin.H{"ok": false, "msg": msg})
}
func GinOk(c *gin.Context, msg string) {
c.JSON(http.StatusOK, gin.H{"ok": true, "msg": msg})
}
func GinOkData(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, gin.H{"ok": true, "data": data})
}
func GinErrorCheck(c *gin.Context, err error) bool {
if err != nil {
GinError(c, http.StatusBadRequest, err.Error())
return true
}
return false
}
func setValue(data string, value reflect.Value) error {
switch value.Kind() {
case reflect.Bool:
v, err := strconv.ParseBool(data)
if err != nil {
return err
}
value.SetBool(v)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
v, err := strconv.ParseInt(data, 10, 64)
if err != nil {
return err
}
value.SetInt(v)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
v, err := strconv.ParseUint(data, 10, 64)
if err != nil {
return err
}
value.SetUint(v)
case reflect.Float32, reflect.Float64:
v, err := strconv.ParseFloat(data, 64)
if err != nil {
return err
}
value.SetFloat(v)
case reflect.Pointer:
return setValue(data, reflect.Indirect(value))
case reflect.String:
value.SetString(data)
case reflect.Struct:
err := json.Unmarshal([]byte(data), value.Pointer())
if err != nil {
return err
}
default:
log.Errorf("utils.setValue unsupported type %s", value.Type().Name())
return errors.New("unsupported type")
}
return nil
}
type OKType = map[string]any
var OK = OKType{"ok": true}
func routeWrapperHandler(handler any, names []string) func(ctx *gin.Context) {
return func(c *gin.Context) {
handlerFunc := reflect.ValueOf(handler)
handlerFuncType := handlerFunc.Type()
handlerFuncParamLen := handlerFuncType.NumIn()
if handlerFuncParamLen != len(names) {
panic("Handler func Param len != len(names)")
}
inputArgs := make([]reflect.Value, handlerFuncParamLen)
for i := 0; i < handlerFuncParamLen; i += 1 {
paraType := handlerFuncType.In(i)
var paraValue reflect.Value
if paraType.Kind() == reflect.Pointer {
paraValue = reflect.New(paraType.Elem())
} else {
paraValue = reflect.New(paraType)
}
v, ok := c.Get(names[i])
if ok {
t := reflect.ValueOf(v)
paraValue.Elem().Set(t)
} else {
vs := c.Param(names[i])
if vs == "" {
vs = c.Query(names[i])
}
if vs != "" {
err := setValue(vs, paraValue)
if err != nil {
log.WithError(err).Error("Cannot set value")
continue
}
} else {
if paraValue.Kind() == reflect.Struct || (paraType.Kind() == reflect.Pointer && paraType.Elem().Kind() == reflect.Struct) {
err := c.BindJSON(paraValue.Interface())
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"ok": false,
"msg": fmt.Sprintf("Cannot BindJson %s: %s", paraType.Name(), err.Error())})
return
}
}
}
}
if paraType.Kind() == reflect.Pointer {
inputArgs[i] = paraValue
} else {
inputArgs[i] = paraValue.Elem()
}
}
out := handlerFunc.Call(inputArgs)
var ret = out[0].Interface()
if out[1].Interface() != nil {
var err = out[1].Interface().(error)
c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "msg": err.Error()})
} else {
c.JSON(http.StatusOK, ret)
}
}
}
func RouteWrapper(r *gin.Engine, method string, path string, handler any, names []string) {
r.Handle(method, path, routeWrapperHandler(handler, names))
}

View File

@ -1,30 +0,0 @@
package utils
import (
"github.com/gin-gonic/gin"
"net/http"
)
func UnnamedPointer[T any](v T) *T {
return &v
}
func GinError(c *gin.Context, code int, msg string) {
c.JSON(code, gin.H{"ok": false, "msg": msg})
}
func GinOk(c *gin.Context, msg string) {
c.JSON(http.StatusOK, gin.H{"ok": true, "msg": msg})
}
func GinOkData(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, gin.H{"ok": true, "data": data})
}
func GinErrorCheck(c *gin.Context, err error) bool {
if err != nil {
GinError(c, http.StatusBadRequest, err.Error())
return true
}
return false
}

5
test/test_generic.go Normal file
View File

@ -0,0 +1,5 @@
package main
func main() {
//controller.BaseController[model.DataModel, *model.DataModel]{}.Add(nil)
}

25
test/test_wrapper.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"encoding/json"
"fmt"
"github.com/gin-gonic/gin"
"k8s-manager/src/model"
"k8s-manager/src/utils"
)
func DataHandler(model model.DataModel, q1 string, q2 int) (map[string]any, error) {
s, _ := json.Marshal(model)
fmt.Println(string(s))
fmt.Printf("Call DataHandler %s %d\n", q1, q2)
return gin.H{"name": model.Name}, nil
}
func main() {
r := gin.Default()
utils.RouteWrapper(r, "GET", "/index", DataHandler, []string{"model", "q1", "q2"})
err := r.Run(":9000")
if err != nil {
fmt.Println(err)
}
}

View File

@ -1,7 +1,6 @@
package main
import (
"encoding/json"
"fmt"
k8s_manager "k8s-manager/src"
"k8s-manager/src/model"
@ -10,9 +9,10 @@ import (
func CreateTables() {
db := k8s_manager.DB()
err := db.Migrator().AutoMigrate(
&model.ProjectModel{},
&model.ImageModel{},
&model.DataModel{},
//&model.ProjectModel{},
//&model.ImageModel{},
//&model.DataModel{},
&model.ProjectTrainDataModel{},
)
if err != nil {
fmt.Println(err)
@ -22,9 +22,10 @@ func CreateTables() {
func main() {
k8s_manager.InitConfig()
db := k8s_manager.DB()
p := model.ProjectModel{}
db.Preload("TrainData").Where("user_name = ?", "wmj").Find(&p)
j, _ := json.Marshal(&p)
fmt.Println(string(j))
CreateTables()
// db := k8s_manager.DB()
// p := model.ProjectModel{}
// db.Preload("TrainData").Where("user_name = ?", "wmj").Find(&p)
// j, _ := json.Marshal(&p)
// fmt.Println(string(j))
}

3
webdav/Dockerfile Normal file
View File

@ -0,0 +1,3 @@
FROM ugeek/webdav:amd64
COPY entrypoint.sh /

7
webdav/Makefile Normal file
View File

@ -0,0 +1,7 @@
TAG = docker.educg.net/cg/webdav:v1.0
build:
docker build -t $(TAG) .
push:
docker push $(TAG)

11
webdav/entrypoint.sh Normal file
View File

@ -0,0 +1,11 @@
#!/bin/bash
if [ -n "$USERNAME" ] && [ -n "$PASSWORD" ]
then
htpasswd -bc /etc/nginx/htpasswd $USERNAME $PASSWORD
echo Done.
else
echo Using no auth.
sed -i 's%auth_basic "Restricted";% %g' /etc/nginx/conf.d/default.conf
sed -i 's%auth_basic_user_file htpasswd;% %g' /etc/nginx/conf.d/default.conf
fi

71
webdav/webdav.yaml Normal file
View File

@ -0,0 +1,71 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: cg-webdav
labels:
app: cg-webdav
spec:
replicas: 1
selector:
matchLabels:
app: cg-webdav
template:
metadata:
labels:
app: cg-webdav
spec:
containers:
- name: cg-webdav-container
# image: bytemark/webdav:2.4
image: docker.educg.net/cg/webdav:v1.0
# command: ["/bin/bash"]
# args: ["-c", "sleep 365d"]
ports:
- containerPort: 80
env:
- name: TZ
value: Asia/Shanghai
volumeMounts:
- name: webdav-nfs
mountPath: /media
# mountPath: /var/lib/dav
volumes:
- name: webdav-nfs
nfs:
server: 192.168.252.250
path: /mnt/CGdata/ms
---
apiVersion: v1
kind: Service
metadata:
name: cg-webdav-svc
labels:
name: cg-webdav-svc
spec:
ports:
- port: 80
targetPort: 80
selector:
app: cg-webdav
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: webdav-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
nginx.ingress.kubernetes.io/proxy-body-size: 10g
nginx.ingress.kubernetes.io/proxy-buffering: "on"
nginx.ingress.kubernetes.io/proxy-buffer-size: "1024k"
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /files(/|$)(.*)
pathType: Prefix
backend:
service:
name: cg-webdav-svc
port:
number: 80