Browse Source

add cloudbrain api support

tags/v1.21.12.1
palytoxin 5 years ago
parent
commit
fd8f425548
16 changed files with 549 additions and 36 deletions
  1. +2
    -1
      go.mod
  2. +5
    -0
      go.sum
  3. +210
    -0
      models/cloudbrain.go
  4. +1
    -0
      models/migrations/migrations.go
  5. +26
    -0
      models/migrations/v141.go
  6. +1
    -0
      models/models.go
  7. +16
    -0
      modules/auth/cloudbrain.go
  8. +58
    -0
      modules/cloudbrain/cloudbrain.go
  9. +120
    -0
      modules/cloudbrain/resty.go
  10. +19
    -0
      modules/setting/cloudbrain.go
  11. +51
    -2
      routers/repo/cloudbrain.go
  12. +3
    -2
      routers/routes/routes.go
  13. +0
    -26
      templates/repo/cloudbrain/cloudbrain_list.tmpl
  14. +30
    -2
      templates/repo/cloudbrain/index.tmpl
  15. +3
    -2
      templates/repo/cloudbrain/new.tmpl
  16. +4
    -1
      vendor/modules.txt

+ 2
- 1
go.mod View File

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


+ 5
- 0
go.sum View File

@@ -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=


+ 210
- 0
models/cloudbrain.go View File

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

+ 1
- 0
models/migrations/migrations.go View File

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


+ 26
- 0
models/migrations/v141.go View File

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

+ 1
- 0
models/models.go View File

@@ -126,6 +126,7 @@ func init() {
new(LanguageStat),
new(EmailHash),
new(Dataset),
new(Cloudbrain),
)

gonicNames := []string{"SSL", "UID"}


+ 16
- 0
modules/auth/cloudbrain.go View File

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

+ 58
- 0
modules/cloudbrain/cloudbrain.go View File

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

+ 120
- 0
modules/cloudbrain/resty.go View File

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

+ 19
- 0
modules/setting/cloudbrain.go View File

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

+ 51
- 2
routers/repo/cloudbrain.go View File

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

+ 3
- 2
routers/routes/routes.go View File

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


+ 0
- 26
templates/repo/cloudbrain/cloudbrain_list.tmpl View File

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

+ 30
- 2
templates/repo/cloudbrain/index.tmpl View File

@@ -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
- 2
templates/repo/cloudbrain/new.tmpl View File

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


+ 4
- 1
vendor/modules.txt View File

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


Loading…
Cancel
Save