| @@ -43,6 +43,7 @@ require ( | |||
| github.com/go-ini/ini v1.56.0 // indirect | |||
| github.com/go-openapi/jsonreference v0.19.3 // indirect | |||
| github.com/go-redis/redis v6.15.2+incompatible | |||
| github.com/go-resty/resty/v2 v2.3.0 | |||
| github.com/go-sql-driver/mysql v1.4.1 | |||
| github.com/go-swagger/go-swagger v0.21.0 | |||
| github.com/gobwas/glob v0.2.3 | |||
| @@ -106,7 +107,7 @@ require ( | |||
| github.com/yuin/goldmark-meta v0.0.0-20191126180153-f0638e958b60 | |||
| golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 | |||
| golang.org/x/mod v0.3.0 // indirect | |||
| golang.org/x/net v0.0.0-20200506145744-7e3656a0809f | |||
| golang.org/x/net v0.0.0-20200513185701-a91f0712d120 | |||
| golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d | |||
| golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f | |||
| golang.org/x/text v0.3.2 | |||
| @@ -270,6 +270,9 @@ github.com/go-openapi/validate v0.19.3 h1:PAH/2DylwWcIU1s0Y7k3yNmeAgWOcKrNE2Q7Ww | |||
| github.com/go-openapi/validate v0.19.3/go.mod h1:90Vh6jjkTn+OT1Eefm0ZixWNFjhtOH7vS9k0lo6zwJo= | |||
| github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4= | |||
| github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= | |||
| github.com/go-resty/resty v1.12.0 h1:L1P5qymrXL5H/doXe2pKUr1wxovAI5ilm2LdVLbwThc= | |||
| github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So= | |||
| github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU= | |||
| github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= | |||
| github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= | |||
| github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= | |||
| @@ -740,6 +743,8 @@ golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3ob | |||
| golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |||
| golang.org/x/net v0.0.0-20200506145744-7e3656a0809f h1:QBjCr1Fz5kw158VqdE9JfI9cJnl/ymnJWAdMuinqL7Y= | |||
| golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= | |||
| golang.org/x/net v0.0.0-20200513185701-a91f0712d120 h1:EZ3cVSzKOlJxAd8e8YAJ7no8nNypTxexh/YE/xW3ZEY= | |||
| golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= | |||
| golang.org/x/oauth2 v0.0.0-20180620175406-ef147856a6dd/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | |||
| golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | |||
| golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | |||
| @@ -0,0 +1,210 @@ | |||
| package models | |||
| import ( | |||
| "fmt" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| "code.gitea.io/gitea/modules/timeutil" | |||
| "xorm.io/builder" | |||
| ) | |||
| type CloudbrainStatus int8 | |||
| const ( | |||
| JobWaiting CloudbrainStatus = iota | |||
| JobStopped | |||
| JobSucceeded | |||
| JobFailed | |||
| ) | |||
| type Cloudbrain struct { | |||
| ID int64 `xorm:"pk autoincr"` | |||
| JobID string | |||
| // Title string `xorm:"INDEX NOT NULL"` | |||
| Status int32 `xorm:"INDEX"` | |||
| UserID int64 `xorm:"INDEX"` | |||
| RepoID int64 `xorm:"INDEX"` | |||
| CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | |||
| UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | |||
| User *User `xorm:"-"` | |||
| Repo *Repository `xorm:"-"` | |||
| } | |||
| type CloudBrainLoginResult struct { | |||
| Code string | |||
| Msg string | |||
| Payload struct { | |||
| UserID string `json:"userId"` | |||
| RealName string `json:"realName"` | |||
| Token string `json:"token"` | |||
| Admin bool `json:"admin"` | |||
| } | |||
| } | |||
| type TaskRole struct { | |||
| Name string `json:"name"` | |||
| TaskNumber int8 `json:"taskNumber"` | |||
| MinSucceededTaskCount int8 `json:"minSucceededTaskCount"` | |||
| MinFailedTaskCount int8 `json:"minFailedTaskCount"` | |||
| CPUNumber int8 `json:"cpuNumber"` | |||
| GPUNumber int8 `json:"gpuNumber"` | |||
| MemoryMB int `json:"memoryMB"` | |||
| ShmMB int `json:"shmMB"` | |||
| Command string `json:"command"` | |||
| NeedIBDevice bool `json:"needIBDevice"` | |||
| IsMainRole bool `json:"isMainRole"` | |||
| } | |||
| type CreateJobParams struct { | |||
| JobName string `json:"jobName"` | |||
| RetryCount int8 `json:"retryCount"` | |||
| GpuType string `json:"gpuType"` | |||
| Image string `json:"image"` | |||
| TaskRoles []TaskRole `json:"taskRoles"` | |||
| } | |||
| type CreateJobResult struct { | |||
| Code string | |||
| Msg string | |||
| Payload struct { | |||
| JobID string `json:"jobId"` | |||
| } | |||
| } | |||
| type GetJobResult struct { | |||
| Code string | |||
| Msg string | |||
| Payload struct { | |||
| ID string `json:"Id"` | |||
| Name string | |||
| Platform string | |||
| JobStatus struct { | |||
| Username string | |||
| State string | |||
| SubState string `json:"subState"` | |||
| ExecutionType string `json:"executionType"` | |||
| Retries int8 `json:"retries"` | |||
| CreatedTime int64 `json:"createdTime"` | |||
| CompletedTime int64 `json:"completedTime"` | |||
| AppID string `json:"appId"` | |||
| AppProgress string `json:"appProgress"` | |||
| AppTrackingURL string `json:"appTrackingUrl"` | |||
| AppLaunchedTime int64 `json:"appLaunchedTime"` | |||
| AppCompletedTime int64 `json:"appCompletedTime"` | |||
| AppExitCode int8 `json:"appExitCode"` | |||
| AppExitDiagnostics string `json:"appExitDiagnostics"` | |||
| AppExitType string `json:"appExitType"` | |||
| VirtualCluster string `json:"virtualCluster"` | |||
| } `json:"jobStatus"` | |||
| TaskRoles string `json:"taskRoles"` | |||
| Resource struct { | |||
| CPU int8 `json:"cpu"` | |||
| Memory string | |||
| GPU string `json:"nvidia.com/gpu"` | |||
| } `json:"resource"` | |||
| Config struct { | |||
| Image string | |||
| JobID string `json:"jobId"` | |||
| GpuType string `json:"gpuType"` | |||
| JobName string `json:"jobName"` | |||
| JobType string `json:"jobType"` | |||
| RetryCount int8 `json:"retryCount"` | |||
| TaskRoles []struct { | |||
| Name string `json:"name"` | |||
| ShmMB int32 `json:"shmMB"` | |||
| Command string `json:"command"` | |||
| MemoryMB int64 `json:"memoryMB"` | |||
| CPUNumber int8 `json:"cpuNumber"` | |||
| GPUNumber int8 `json:"gpuNumber"` | |||
| IsMainRole bool `json:"isMainRole"` | |||
| TaskNumber int32 `json:"taskNumber"` | |||
| NeedIBDevice bool `json:"needIBDevice"` | |||
| MinFailedTaskCount int8 `json:"minFailedTaskCount"` | |||
| MinSucceededTaskCount int8 `json:"minSucceededTaskCount"` | |||
| } `json:"taskRoles"` | |||
| } | |||
| Userinfo struct { | |||
| User string | |||
| OrgID string `json:"org_id"` | |||
| } | |||
| } | |||
| } | |||
| type CloudbrainsOptions struct { | |||
| ListOptions | |||
| RepoID int64 // include all repos if empty | |||
| UserID int64 | |||
| JobStatus CloudbrainStatus | |||
| SortType string | |||
| CloudbrainIDs []int64 | |||
| } | |||
| func Cloudbrains(opts *CloudbrainsOptions) ([]*Cloudbrain, int64, error) { | |||
| sess := x.NewSession() | |||
| defer sess.Close() | |||
| var cond = builder.NewCond() | |||
| if opts.RepoID > 0 { | |||
| cond.And( | |||
| builder.Eq{"cloudbrain.repo_id": opts.RepoID}, | |||
| ) | |||
| } | |||
| if opts.UserID > 0 { | |||
| cond.And( | |||
| builder.Eq{"cloudbrain.user_id": opts.UserID}, | |||
| ) | |||
| } | |||
| switch opts.JobStatus { | |||
| case JobWaiting: | |||
| cond.And(builder.Eq{"cloudbrain.status": int(JobWaiting)}) | |||
| case JobFailed: | |||
| cond.And(builder.Eq{"cloudbrain.status": int(JobFailed)}) | |||
| case JobStopped: | |||
| cond.And(builder.Eq{"cloudbrain.status": int(JobStopped)}) | |||
| case JobSucceeded: | |||
| cond.And(builder.Eq{"cloudbrain.status": int(JobSucceeded)}) | |||
| } | |||
| if len(opts.CloudbrainIDs) > 0 { | |||
| cond.And(builder.In("cloudbrain.id", opts.CloudbrainIDs)) | |||
| } | |||
| count, err := sess.Where(cond).Count(new(Cloudbrain)) | |||
| if err != nil { | |||
| return nil, 0, fmt.Errorf("Count: %v", err) | |||
| } | |||
| if opts.Page >= 0 && opts.PageSize > 0 { | |||
| var start int | |||
| if opts.Page == 0 { | |||
| start = 0 | |||
| } else { | |||
| start = (opts.Page - 1) * opts.PageSize | |||
| } | |||
| sess.Limit(opts.PageSize, start) | |||
| } | |||
| sess.OrderBy("cloudbrain.created_unix DESC") | |||
| cloudbrains := make([]*Cloudbrain, 0, setting.UI.IssuePagingNum) | |||
| if err := sess.Find(&cloudbrains); err != nil { | |||
| return nil, 0, fmt.Errorf("Find: %v", err) | |||
| } | |||
| sess.Close() | |||
| return cloudbrains, count, nil | |||
| } | |||
| func CreateCloudbrain(cloudbrain *Cloudbrain) (err error) { | |||
| if _, err = x.Insert(cloudbrain); err != nil { | |||
| return err | |||
| } | |||
| return nil | |||
| } | |||
| @@ -213,6 +213,7 @@ var migrations = []Migration{ | |||
| // v139 -> v140 | |||
| NewMigration("prepend refs/heads/ to issue refs", prependRefsHeadsToIssueRefs), | |||
| NewMigration("add dataset migration", addDatasetTable), | |||
| NewMigration("add cloudbrain migration", addCloudBrainTable), | |||
| } | |||
| // GetCurrentDBVersion returns the current db version | |||
| @@ -0,0 +1,26 @@ | |||
| package migrations | |||
| import ( | |||
| "fmt" | |||
| "code.gitea.io/gitea/modules/timeutil" | |||
| "xorm.io/xorm" | |||
| ) | |||
| func addCloudBrainTable(x *xorm.Engine) error { | |||
| type Cloudbrain struct { | |||
| ID int64 `xorm:"pk autoincr"` | |||
| JobId string | |||
| // Title string `xorm:"INDEX NOT NULL"` | |||
| Status int32 `xorm:"INDEX"` | |||
| UserID int64 `xorm:"INDEX"` | |||
| RepoID int64 `xorm:"INDEX"` | |||
| CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"` | |||
| UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"` | |||
| } | |||
| if err := x.Sync2(new(Cloudbrain)); err != nil { | |||
| return fmt.Errorf("Sync2: %v", err) | |||
| } | |||
| return nil | |||
| } | |||
| @@ -126,6 +126,7 @@ func init() { | |||
| new(LanguageStat), | |||
| new(EmailHash), | |||
| new(Dataset), | |||
| new(Cloudbrain), | |||
| ) | |||
| gonicNames := []string{"SSL", "UID"} | |||
| @@ -0,0 +1,16 @@ | |||
| package auth | |||
| import ( | |||
| "gitea.com/macaron/binding" | |||
| "gitea.com/macaron/macaron" | |||
| ) | |||
| // CreateDatasetForm form for dataset page | |||
| type CreateCloudBrainForm struct { | |||
| Image string `binding:"Required"` | |||
| Command string `binding:"Required"` | |||
| } | |||
| func (f *CreateCloudBrainForm) Validate(ctx *macaron.Context, errs binding.Errors) binding.Errors { | |||
| return validate(errs, ctx.Data, f, ctx.Locale) | |||
| } | |||
| @@ -0,0 +1,58 @@ | |||
| package cloudbrain | |||
| import ( | |||
| "errors" | |||
| "fmt" | |||
| "strconv" | |||
| "time" | |||
| "code.gitea.io/gitea/modules/context" | |||
| "code.gitea.io/gitea/models" | |||
| ) | |||
| func GenerateTask(ctx *context.Context, image, command string) error { | |||
| nowStr := strconv.FormatInt(time.Now().Unix(), 10) | |||
| jobName := fmt.Sprintf("%s%s", ctx.User.Name, nowStr[len(nowStr)-5:]) | |||
| jobResult, err := CreateJob(jobName, models.CreateJobParams{ | |||
| JobName: jobName, | |||
| RetryCount: 1, | |||
| GpuType: "debug", | |||
| Image: image, | |||
| TaskRoles: []models.TaskRole{ | |||
| { | |||
| Name: jobName, | |||
| TaskNumber: 1, | |||
| MinSucceededTaskCount: 1, | |||
| MinFailedTaskCount: 1, | |||
| CPUNumber: 2, | |||
| GPUNumber: 1, | |||
| MemoryMB: 16384, | |||
| ShmMB: 8192, | |||
| Command: command, | |||
| NeedIBDevice: false, | |||
| IsMainRole: false, | |||
| }, | |||
| }, | |||
| }) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| if jobResult.Code != "S000" { | |||
| return errors.New(jobResult.Msg) | |||
| } | |||
| err = models.CreateCloudbrain(&models.Cloudbrain{ | |||
| Status: int32(models.JobWaiting), | |||
| UserID: ctx.User.ID, | |||
| RepoID: ctx.Repo.Repository.ID, | |||
| JobID: jobResult.Payload.JobID, | |||
| }) | |||
| if err != nil { | |||
| return err | |||
| } | |||
| return nil | |||
| } | |||
| @@ -0,0 +1,120 @@ | |||
| package cloudbrain | |||
| import ( | |||
| "fmt" | |||
| "code.gitea.io/gitea/models" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| resty "github.com/go-resty/resty/v2" | |||
| ) | |||
| var ( | |||
| restyClient *resty.Client | |||
| HOST string | |||
| TOKEN string | |||
| ) | |||
| func getRestyClient() *resty.Client { | |||
| if restyClient == nil { | |||
| restyClient = resty.New() | |||
| } | |||
| return restyClient | |||
| } | |||
| func checkSetting() { | |||
| if len(HOST) != 0 && len(TOKEN) != 0 && restyClient != nil { | |||
| return | |||
| } | |||
| _ = loginCloudbrain() | |||
| } | |||
| func loginCloudbrain() error { | |||
| conf := setting.GetCloudbrainConfig() | |||
| username := conf.Username | |||
| password := conf.Password | |||
| HOST = conf.Host | |||
| var loginResult models.CloudBrainLoginResult | |||
| client := getRestyClient() | |||
| res, err := client.R(). | |||
| SetHeader("Content-Type", "application/json"). | |||
| SetBody(map[string]interface{}{"username": username, "password": password, "expiration": "604800"}). | |||
| SetResult(&loginResult). | |||
| Post(HOST + "/rest-server/api/v1/token") | |||
| if err != nil { | |||
| return fmt.Errorf("resty loginCloudbrain: %s", err) | |||
| } | |||
| if loginResult.Code != "S000" { | |||
| return fmt.Errorf("%s: %s", loginResult.Msg, res.String()) | |||
| } | |||
| TOKEN = loginResult.Payload.Token | |||
| return nil | |||
| } | |||
| func CreateJob(jobName string, createJobParams models.CreateJobParams) (*models.CreateJobResult, error) { | |||
| checkSetting() | |||
| client := getRestyClient() | |||
| var jobResult models.CreateJobResult | |||
| retry := 0 | |||
| sendjob: | |||
| res, err := client.R(). | |||
| SetHeader("Content-Type", "application/json"). | |||
| SetAuthToken(TOKEN). | |||
| SetBody(createJobParams). | |||
| SetResult(&jobResult). | |||
| Put(HOST + "/rest-server/api/v1/jobs/" + jobName) | |||
| if err != nil { | |||
| return nil, fmt.Errorf("resty create job: %s", err) | |||
| } | |||
| if jobResult.Code == "S401" && retry < 1 { | |||
| retry++ | |||
| _ = loginCloudbrain() | |||
| goto sendjob | |||
| } | |||
| if jobResult.Code != "S000" { | |||
| return &jobResult, fmt.Errorf("jobResult err: %s", res.String()) | |||
| } | |||
| return &jobResult, nil | |||
| } | |||
| func GetJob(jobID string) (*models.GetJobResult, error) { | |||
| checkSetting() | |||
| // http://192.168.204.24/rest-server/api/v1/jobs/90e26e500c4b3011ea0a251099a987938b96 | |||
| client := getRestyClient() | |||
| var getJobResult models.GetJobResult | |||
| retry := 0 | |||
| sendjob: | |||
| res, err := client.R(). | |||
| SetHeader("Content-Type", "application/json"). | |||
| SetAuthToken(TOKEN). | |||
| SetResult(&getJobResult). | |||
| Get(HOST + "/rest-server/api/v1/jobs/" + jobID) | |||
| if err != nil { | |||
| return nil, fmt.Errorf("resty GetJob: %s", err) | |||
| } | |||
| if getJobResult.Code == "S401" && retry < 1 { | |||
| retry++ | |||
| _ = loginCloudbrain() | |||
| goto sendjob | |||
| } | |||
| if getJobResult.Code != "S000" { | |||
| return &getJobResult, fmt.Errorf("jobResult GetJob err: %s", res.String()) | |||
| } | |||
| return &getJobResult, nil | |||
| } | |||
| @@ -0,0 +1,19 @@ | |||
| package setting | |||
| type CloudbrainLoginConfig struct { | |||
| Username string | |||
| Password string | |||
| Host string | |||
| } | |||
| var ( | |||
| Cloudbrain = CloudbrainLoginConfig{} | |||
| ) | |||
| func GetCloudbrainConfig() CloudbrainLoginConfig { | |||
| cloudbrainSec := Cfg.Section("cloudbrain") | |||
| Cloudbrain.Username = cloudbrainSec.Key("USERNAME").MustString("") | |||
| Cloudbrain.Password = cloudbrainSec.Key("PASSWORD").MustString("") | |||
| Cloudbrain.Host = cloudbrainSec.Key("HOST").MustString("") | |||
| return Cloudbrain | |||
| } | |||
| @@ -1,8 +1,12 @@ | |||
| package repo | |||
| import ( | |||
| "code.gitea.io/gitea/models" | |||
| "code.gitea.io/gitea/modules/auth" | |||
| "code.gitea.io/gitea/modules/base" | |||
| "code.gitea.io/gitea/modules/cloudbrain" | |||
| "code.gitea.io/gitea/modules/context" | |||
| "code.gitea.io/gitea/modules/setting" | |||
| ) | |||
| const ( | |||
| @@ -10,14 +14,59 @@ const ( | |||
| tplCloudBrainNew base.TplName = "repo/cloudbrain/new" | |||
| ) | |||
| // MustEnableDataset check if repository enable internal cb | |||
| func MustEnableCloudbrain(ctx *context.Context) { | |||
| if !ctx.Repo.CanRead(models.UnitTypeCloudBrain) { | |||
| ctx.NotFound("MustEnableCloudbrain", nil) | |||
| return | |||
| } | |||
| } | |||
| func CloudBrainIndex(ctx *context.Context) { | |||
| ctx.Data["PageIsViewCloudBrain"] = true | |||
| MustEnableCloudbrain(ctx) | |||
| repo := ctx.Repo.Repository | |||
| page := ctx.QueryInt("page") | |||
| if page <= 0 { | |||
| page = 1 | |||
| } | |||
| ciTasks, count, err := models.Cloudbrains(&models.CloudbrainsOptions{ | |||
| ListOptions: models.ListOptions{ | |||
| Page: page, | |||
| PageSize: setting.UI.IssuePagingNum, | |||
| }, | |||
| RepoID: repo.ID, | |||
| // SortType: sortType, | |||
| }) | |||
| if err != nil { | |||
| ctx.ServerError("Cloudbrain", err) | |||
| return | |||
| } | |||
| pager := context.NewPagination(int(count), setting.UI.IssuePagingNum, page, 5) | |||
| pager.SetDefaultParams(ctx) | |||
| ctx.Data["Page"] = pager | |||
| ctx.Data["PageIsCloudBrain"] = true | |||
| ctx.Data["Tasks"] = ciTasks | |||
| ctx.HTML(200, tplCloudBrainIndex) | |||
| } | |||
| func CloudBrainNew(ctx *context.Context) { | |||
| ctx.Data["PageIsViewCloudBrain"] = true | |||
| ctx.Data["PageIsCloudBrain"] = true | |||
| ctx.Data["image"] = "192.168.202.74:5000/user-images/deepo:v2.0" | |||
| ctx.Data["command"] = `pip3 install jupyterlab==1.1.4;service ssh stop;jupyter lab --no-browser --ip=0.0.0.0 --allow-root --notebook-dir=\"/userhome\" --port=80 --NotebookApp.token=\"\" --LabApp.allow_origin=\"self https://cloudbrain.pcl.ac.cn\"` | |||
| ctx.HTML(200, tplCloudBrainNew) | |||
| } | |||
| func CloudBrainCreate(ctx *context.Context, form auth.CreateCloudBrainForm) { | |||
| ctx.Data["PageIsCloudBrain"] = true | |||
| image := form.Image | |||
| command := form.Command | |||
| err := cloudbrain.GenerateTask(ctx, image, command) | |||
| if err != nil { | |||
| ctx.RenderWithErr(err.Error(), tplCloudBrainNew, &form) | |||
| return | |||
| } | |||
| ctx.Redirect(setting.AppSubURL + ctx.Repo.RepoLink + "/cloudbrain") | |||
| } | |||
| @@ -873,8 +873,9 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
| m.Group("/cloudbrain", func() { | |||
| m.Get("", reqRepoCloudBrainReader, repo.CloudBrainIndex) | |||
| m.Get("/new", reqRepoCloudBrainWriter, repo.CloudBrainNew) | |||
| }) | |||
| m.Get("/create", reqRepoCloudBrainWriter, repo.CloudBrainNew) | |||
| m.Post("/create", reqRepoCloudBrainWriter, bindIgnErr(auth.CreateCloudBrainForm{}), repo.CloudBrainCreate) | |||
| }, context.RepoRef()) | |||
| m.Group("/wiki", func() { | |||
| m.Get("/?:page", repo.Wiki) | |||
| @@ -1,26 +0,0 @@ | |||
| <div class="ui grid item"> | |||
| <div class="row"> | |||
| <div class="five wide column"> | |||
| <a class="title" href="#"> | |||
| <span class="fitted">{{svg "octicon-tasklist" 16}}</span> | |||
| <span class="fitted">任务名</span> | |||
| </a> | |||
| </div> | |||
| <div class="two wide column"> | |||
| waiting | |||
| </div> | |||
| <div class="three wide column"> | |||
| <span class="ui text center">{{svg "octicon-flame" 16}} 2020-07-13 16:06:08</span> | |||
| </div> | |||
| <div class="two wide column"> | |||
| <span class="ui text center clipboard">18h 0m 8s</span> | |||
| </div> | |||
| <div class="two wide column"> | |||
| <span class="ui text center clipboard">Stop</span> | |||
| </div> | |||
| <div class="two wide column"> | |||
| <span class="ui text center clipboard">再次提交</span> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -9,7 +9,7 @@ | |||
| <div class="column"> | |||
| </div> | |||
| <div class="column right aligned"> | |||
| <a class="ui green button" href="{{.RepoLink}}/cloudbrain/new">{{.i18n.Tr "repo.cloudbrain.new"}}</a> | |||
| <a class="ui green button" href="{{.RepoLink}}/cloudbrain/create">{{.i18n.Tr "repo.cloudbrain.new"}}</a> | |||
| </div> | |||
| </div> | |||
| <div class="ui divider"></div> | |||
| @@ -30,7 +30,35 @@ | |||
| </div> | |||
| </div> | |||
| <div class="dataset list"> | |||
| {{template "repo/cloudbrain/cloudbrain_list" .}} | |||
| {{range .Tasks}} | |||
| <div class="ui grid item"> | |||
| <div class="row"> | |||
| <div class="five wide column"> | |||
| <a class="title" href="{{$.Link}}/{{.JobID}}"> | |||
| <span class="fitted">{{svg "octicon-tasklist" 16}}</span> | |||
| <span class="fitted">{{.JobID}}</span> | |||
| </a> | |||
| </div> | |||
| <div class="two wide column"> | |||
| waiting | |||
| </div> | |||
| <div class="three wide column"> | |||
| <span class="ui text center">{{svg "octicon-flame" 16}} {{TimeSinceUnix .CreatedUnix $.Lang}}</span> | |||
| </div> | |||
| <div class="two wide column"> | |||
| <span class="ui text center clipboard">18h 0m 8s</span> | |||
| </div> | |||
| <div class="two wide column"> | |||
| <span class="ui text center clipboard">{{.Status}}</span> | |||
| </div> | |||
| <div class="two wide column"> | |||
| <span class="ui text center clipboard">再次提交</span> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| {{end}} | |||
| {{template "base/paginate" .}} | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -3,7 +3,8 @@ | |||
| {{template "repo/header" .}} | |||
| <div class="ui middle very relaxed page grid"> | |||
| <div class="column"> | |||
| <form class="ui form" action="/repo/create" method="post"> | |||
| {{template "base/alert" .}} | |||
| <form class="ui form" action="{{.Link}}" method="post"> | |||
| {{.CsrfTokenHtml}} | |||
| <h3 class="ui top attached header"> | |||
| New Cloudbrain task | |||
| @@ -12,7 +13,7 @@ | |||
| <br> | |||
| <div class="inline required field"> | |||
| <label>镜像</label> | |||
| <input name="title" id="issue_title" placeholder="输入镜像" value="{{.image}}" tabindex="3" autofocus required maxlength="255"> | |||
| <input name="image" id="cloudbrain_image" placeholder="输入镜像" value="{{.image}}" tabindex="3" autofocus required maxlength="255"> | |||
| </div> | |||
| <div class="inline required field"> | |||
| <label>启动命令</label> | |||
| @@ -314,6 +314,9 @@ github.com/go-redis/redis/internal/hashtag | |||
| github.com/go-redis/redis/internal/pool | |||
| github.com/go-redis/redis/internal/proto | |||
| github.com/go-redis/redis/internal/util | |||
| # github.com/go-resty/resty/v2 v2.3.0 | |||
| ## explicit | |||
| github.com/go-resty/resty/v2 | |||
| # github.com/go-sql-driver/mysql v1.4.1 | |||
| ## explicit | |||
| github.com/go-sql-driver/mysql | |||
| @@ -719,7 +722,7 @@ golang.org/x/crypto/ssh/knownhosts | |||
| ## explicit | |||
| golang.org/x/mod/module | |||
| golang.org/x/mod/semver | |||
| # golang.org/x/net v0.0.0-20200506145744-7e3656a0809f | |||
| # golang.org/x/net v0.0.0-20200513185701-a91f0712d120 | |||
| ## explicit | |||
| golang.org/x/net/context | |||
| golang.org/x/net/context/ctxhttp | |||