diff --git a/backend/app/api/v1/container.go b/backend/app/api/v1/container.go index 5e6be161f..90773bef1 100644 --- a/backend/app/api/v1/container.go +++ b/backend/app/api/v1/container.go @@ -153,17 +153,69 @@ func (b *BaseApi) OperatorCompose(c *gin.Context) { helper.SuccessWithData(c, nil) } +// @Tags Container +// @Summary Update container +// @Description 更新容器 +// @Accept json +// @Param request body dto.ContainerOperate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /containers/update [post] +// @x-panel-log {"bodyKeys":["name","image"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"更新容器 [name][image]","formatEN":"update container [name][image]"} +func (b *BaseApi) ContainerUpdate(c *gin.Context) { + var req dto.ContainerOperate + 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.ContainerUpdate(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, nil) +} + +// @Tags Container +// @Summary Load container info +// @Description 获取容器表单信息 +// @Accept json +// @Param request body dto.OperationWithName true "request" +// @Success 200 {object} dto.ContainerOperate +// @Security ApiKeyAuth +// @Router /containers/info [post] +func (b *BaseApi) ContainerInfo(c *gin.Context) { + var req dto.OperationWithName + 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 + } + data, err := containerService.ContainerInfo(req) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, data) +} + // @Tags Container // @Summary Create container // @Description 创建容器 // @Accept json -// @Param request body dto.ContainerCreate true "request" +// @Param request body dto.ContainerOperate true "request" // @Success 200 // @Security ApiKeyAuth // @Router /containers [post] // @x-panel-log {"bodyKeys":["name","image"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"创建容器 [name][image]","formatEN":"create container [name][image]"} func (b *BaseApi) ContainerCreate(c *gin.Context) { - var req dto.ContainerCreate + var req dto.ContainerOperate if err := c.ShouldBindJSON(&req); err != nil { helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) return diff --git a/backend/app/dto/container.go b/backend/app/dto/container.go index c37fb8275..caee347d2 100644 --- a/backend/app/dto/container.go +++ b/backend/app/dto/container.go @@ -30,7 +30,7 @@ type ContainerInfo struct { IsFromCompose bool `json:"isFromCompose"` } -type ContainerCreate struct { +type ContainerOperate struct { Name string `json:"name"` Image string `json:"image"` PublishAllPorts bool `json:"publishAllPorts"` diff --git a/backend/app/service/container.go b/backend/app/service/container.go index 2f01d2b1e..51b0e1d51 100644 --- a/backend/app/service/container.go +++ b/backend/app/service/container.go @@ -39,7 +39,9 @@ type IContainerService interface { PageCompose(req dto.SearchWithPage) (int64, interface{}, error) CreateCompose(req dto.ComposeCreate) (string, error) ComposeOperation(req dto.ComposeOperation) error - ContainerCreate(req dto.ContainerCreate) error + ContainerCreate(req dto.ContainerOperate) error + ContainerUpdate(req dto.ContainerOperate) error + ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error) ContainerLogClean(req dto.OperationWithName) error ContainerOperation(req dto.ContainerOperation) error ContainerLogs(wsConn *websocket.Conn, container, since, tail string, follow bool) error @@ -213,75 +215,125 @@ func (u *ContainerService) Prune(req dto.ContainerPrune) (dto.ContainerPruneRepo return report, nil } -func (u *ContainerService) ContainerCreate(req dto.ContainerCreate) error { - portMap, err := checkPortStats(req.ExposedPorts) +func (u *ContainerService) ContainerCreate(req dto.ContainerOperate) error { + client, err := docker.NewDockerClient() if err != nil { return err } - client, err := docker.NewDockerClient() - if err != nil { + + var config *container.Config + var hostConf *container.HostConfig + if err := loadConfigInfo(req, config, hostConf); err != nil { return err } - exposeds := make(nat.PortSet) - for port := range portMap { - exposeds[port] = struct{}{} + global.LOG.Infof("new container info %s has been made, now start to create", req.Name) + + ctx := context.Background() + if !checkImageExist(client, req.Image) { + if err := pullImages(ctx, client, req.Image); err != nil { + return err + } } - config := &container.Config{ - Image: req.Image, - Cmd: req.Cmd, - Env: req.Env, - Labels: stringsToMap(req.Labels), - Tty: true, - OpenStdin: true, - ExposedPorts: exposeds, + container, err := client.ContainerCreate(ctx, config, hostConf, &network.NetworkingConfig{}, &v1.Platform{}, req.Name) + if err != nil { + _ = client.ContainerRemove(ctx, req.Name, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true}) + return err } - hostConf := &container.HostConfig{ - AutoRemove: req.AutoRemove, - PublishAllPorts: req.PublishAllPorts, - RestartPolicy: container.RestartPolicy{Name: req.RestartPolicy}, + global.LOG.Infof("create container %s successful! now check if the container is started and delete the container information if it is not.", req.Name) + if err := client.ContainerStart(ctx, container.ID, types.ContainerStartOptions{}); err != nil { + _ = client.ContainerRemove(ctx, req.Name, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true}) + return fmt.Errorf("create successful but start failed, err: %v", err) } - if req.RestartPolicy == "on-failure" { - hostConf.RestartPolicy.MaximumRetryCount = 5 + return nil +} + +func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.ContainerOperate, error) { + client, err := docker.NewDockerClient() + if err != nil { + return nil, err } - if req.NanoCPUs != 0 { - hostConf.NanoCPUs = req.NanoCPUs * 1000000000 + ctx := context.Background() + oldContainer, err := client.ContainerInspect(ctx, req.Name) + if err != nil { + return nil, err } - if req.CPUShares != 0 { - hostConf.CPUShares = req.CPUShares + + var data dto.ContainerOperate + data.Name = strings.ReplaceAll(oldContainer.Name, "/", "") + data.Image = oldContainer.Config.Image + data.Cmd = oldContainer.Config.Cmd + data.Env = oldContainer.Config.Env + for key, val := range oldContainer.Config.Labels { + data.Labels = append(data.Labels, fmt.Sprintf("%s=%s", key, val)) } - if req.Memory != 0 { - hostConf.Memory = req.Memory + for key, val := range oldContainer.HostConfig.PortBindings { + var itemPort dto.PortHelper + if !strings.Contains(string(key), "/") { + continue + } + itemPort.ContainerPort = strings.Split(string(key), "/")[0] + itemPort.Protocol = strings.Split(string(key), "/")[1] + for _, binds := range val { + itemPort.HostIP = binds.HostIP + itemPort.HostPort = binds.HostPort + data.ExposedPorts = append(data.ExposedPorts, itemPort) + } } - if len(req.ExposedPorts) != 0 { - hostConf.PortBindings = portMap + data.AutoRemove = oldContainer.HostConfig.AutoRemove + data.PublishAllPorts = oldContainer.HostConfig.PublishAllPorts + data.RestartPolicy = oldContainer.HostConfig.RestartPolicy.Name + if oldContainer.HostConfig.NanoCPUs != 0 { + data.NanoCPUs = oldContainer.HostConfig.NanoCPUs / 1000000000 } - if len(req.Volumes) != 0 { - config.Volumes = make(map[string]struct{}) - for _, volume := range req.Volumes { - config.Volumes[volume.ContainerDir] = struct{}{} - hostConf.Binds = append(hostConf.Binds, fmt.Sprintf("%s:%s:%s", volume.SourceDir, volume.ContainerDir, volume.Mode)) + if oldContainer.HostConfig.Memory != 0 { + data.Memory = oldContainer.HostConfig.Memory + } + for _, bind := range oldContainer.HostConfig.Binds { + parts := strings.Split(bind, ":") + if len(parts) != 3 { + continue } + data.Volumes = append(data.Volumes, dto.VolumeHelper{SourceDir: parts[0], ContainerDir: parts[1], Mode: parts[2]}) } - global.LOG.Infof("new container info %s has been made, now start to create", req.Name) + return &data, nil +} +func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error { + client, err := docker.NewDockerClient() + if err != nil { + return err + } ctx := context.Background() + oldContainer, err := client.ContainerInspect(ctx, req.Name) + if err != nil { + return err + } if !checkImageExist(client, req.Image) { if err := pullImages(ctx, client, req.Image); err != nil { return err } } + config := oldContainer.Config + hostConf := oldContainer.HostConfig + if err := loadConfigInfo(req, config, hostConf); err != nil { + return err + } + if err := client.ContainerRemove(ctx, req.Name, types.ContainerRemoveOptions{Force: true}); err != nil { + return err + } + + global.LOG.Infof("new container info %s has been update, now start to recreate", req.Name) container, err := client.ContainerCreate(ctx, config, hostConf, &network.NetworkingConfig{}, &v1.Platform{}, req.Name) if err != nil { - _ = client.ContainerRemove(ctx, req.Name, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true}) - return err + return fmt.Errorf("recreate contianer failed, err: %v", err) } - global.LOG.Infof("create container %s successful! now check if the container is started and delete the container information if it is not.", req.Name) + global.LOG.Infof("update container %s successful! now check if the container is started.", req.Name) if err := client.ContainerStart(ctx, container.ID, types.ContainerStartOptions{}); err != nil { - _ = client.ContainerRemove(ctx, req.Name, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true}) - return fmt.Errorf("create successful but start failed, err: %v", err) + return fmt.Errorf("update successful but start failed, err: %v", err) } + return nil } @@ -559,3 +611,44 @@ func checkPortStats(ports []dto.PortHelper) (nat.PortMap, error) { } return portMap, nil } + +func loadConfigInfo(req dto.ContainerOperate, config *container.Config, hostConf *container.HostConfig) error { + portMap, err := checkPortStats(req.ExposedPorts) + if err != nil { + return err + } + exposeds := make(nat.PortSet) + for port := range portMap { + exposeds[port] = struct{}{} + } + config.Image = req.Image + config.Cmd = req.Cmd + config.Env = req.Env + config.Labels = stringsToMap(req.Labels) + config.ExposedPorts = exposeds + + hostConf.AutoRemove = req.AutoRemove + hostConf.PublishAllPorts = req.PublishAllPorts + hostConf.RestartPolicy = container.RestartPolicy{Name: req.RestartPolicy} + if req.RestartPolicy == "on-failure" { + hostConf.RestartPolicy.MaximumRetryCount = 5 + } + if req.NanoCPUs != 0 { + hostConf.NanoCPUs = req.NanoCPUs * 1000000000 + } + if req.Memory != 0 { + hostConf.Memory = req.Memory + } + if len(req.ExposedPorts) != 0 { + hostConf.PortBindings = portMap + } + hostConf.Binds = []string{} + if len(req.Volumes) != 0 { + config.Volumes = make(map[string]struct{}) + for _, volume := range req.Volumes { + config.Volumes[volume.ContainerDir] = struct{}{} + hostConf.Binds = append(hostConf.Binds, fmt.Sprintf("%s:%s:%s", volume.SourceDir, volume.ContainerDir, volume.Mode)) + } + } + return nil +} diff --git a/backend/router/ro_container.go b/backend/router/ro_container.go index b8621dd82..594e17947 100644 --- a/backend/router/ro_container.go +++ b/backend/router/ro_container.go @@ -19,6 +19,8 @@ func (s *ContainerRouter) InitContainerRouter(Router *gin.RouterGroup) { baRouter.GET("/stats/:id", baseApi.ContainerStats) baRouter.POST("", baseApi.ContainerCreate) + baRouter.POST("/update", baseApi.ContainerUpdate) + baRouter.POST("/info", baseApi.ContainerInfo) baRouter.POST("/search", baseApi.SearchContainer) baRouter.GET("/search/log", baseApi.ContainerLogs) baRouter.POST("/clean/log", baseApi.CleanContainerLog) diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index 47dcd1dcb..5da806e28 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -916,7 +916,7 @@ var doc = `{ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/dto.ContainerCreate" + "$ref": "#/definitions/dto.ContainerOperate" } } ], @@ -1781,6 +1781,42 @@ var doc = `{ } } }, + "/containers/info": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器表单信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Load container info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithName" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ContainerOperate" + } + } + } + } + }, "/containers/inspect": { "post": { "security": [ @@ -2597,6 +2633,49 @@ var doc = `{ } } }, + "/containers/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新容器", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Update container", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContainerOperate" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "x-panel-log": { + "BeforeFuntions": [], + "bodyKeys": [ + "name", + "image" + ], + "formatEN": "update container [name][image]", + "formatZH": "更新容器 [name][image]", + "paramKeys": [] + } + } + }, "/containers/volume": { "post": { "security": [ @@ -10584,7 +10663,7 @@ var doc = `{ } } }, - "dto.ContainerCreate": { + "dto.ContainerOperate": { "type": "object", "properties": { "autoRemove": { @@ -10596,6 +10675,9 @@ var doc = `{ "type": "string" } }, + "cpushares": { + "type": "integer" + }, "env": { "type": "array", "items": { diff --git a/cmd/server/docs/swagger.json b/cmd/server/docs/swagger.json index 63807b809..ae97ed90e 100644 --- a/cmd/server/docs/swagger.json +++ b/cmd/server/docs/swagger.json @@ -902,7 +902,7 @@ "in": "body", "required": true, "schema": { - "$ref": "#/definitions/dto.ContainerCreate" + "$ref": "#/definitions/dto.ContainerOperate" } } ], @@ -1767,6 +1767,42 @@ } } }, + "/containers/info": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "获取容器表单信息", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Load container info", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.OperationWithName" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ContainerOperate" + } + } + } + } + }, "/containers/inspect": { "post": { "security": [ @@ -2583,6 +2619,49 @@ } } }, + "/containers/update": { + "post": { + "security": [ + { + "ApiKeyAuth": [] + } + ], + "description": "更新容器", + "consumes": [ + "application/json" + ], + "tags": [ + "Container" + ], + "summary": "Update container", + "parameters": [ + { + "description": "request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContainerOperate" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "x-panel-log": { + "BeforeFuntions": [], + "bodyKeys": [ + "name", + "image" + ], + "formatEN": "update container [name][image]", + "formatZH": "更新容器 [name][image]", + "paramKeys": [] + } + } + }, "/containers/volume": { "post": { "security": [ @@ -10570,7 +10649,7 @@ } } }, - "dto.ContainerCreate": { + "dto.ContainerOperate": { "type": "object", "properties": { "autoRemove": { @@ -10582,6 +10661,9 @@ "type": "string" } }, + "cpushares": { + "type": "integer" + }, "env": { "type": "array", "items": { diff --git a/cmd/server/docs/swagger.yaml b/cmd/server/docs/swagger.yaml index 872bb21aa..8aefbc18e 100644 --- a/cmd/server/docs/swagger.yaml +++ b/cmd/server/docs/swagger.yaml @@ -253,7 +253,7 @@ definitions: - name - path type: object - dto.ContainerCreate: + dto.ContainerOperate: properties: autoRemove: type: boolean @@ -261,6 +261,8 @@ definitions: items: type: string type: array + cpushares: + type: integer env: items: type: string @@ -3926,7 +3928,7 @@ paths: name: request required: true schema: - $ref: '#/definitions/dto.ContainerCreate' + $ref: '#/definitions/dto.ContainerOperate' responses: "200": description: "" @@ -4483,6 +4485,28 @@ paths: formatEN: tag image [reponame][targetName] formatZH: tag 镜像 [reponame][targetName] paramKeys: [] + /containers/info: + post: + consumes: + - application/json + description: 获取容器表单信息 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.OperationWithName' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.ContainerOperate' + security: + - ApiKeyAuth: [] + summary: Load container info + tags: + - Container /containers/inspect: post: consumes: @@ -5000,6 +5024,34 @@ paths: formatEN: update compose template information [name] formatZH: 更新 compose 模版 [name] paramKeys: [] + /containers/update: + post: + consumes: + - application/json + description: 更新容器 + parameters: + - description: request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.ContainerOperate' + responses: + "200": + description: "" + security: + - ApiKeyAuth: [] + summary: Update container + tags: + - Container + x-panel-log: + BeforeFuntions: [] + bodyKeys: + - name + - image + formatEN: update container [name][image] + formatZH: 更新容器 [name][image] + paramKeys: [] /containers/volume: post: consumes: diff --git a/frontend/src/api/interface/container.ts b/frontend/src/api/interface/container.ts index a14f98ae1..6f581d83c 100644 --- a/frontend/src/api/interface/container.ts +++ b/frontend/src/api/interface/container.ts @@ -10,13 +10,18 @@ export namespace Container { name: string; filters: string; } - export interface ContainerCreate { + export interface ContainerHelper { name: string; image: string; + cmdStr: string; + memoryUnit: string; + memoryItem: number; cmd: Array; publishAllPorts: boolean; exposedPorts: Array; nanoCPUs: number; + cpuShares: number; + cpuUnit: string; memory: number; volumes: Array; autoRemove: boolean; diff --git a/frontend/src/api/modules/container.ts b/frontend/src/api/modules/container.ts index e1f8c79d9..eea00225a 100644 --- a/frontend/src/api/modules/container.ts +++ b/frontend/src/api/modules/container.ts @@ -5,9 +5,15 @@ import { Container } from '../interface/container'; export const searchContainer = (params: Container.ContainerSearch) => { return http.post>(`/containers/search`, params, 400000); }; -export const createContainer = (params: Container.ContainerCreate) => { +export const createContainer = (params: Container.ContainerHelper) => { return http.post(`/containers`, params, 3000000); }; +export const updateContainer = (params: Container.ContainerHelper) => { + return http.post(`/containers/update`, params, 3000000); +}; +export const loadContainerInfo = (name: string) => { + return http.post(`/containers/info`, { name: name }); +}; export const cleanContainerLog = (containerName: string) => { return http.post(`/containers/clean/log`, { name: containerName }); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 7409689b0..4048def71 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -444,6 +444,8 @@ const message = { }, container: { createContainer: 'Create container', + updateContaienrHelper: + 'Container editing requires rebuilding the container. Any data that has not been persisted will be lost. Do you want to continue?', containerList: 'Container list', operatorHelper: '{0} will be performed on the selected container. Do you want to continue?', operatorAppHelper: @@ -489,6 +491,7 @@ const message = { user: 'User', command: 'Command', + commandHelper: 'Please enter the correct command, separated by spaces if there are multiple commands.', custom: 'Custom', emptyUser: 'When empty, you will log in as default', containerTerminal: 'Terminal', @@ -514,9 +517,10 @@ const message = { mode: 'Mode', env: 'Environment', restartPolicy: 'Restart policy', + always: 'always', unlessStopped: 'unless-stopped', onFailure: 'on-failure(five times by default)', - no: 'no', + no: 'never', image: 'Image', imagePull: 'Image pull', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index a26349acd..2de4334f4 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -453,6 +453,7 @@ const message = { }, container: { createContainer: '创建容器', + updateContaienrHelper: '容器编辑需要重建容器,任何未持久化的数据将会丢失,是否继续?', containerList: '容器列表', operatorHelper: '将对选中容器进行 {0} 操作,是否继续?', operatorAppHelper: '存在来源于应用商店的容器,{0} 操作可能会影响到该服务的正常使用,是否确认?', @@ -495,6 +496,7 @@ const message = { user: '用户', command: '命令', + commandHelper: '请输入正确的命令,多个命令空格分割', custom: '自定义', containerTerminal: '终端', emptyUser: '为空时,将使用容器默认的用户登录', @@ -520,6 +522,7 @@ const message = { mode: '权限', env: '环境变量', restartPolicy: '重启规则', + always: '一直重启', unlessStopped: '关闭后重启', onFailure: '失败后重启(默认重启 5 次)', no: '不重启', diff --git a/frontend/src/views/container/container/index.vue b/frontend/src/views/container/container/index.vue index 4b29d0737..0959bd079 100644 --- a/frontend/src/views/container/container/index.vue +++ b/frontend/src/views/container/container/index.vue @@ -9,7 +9,7 @@