diff --git a/backend/app/api/v1/compose_template.go b/backend/app/api/v1/compose_template.go index 5c9e03b9d..03be6b7b1 100644 --- a/backend/app/api/v1/compose_template.go +++ b/backend/app/api/v1/compose_template.go @@ -90,6 +90,7 @@ func (b *BaseApi) UpdateComposeTemplate(c *gin.Context) { upMap := make(map[string]interface{}) upMap["from"] = req.From + upMap["path"] = req.Path upMap["content"] = req.Content upMap["description"] = req.Description if err := composeTemplateService.Update(id, upMap); err != nil { diff --git a/backend/app/api/v1/container.go b/backend/app/api/v1/container.go index d2bea277e..445f75d75 100644 --- a/backend/app/api/v1/container.go +++ b/backend/app/api/v1/container.go @@ -32,6 +32,64 @@ func (b *BaseApi) SearchContainer(c *gin.Context) { }) } +func (b *BaseApi) SearchCompose(c *gin.Context) { + var req dto.PageInfo + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if err := global.VALID.Struct(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + + total, list, err := containerService.PageCompose(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, dto.PageResult{ + Items: list, + Total: total, + }) +} + +func (b *BaseApi) CreateCompose(c *gin.Context) { + var req dto.ComposeCreate + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if err := global.VALID.Struct(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + + if err := containerService.CreateCompose(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +func (b *BaseApi) OperatorCompose(c *gin.Context) { + var req dto.ComposeOperation + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if err := global.VALID.Struct(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + + if err := containerService.ComposeOperation(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + func (b *BaseApi) ContainerCreate(c *gin.Context) { var req dto.ContainerCreate if err := c.ShouldBindJSON(&req); err != nil { diff --git a/backend/app/dto/compose_template.go b/backend/app/dto/compose_template.go index 5a17fbbdd..140ae6143 100644 --- a/backend/app/dto/compose_template.go +++ b/backend/app/dto/compose_template.go @@ -6,12 +6,14 @@ type ComposeTemplateCreate struct { Name string `json:"name" validate:"required"` From string `json:"from" validate:"required,oneof=edit path"` Description string `json:"description"` + Path string `json:"path"` Content string `json:"content"` } type ComposeTemplateUpdate struct { From string `json:"from" validate:"required,oneof=edit path"` Description string `json:"description"` + Path string `json:"path"` Content string `json:"content"` } @@ -21,5 +23,6 @@ type ComposeTemplateInfo struct { Name string `json:"name"` From string `json:"from"` Description string `json:"description"` + Path string `json:"path"` Content string `json:"content"` } diff --git a/backend/app/dto/container.go b/backend/app/dto/container.go index 0ff054e64..9d49fdac7 100644 --- a/backend/app/dto/container.go +++ b/backend/app/dto/container.go @@ -4,7 +4,7 @@ import "time" type PageContainer struct { PageInfo - Status string `json:"status" validate:"required,oneof=all running"` + Filters string `json:"filters"` } type InspectReq struct { @@ -66,7 +66,7 @@ type ContainerLog struct { type ContainerOperation struct { ContainerID string `json:"containerID" validate:"required"` - Operation string `json:"operation" validate:"required,oneof=start stop reStart kill pause unPause reName remove"` + Operation string `json:"operation" validate:"required,oneof=start stop restart kill pause unpause rename remove"` NewName string `json:"newName"` } @@ -108,3 +108,30 @@ type VolumeCreat struct { type BatchDelete struct { Ids []string `json:"ids" validate:"required"` } + +type ComposeInfo struct { + Name string `json:"name"` + CreatedAt string `json:"createdAt"` + ContainerNumber int `json:"containerNumber"` + ConfigFile string `json:"configFile"` + Workdir string `json:"workdir"` + Path string `json:"path"` + Containers []ComposeContainer `json:"containers"` +} +type ComposeContainer struct { + ContainerID string `json:"containerID"` + Name string `json:"name"` + CreateTime string `json:"createTime"` + State string `json:"state"` +} +type ComposeCreate struct { + Name string `json:"name" validate:"required"` + From string `json:"from" validate:"required,oneof=edit path template"` + File string `json:"file"` + Path string `json:"path"` + Template uint `json:"template"` +} +type ComposeOperation struct { + Path string `json:"path" validate:"required"` + Operation string `json:"operation" validate:"required,oneof=up stop pause unpause restart down"` +} diff --git a/backend/app/model/compose_template.go b/backend/app/model/compose_template.go index 948b9b435..c7f9824ea 100644 --- a/backend/app/model/compose_template.go +++ b/backend/app/model/compose_template.go @@ -5,6 +5,7 @@ type ComposeTemplate struct { Name string `gorm:"type:varchar(64);not null;unique" json:"name"` From string `gorm:"type:varchar(64);not null" json:"from"` - Description string `gorm:"type:varchar(256);" json:"description"` + Description string `gorm:"type:varchar(256)" json:"description"` + Path string `gorm:"type:varchar(64)" json:"path"` Content string `gorm:"type:longtext" json:"content"` } diff --git a/backend/app/service/container.go b/backend/app/service/container.go index 4ede2c37d..4df1419cd 100644 --- a/backend/app/service/container.go +++ b/backend/app/service/container.go @@ -1,18 +1,22 @@ package service import ( + "bufio" "bytes" "context" "encoding/json" "fmt" "io" "io/ioutil" + "os" + "os/exec" "strconv" "strings" "time" "github.com/1Panel-dev/1Panel/app/dto" "github.com/1Panel-dev/1Panel/constant" + "github.com/1Panel-dev/1Panel/global" "github.com/1Panel-dev/1Panel/utils/docker" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" @@ -26,11 +30,18 @@ import ( type ContainerService struct{} +const composeProjectLabel = "com.docker.compose.project" +const composeConfigLabel = "com.docker.compose.project.config_files" +const composeWorkdirLabel = "com.docker.compose.project.working_dir" + type IContainerService interface { Page(req dto.PageContainer) (int64, interface{}, error) PageNetwork(req dto.PageInfo) (int64, interface{}, error) PageVolume(req dto.PageInfo) (int64, interface{}, error) ListVolume() ([]dto.Options, error) + PageCompose(req dto.PageInfo) (int64, interface{}, error) + CreateCompose(req dto.ComposeCreate) error + ComposeOperation(req dto.ComposeOperation) error ContainerCreate(req dto.ContainerCreate) error ContainerOperation(req dto.ContainerOperation) error ContainerLogs(param dto.ContainerLog) (string, error) @@ -56,7 +67,12 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro if err != nil { return 0, nil, err } - list, err = client.ContainerList(context.Background(), types.ContainerListOptions{All: req.Status == "all"}) + options := types.ContainerListOptions{All: true} + if len(req.Filters) != 0 { + options.Filters = filters.NewArgs() + options.Filters.Add("label", req.Filters) + } + list, err = client.ContainerList(context.Background(), options) if err != nil { return 0, nil, err } @@ -85,6 +101,128 @@ func (u *ContainerService) Page(req dto.PageContainer) (int64, interface{}, erro return int64(total), backDatas, nil } +func (u *ContainerService) PageCompose(req dto.PageInfo) (int64, interface{}, error) { + var ( + records []dto.ComposeInfo + BackDatas []dto.ComposeInfo + ) + client, err := docker.NewDockerClient() + if err != nil { + return 0, nil, err + } + + options := types.ContainerListOptions{All: true} + options.Filters = filters.NewArgs() + options.Filters.Add("label", composeProjectLabel) + + list, err := client.ContainerList(context.Background(), options) + if err != nil { + return 0, nil, err + } + composeMap := make(map[string]dto.ComposeInfo) + for _, container := range list { + if name, ok := container.Labels[composeProjectLabel]; ok { + containerItem := dto.ComposeContainer{ + ContainerID: container.ID, + Name: container.Names[0][1:], + State: container.State, + CreateTime: time.Unix(container.Created, 0).Format("2006-01-02 15:04:05"), + } + if compose, has := composeMap[name]; has { + compose.ContainerNumber++ + compose.Containers = append(compose.Containers, containerItem) + composeMap[name] = compose + } else { + config := container.Labels[composeConfigLabel] + workdir := container.Labels[composeWorkdirLabel] + composeItem := dto.ComposeInfo{ + ContainerNumber: 1, + CreatedAt: time.Unix(container.Created, 0).Format("2006-01-02 15:04:05"), + ConfigFile: config, + Workdir: workdir, + Containers: []dto.ComposeContainer{containerItem}, + } + if len(config) != 0 && len(workdir) != 0 && strings.Contains(config, workdir) { + composeItem.Path = config + } else { + composeItem.Path = workdir + } + composeMap[name] = composeItem + } + } + } + for key, value := range composeMap { + value.Name = key + records = append(records, value) + } + total, start, end := len(records), (req.Page-1)*req.PageSize, req.Page*req.PageSize + if start > total { + BackDatas = make([]dto.ComposeInfo, 0) + } else { + if end >= total { + end = total + } + BackDatas = records[start:end] + } + return int64(total), BackDatas, nil +} + +func (u *ContainerService) CreateCompose(req dto.ComposeCreate) error { + if req.From == "template" { + template, err := composeRepo.Get(commonRepo.WithByID(req.Template)) + if err != nil { + return err + } + req.From = template.From + if req.From == "edit" { + req.File = template.Content + } else { + req.Path = template.Path + } + } + if req.From == "edit" { + dir := fmt.Sprintf("%s/%s", constant.TmpComposeBuildDir, req.Name) + if _, err := os.Stat(dir); err != nil && os.IsNotExist(err) { + if err = os.MkdirAll(dir, os.ModePerm); err != nil { + return err + } + } + + path := fmt.Sprintf("%s/docker-compose.yml", dir) + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666) + if err != nil { + return err + } + defer file.Close() + write := bufio.NewWriter(file) + _, _ = write.WriteString(string(req.File)) + write.Flush() + req.Path = path + } + go func() { + cmd := exec.Command("docker-compose", "-f", req.Path, "up", "-d") + stdout, err := cmd.CombinedOutput() + if err != nil { + global.LOG.Debugf("docker-compose up %s failed, err: %v", req.Name, err) + return + } + global.LOG.Debugf("docker-compose up %s successful, logs: %v", req.Name, string(stdout)) + }() + + return nil +} + +func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error { + cmd := exec.Command("docker-compose", "-f", req.Path, req.Operation) + stdout, err := cmd.CombinedOutput() + if err != nil { + return err + } + global.LOG.Debugf("docker-compose %s %s successful: logs: %v", req.Operation, req.Path, string(stdout)) + + return err +} + func (u *ContainerService) Inspect(req dto.InspectReq) (string, error) { client, err := docker.NewDockerClient() if err != nil { diff --git a/backend/app/service/image_test.go b/backend/app/service/image_test.go index adecf7c76..9917465d7 100644 --- a/backend/app/service/image_test.go +++ b/backend/app/service/image_test.go @@ -9,11 +9,11 @@ import ( "os" "testing" - "github.com/1Panel-dev/1Panel/app/dto" "github.com/1Panel-dev/1Panel/constant" "github.com/1Panel-dev/1Panel/utils/docker" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/network" "github.com/docker/docker/pkg/archive" v1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -96,31 +96,11 @@ func TestNetwork(t *testing.T) { if err != nil { fmt.Println(err) } - res, err := client.ContainerStatsOneShot(context.TODO(), "30e4d3395b87") - if err != nil { - fmt.Println(err) - } - defer res.Body.Close() - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - fmt.Println(err) - } - var state *types.StatsJSON - if err := json.Unmarshal(body, &state); err != nil { - fmt.Println(err) - } - - fmt.Println(string(body)) - - var data dto.ContainterStats - previousCPU := state.PreCPUStats.CPUUsage.TotalUsage - previousSystem := state.PreCPUStats.SystemUsage - data.CPUPercent = calculateCPUPercentUnix(previousCPU, previousSystem, state) - data.IORead, data.IOWrite = calculateBlockIO(state.BlkioStats) - data.Memory = float64(state.MemoryStats.Usage) - data.NetworkRX, data.NetworkTX = calculateNetwork(state.Networks) - fmt.Println(data) + options := types.ContainerListOptions{All: true} + options.Filters = filters.NewArgs() + options.Filters.Add("label", "maintainer") + ss, _ := client.ContainerList(context.TODO(), options) + fmt.Println(ss) } func TestContainer(t *testing.T) { diff --git a/backend/constant/container.go b/backend/constant/container.go index bd80ea499..353179268 100644 --- a/backend/constant/container.go +++ b/backend/constant/container.go @@ -3,13 +3,18 @@ package constant const ( ContainerOpStart = "start" ContainerOpStop = "stop" - ContainerOpRestart = "reStart" + ContainerOpRestart = "restart" ContainerOpKill = "kill" ContainerOpPause = "pause" - ContainerOpUnpause = "unPause" - ContainerOpRename = "reName" + ContainerOpUnpause = "unpause" + ContainerOpRename = "rename" ContainerOpRemove = "remove" - DaemonJsonDir = "/System/Volumes/Data/Users/slooop/.docker/daemon.json" - TmpDockerBuildDir = "/opt/1Panel/build" + ComposeOpStop = "stop" + ComposeOpRestart = "restart" + ComposeOpRemove = "remove" + + DaemonJsonDir = "/System/Volumes/Data/Users/slooop/.docker/daemon.json" + TmpDockerBuildDir = "/opt/1Panel/data/docker/build" + TmpComposeBuildDir = "/opt/1Panel/data/docker/compose" ) diff --git a/backend/router/ro_container.go b/backend/router/ro_container.go index b73ba520c..bc544da90 100644 --- a/backend/router/ro_container.go +++ b/backend/router/ro_container.go @@ -33,11 +33,15 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) { withRecordRouter.POST("/repo", baseApi.CreateRepo) withRecordRouter.POST("/repo/del", baseApi.DeleteRepo) - baRouter.POST("/compose/search", baseApi.SearchComposeTemplate) - baRouter.PUT("/compose/:id", baseApi.UpdateComposeTemplate) - baRouter.GET("/compose", baseApi.ListComposeTemplate) - withRecordRouter.POST("/compose", baseApi.CreateComposeTemplate) - withRecordRouter.POST("/compose/del", baseApi.DeleteComposeTemplate) + baRouter.POST("/compose/search", baseApi.SearchCompose) + baRouter.POST("/compose/up", baseApi.CreateCompose) + baRouter.POST("/compose/operate", baseApi.OperatorCompose) + + baRouter.POST("/template/search", baseApi.SearchComposeTemplate) + baRouter.PUT("/template/:id", baseApi.UpdateComposeTemplate) + baRouter.GET("/template", baseApi.ListComposeTemplate) + withRecordRouter.POST("/template", baseApi.CreateComposeTemplate) + withRecordRouter.POST("/template/del", baseApi.DeleteComposeTemplate) baRouter.POST("/image/search", baseApi.SearchImage) baRouter.GET("/image", baseApi.ListImage) diff --git a/frontend/src/api/interface/container.ts b/frontend/src/api/interface/container.ts index ab4c242d7..a5df8c930 100644 --- a/frontend/src/api/interface/container.ts +++ b/frontend/src/api/interface/container.ts @@ -1,9 +1,14 @@ +import { ReqPage } from '.'; + export namespace Container { export interface ContainerOperate { containerID: string; operation: string; newName: string; } + export interface ContainerSearch extends ReqPage { + filters: string; + } export interface ContainerCreate { name: string; image: string; @@ -161,16 +166,46 @@ export namespace Container { downloadUrl: string; } + export interface ComposeInfo { + name: string; + createdAt: string; + containerNumber: number; + configFile: string; + workdir: string; + path: string; + containers: Array; + expand: boolean; + } + export interface ComposeContainer { + name: string; + createTime: string; + containerID: string; + state: string; + } + export interface ComposeCreate { + name: string; + from: string; + file: string; + path: string; + template: number; + } + export interface ComposeOpration { + operation: string; + path: string; + } + export interface TemplateCreate { name: string; from: string; description: string; + path: string; content: string; } export interface TemplateUpdate { id: number; from: string; description: string; + path: string; content: string; } export interface TemplateInfo { @@ -179,6 +214,7 @@ export namespace Container { name: string; from: string; description: string; + path: string; content: string; } diff --git a/frontend/src/api/modules/container.ts b/frontend/src/api/modules/container.ts index 27afcbcb4..1c19ac278 100644 --- a/frontend/src/api/modules/container.ts +++ b/frontend/src/api/modules/container.ts @@ -2,7 +2,7 @@ import http from '@/api'; import { ResPage, ReqPage } from '../interface'; import { Container } from '../interface/container'; -export const searchContainer = (params: ReqPage) => { +export const searchContainer = (params: Container.ContainerSearch) => { return http.post>(`/containers/search`, params); }; export const createContainer = (params: Container.ContainerCreate) => { @@ -94,17 +94,28 @@ export const deleteImageRepo = (params: { ids: number[] }) => { // composeTemplate export const searchComposeTemplate = (params: ReqPage) => { - return http.post>(`/containers/compose/search`, params); + return http.post>(`/containers/template/search`, params); }; -export const listComposeTemplate = (params: ReqPage) => { - return http.post>(`/containers/compose/search`, params); +export const listComposeTemplate = () => { + return http.get(`/containers/template`); }; export const deleteComposeTemplate = (params: { ids: number[] }) => { - return http.post(`/containers/compose/del`, params); + return http.post(`/containers/template/del`, params); }; export const createComposeTemplate = (params: Container.TemplateCreate) => { - return http.post(`/containers/compose`, params); + return http.post(`/containers/template`, params); }; export const updateComposeTemplate = (params: Container.TemplateUpdate) => { - return http.put(`/containers/compose/${params.id}`, params); + return http.put(`/containers/template/${params.id}`, params); +}; + +// compose +export const searchCompose = (params: ReqPage) => { + return http.post>(`/containers/compose/search`, params); +}; +export const upCompose = (params: Container.ComposeCreate) => { + return http.post(`/containers/compose/up`, params); +}; +export const ComposeOperator = (params: Container.ComposeOpration) => { + return http.post(`/containers/compose/operate`, params); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 99131c842..fc288d354 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -155,11 +155,11 @@ export default { operatorHelper: '{0} will be performed on the selected container. Do you want to continue?', start: 'Start', stop: 'Stop', - reStart: 'ReStart', + reStart: 'Restart', kill: 'Kill', pause: 'Pause', - unPause: 'UnPause', - reName: 'ReName', + unpause: 'Unpause', + rename: 'Rename', remove: 'Remove', container: 'Container', upTime: 'UpTime', @@ -247,6 +247,10 @@ export default { composeTemplate: 'Compose template', description: 'Description', content: 'Content', + containerNumber: 'Container number', + down: 'Down', + up: 'Up', + operatorComposeHelper: '{0} will be performed on the selected compose. Do you want to continue?', }, cronjob: { cronTask: 'Task', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 4966ad20d..f1e7d7793 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -153,11 +153,11 @@ export default { operatorHelper: '将对选中容器进行 {0} 操作,是否继续?', start: '启动', stop: '停止', - reStart: '重启', + restart: '重启', kill: '强制停止', pause: '暂停', - unPause: '恢复', - reName: '重命名', + unpause: '恢复', + rename: '重命名', remove: '移除', container: '容器', upTime: '运行时长', @@ -248,6 +248,10 @@ export default { composeTemplate: '编排模版', description: '描述', content: '内容', + containerNumber: '容器数量', + down: '删除', + up: '启动', + operatorComposeHelper: '将对选中 Compose 进行 {0} 操作,是否继续?', }, cronjob: { cronTask: '计划任务', diff --git a/frontend/src/routers/modules/container.ts b/frontend/src/routers/modules/container.ts index 1d0bb4236..01d7108cd 100644 --- a/frontend/src/routers/modules/container.ts +++ b/frontend/src/routers/modules/container.ts @@ -11,10 +11,74 @@ const containerRouter = { }, children: [ { - path: '/containers', + path: ':filters?', name: 'Container', - component: () => import('@/views/container/index.vue'), - meta: {}, + component: () => import('@/views/container/container/index.vue'), + props: true, + hidden: true, + meta: { + activeMenu: '/containers', + }, + }, + { + path: 'image', + name: 'Image', + component: () => import('@/views/container/image/index.vue'), + props: true, + hidden: true, + meta: { + activeMenu: '/containers', + }, + }, + { + path: 'network', + name: 'Network', + component: () => import('@/views/container/network/index.vue'), + props: true, + hidden: true, + meta: { + activeMenu: '/containers', + }, + }, + { + path: 'volume', + name: 'Volume', + component: () => import('@/views/container/volume/index.vue'), + props: true, + hidden: true, + meta: { + activeMenu: '/containers', + }, + }, + { + path: 'repo', + name: 'Repo', + component: () => import('@/views/container/repo/index.vue'), + props: true, + hidden: true, + meta: { + activeMenu: '/containers', + }, + }, + { + path: 'compose', + name: 'Compose', + component: () => import('@/views/container/compose/index.vue'), + props: true, + hidden: true, + meta: { + activeMenu: '/containers', + }, + }, + { + path: 'template', + name: 'composeTemplate', + component: () => import('@/views/container/template/index.vue'), + props: true, + hidden: true, + meta: { + activeMenu: '/containers', + }, }, ], }; diff --git a/frontend/src/views/container/compose/index.vue b/frontend/src/views/container/compose/index.vue new file mode 100644 index 000000000..7b189c432 --- /dev/null +++ b/frontend/src/views/container/compose/index.vue @@ -0,0 +1,140 @@ + + + diff --git a/frontend/src/views/container/compose/operator/index.vue b/frontend/src/views/container/compose/operator/index.vue new file mode 100644 index 000000000..abbfeda56 --- /dev/null +++ b/frontend/src/views/container/compose/operator/index.vue @@ -0,0 +1,133 @@ + + + diff --git a/frontend/src/views/container/container/index.vue b/frontend/src/views/container/container/index.vue index 9b912c0bc..3940c8915 100644 --- a/frontend/src/views/container/container/index.vue +++ b/frontend/src/views/container/container/index.vue @@ -1,5 +1,6 @@