diff --git a/agent/app/dto/container.go b/agent/app/dto/container.go index 7d4f15e12e28..6f433ab6d083 100644 --- a/agent/app/dto/container.go +++ b/agent/app/dto/container.go @@ -134,10 +134,15 @@ type ExtraHost struct { IP string `json:"ip"` } type ContainerNetwork struct { - Network string `json:"network"` - Ipv4 string `json:"ipv4"` - Ipv6 string `json:"ipv6"` - MacAddr string `json:"macAddr"` + Network string `json:"network"` + Ipv4 string `json:"ipv4"` + Ipv6 string `json:"ipv6"` + MacAddr string `json:"macAddr"` + Links []string `json:"links"` + Aliases []string `json:"aliases"` + DriverOpts map[string]string `json:"driverOpts"` + GwPriority int `json:"gwPriority"` + LinkLocalIPs []string `json:"linkLocalIPs"` } type ContainerCreateByCommand struct { diff --git a/agent/app/service/backup_container.go b/agent/app/service/backup_container.go index 7b2af342e79e..2a6267fb223d 100644 --- a/agent/app/service/backup_container.go +++ b/agent/app/service/backup_container.go @@ -604,23 +604,40 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T return nil } -func removeUnsupportedEndpointStaticIPAM(cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) { +func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) { + if cli.NewVersionError(ctx, "1.44", "specify mac-address per network") != nil { + removeEndpointMacAddresses(primary, extras) + } + endpointGroups := []map[string]*network.EndpointSettings{extras} if primary != nil { - removeUnsupportedEndpointStaticIPAMFromEndpoints(cli, primary.EndpointsConfig) + endpointGroups = append(endpointGroups, primary.EndpointsConfig) + } + for _, endpoints := range endpointGroups { + for netName, endpoint := range endpoints { + if endpoint == nil || endpoint.IPAMConfig == nil { + continue + } + info, err := cli.NetworkInspect(ctx, netName, network.InspectOptions{}) + if err != nil { + continue + } + removeUnsupportedEndpointStaticIP(netName, info, endpoint) + } } - removeUnsupportedEndpointStaticIPAMFromEndpoints(cli, extras) } -func removeUnsupportedEndpointStaticIPAMFromEndpoints(cli *client.Client, endpoints map[string]*network.EndpointSettings) { - for netName, endpoint := range endpoints { - if endpoint == nil || endpoint.IPAMConfig == nil { - continue +func removeEndpointMacAddresses(primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) { + if primary != nil { + for _, endpoint := range primary.EndpointsConfig { + if endpoint != nil { + endpoint.MacAddress = "" + } } - info, err := cli.NetworkInspect(context.Background(), netName, network.InspectOptions{}) - if err != nil { - continue + } + for _, endpoint := range extras { + if endpoint != nil { + endpoint.MacAddress = "" } - removeUnsupportedEndpointStaticIP(netName, info, endpoint) } } @@ -639,7 +656,7 @@ func removeUnsupportedEndpointStaticIP(netName string, info network.Inspect, end if endpoint.IPAMConfig.IPv6Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv6Address, true) { endpoint.IPAMConfig.IPv6Address = "" } - if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" { + if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" && len(endpoint.IPAMConfig.LinkLocalIPs) == 0 { endpoint.IPAMConfig = nil } } @@ -776,11 +793,30 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin if name == "host" || name == "none" { continue } - endpointSetting := &network.EndpointSettings{Aliases: append([]string(nil), endpoint.Aliases...), MacAddress: endpoint.MacAddress} + if endpoint == nil { + if name == primaryName { + config.EndpointsConfig[name] = &network.EndpointSettings{} + } else { + extraNetworks[name] = &network.EndpointSettings{} + } + continue + } + endpointSetting := &network.EndpointSettings{ + Links: append([]string(nil), endpoint.Links...), + Aliases: append([]string(nil), endpoint.Aliases...), + DriverOpts: cloneStringMap(endpoint.DriverOpts), + GwPriority: endpoint.GwPriority, + } if endpoint.IPAMConfig != nil { endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{ - IPv4Address: endpoint.IPAMConfig.IPv4Address, - IPv6Address: endpoint.IPAMConfig.IPv6Address, + IPv4Address: endpoint.IPAMConfig.IPv4Address, + IPv6Address: endpoint.IPAMConfig.IPv6Address, + LinkLocalIPs: append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...), + } + } else if name != "bridge" && (endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "") { + endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{ + IPv4Address: endpoint.IPAddress, + IPv6Address: endpoint.GlobalIPv6Address, } } if name == primaryName { @@ -795,6 +831,39 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin return config, extraNetworks } +const unsupportedUserSpecifiedIPAddress = "user specified IP address is supported only when connecting to networks with user configured subnets" + +func clearUnsupportedDynamicEndpointIPAM(err error, endpoints map[string]*network.EndpointSettings, networkSettings *container.NetworkSettings) bool { + if err == nil || !strings.Contains(err.Error(), unsupportedUserSpecifiedIPAddress) { + return false + } + for name, endpoint := range endpoints { + if !isDynamicContainerNetwork(networkSettings, name) || endpoint == nil || endpoint.IPAMConfig == nil { + continue + } + if strings.Contains(err.Error(), "network "+name+":") { + endpoint.IPAMConfig = nil + return true + } + } + cleared := false + for name, endpoint := range endpoints { + if isDynamicContainerNetwork(networkSettings, name) && endpoint != nil && endpoint.IPAMConfig != nil { + endpoint.IPAMConfig = nil + cleared = true + } + } + return cleared +} + +func isDynamicContainerNetwork(networkSettings *container.NetworkSettings, name string) bool { + if networkSettings == nil || name == "bridge" { + return false + } + endpoint := networkSettings.Networks[name] + return endpoint != nil && endpoint.IPAMConfig == nil && (endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "") +} + func cloneContainerConfig(config *container.Config) *container.Config { if config == nil { return &container.Config{} diff --git a/agent/app/service/container.go b/agent/app/service/container.go index 4c262234339e..59c89a4c17cc 100644 --- a/agent/app/service/container.go +++ b/agent/app/service/container.go @@ -485,15 +485,19 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo if err != nil { return err } - defer client.Close() + unlock := containerOperationLock.lock(req.Name) ctx := context.Background() newContainer, _ := client.ContainerInspect(ctx, req.Name) if newContainer.ContainerJSONBase != nil { + unlock() + _ = client.Close() return buserr.New("ErrContainerName") } taskItem, err := task.NewTaskWithOps(req.Name, task.TaskCreate, task.TaskScopeContainer, req.TaskID, 1) if err != nil { + unlock() + _ = client.Close() global.LOG.Errorf("new task for create container failed, err: %v", err) return err } @@ -529,18 +533,20 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo if err != nil { return err } - removeUnsupportedEndpointStaticIPAM(client, networkConf, nil) + normalizeContainerEndpointSettings(ctx, client, networkConf, nil) con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name) if err != nil { taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed")) - _ = client.ContainerRemove(ctx, req.Name, container.RemoveOptions{RemoveVolumes: true, Force: true}) + if con.ID != "" { + _ = client.ContainerRemove(ctx, con.ID, container.RemoveOptions{RemoveVolumes: true, Force: true}) + } return err } err = client.ContainerStart(ctx, con.ID, container.StartOptions{}) taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err) if err != nil { taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed")) - _ = client.ContainerRemove(ctx, req.Name, container.RemoveOptions{RemoveVolumes: true, Force: true}) + _ = client.ContainerRemove(ctx, con.ID, container.RemoveOptions{RemoveVolumes: true, Force: true}) return fmt.Errorf("create successful but start failed, err: %v", err) } return nil @@ -548,12 +554,16 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo if inThread { go func() { + defer unlock() + defer client.Close() if err := taskItem.Execute(); err != nil { global.LOG.Error(err.Error()) } }() return nil } + defer unlock() + defer client.Close() return taskItem.Execute() } @@ -574,21 +584,7 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai data.Image = oldContainer.Config.Image if oldContainer.NetworkSettings != nil { for net, val := range oldContainer.NetworkSettings.Networks { - netItem := dto.ContainerNetwork{ - Network: net, - MacAddr: val.MacAddress, - } - if val.IPAMConfig != nil { - if netItem.Network != "bridge" { - netItem.Ipv4 = val.IPAMConfig.IPv4Address - netItem.Ipv6 = val.IPAMConfig.IPv6Address - } - } else { - if netItem.Network != "bridge" { - netItem.Ipv4 = val.IPAddress - } - } - data.Networks = append(data.Networks, netItem) + data.Networks = append(data.Networks, loadContainerNetworkInfo(net, val)) } } @@ -634,141 +630,40 @@ func (u *ContainerService) ContainerInfo(req dto.OperationWithName) (*dto.Contai return &data, nil } -func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error { - client, err := docker.NewDockerClient() - if err != nil { - return err - } - defer client.Close() - ctx := context.Background() - oldContainer, err := client.ContainerInspect(ctx, req.Name) - if err != nil { - return err - } - - taskItem, err := task.NewTaskWithOps(req.Name, task.TaskUpdate, task.TaskScopeContainer, req.TaskID, 1) - if err != nil { - global.LOG.Errorf("new task for create container failed, err: %v", err) - return err - } - go func() { - taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error { - if !checkImageExist(client, req.Image) || req.ForcePull { - if err := pullImages(taskItem, client, req.Image); err != nil { - if !req.ForcePull { - return err - } - return fmt.Errorf("pull image %s failed, err: %v", req.Image, err) - } - } - return nil - }, nil) - - taskItem.AddSubTask(i18n.GetWithName("ContainerCreate", req.Name), func(t *task.Task) error { - err := client.ContainerRemove(ctx, req.Name, container.RemoveOptions{Force: true}) - taskItem.LogWithStatus(i18n.GetWithName("ContainerRemoveOld", req.Name), err) - if err != nil { - return err - } - - config, hostConf, networkConf, err := loadConfigInfo(false, req, &oldContainer) - taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerLoadInfo"), err) - if err != nil { - taskItem.Log(i18n.GetMsgByKey("ContainerRecreate")) - reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings) - return err - } - removeUnsupportedEndpointStaticIPAM(client, networkConf, nil) - - con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name) - if err != nil { - taskItem.Log(i18n.GetMsgByKey("ContainerRecreate")) - reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings) - return fmt.Errorf("update container failed, err: %v", err) - } - err = client.ContainerStart(ctx, con.ID, container.StartOptions{}) - taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err) - if err != nil { - return fmt.Errorf("update successful but start failed, err: %v", err) - } - return nil - }, nil) - - if err := taskItem.Execute(); err != nil { - global.LOG.Error(err.Error()) +func loadContainerNetworkInfo(name string, endpoint *network.EndpointSettings) dto.ContainerNetwork { + item := dto.ContainerNetwork{Network: name} + if endpoint == nil { + return item + } + item.MacAddr = endpoint.MacAddress + item.Links = append([]string(nil), endpoint.Links...) + item.Aliases = append([]string(nil), endpoint.Aliases...) + item.DriverOpts = cloneStringMap(endpoint.DriverOpts) + item.GwPriority = endpoint.GwPriority + if endpoint.IPAMConfig != nil { + item.LinkLocalIPs = append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...) + } + if name != "bridge" { + if endpoint.IPAMConfig != nil { + item.Ipv4 = endpoint.IPAMConfig.IPv4Address + item.Ipv6 = endpoint.IPAMConfig.IPv6Address + } else { + item.Ipv4 = endpoint.IPAddress + item.Ipv6 = endpoint.GlobalIPv6Address } - }() - - return nil + } + return item } -func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error { - client, err := docker.NewDockerClient() - if err != nil { - return err +func cloneStringMap(source map[string]string) map[string]string { + if len(source) == 0 { + return nil } - defer client.Close() - ctx := context.Background() - taskItem, err := task.NewTaskWithOps(req.Image, task.TaskUpgrade, task.TaskScopeImage, req.TaskID, 1) - if err != nil { - global.LOG.Errorf("new task for create container failed, err: %v", err) - return err + result := make(map[string]string, len(source)) + for key, value := range source { + result[key] = value } - go func() { - taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error { - taskItem.LogStart(i18n.GetWithName("ContainerImagePull", req.Image)) - if !checkImageExist(client, req.Image) || req.ForcePull { - if err := pullImages(taskItem, client, req.Image); err != nil { - if !req.ForcePull { - return err - } - return fmt.Errorf("pull image %s failed, err: %v", req.Image, err) - } - } - return nil - }, nil) - for _, item := range req.Names { - var oldContainer container.InspectResponse - taskItem.AddSubTask(i18n.GetWithName("ContainerLoadInfo", item), func(t *task.Task) error { - taskItem.Logf("----------------- %s -----------------", item) - oldContainer, err = client.ContainerInspect(ctx, item) - if err != nil { - return err - } - return nil - }, nil) - - taskItem.AddSubTask(i18n.GetWithName("ContainerCreate", item), func(t *task.Task) error { - config := oldContainer.Config - config.Image = req.Image - hostConf := oldContainer.HostConfig - err := client.ContainerRemove(ctx, item, container.RemoveOptions{Force: true}) - taskItem.LogWithStatus(i18n.GetWithName("ContainerRemoveOld", item), err) - if err != nil { - return err - } - - con, err := createContainerWithOldNetworks(ctx, client, config, hostConf, oldContainer.NetworkSettings, item) - if err != nil { - taskItem.Log(i18n.GetMsgByKey("ContainerRecreate")) - reCreateAfterUpdate(item, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings) - return fmt.Errorf("upgrade container failed, err: %v", err) - } - err = client.ContainerStart(ctx, con.ID, container.StartOptions{}) - taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerStartCheck"), err) - if err != nil { - return fmt.Errorf("upgrade successful but start failed, err: %v", err) - } - return nil - }, nil) - - } - if err := taskItem.Execute(); err != nil { - global.LOG.Error(err.Error()) - } - }() - - return nil + return result } func (u *ContainerService) ContainerRename(req dto.ContainerRename) error { @@ -778,6 +673,8 @@ func (u *ContainerService) ContainerRename(req dto.ContainerRename) error { return err } defer client.Close() + unlock := containerOperationLock.lock(req.Name, req.NewName) + defer unlock() newContainer, _ := client.ContainerInspect(ctx, req.NewName) if newContainer.ContainerJSONBase != nil { @@ -822,44 +719,48 @@ func (u *ContainerService) ContainerCommit(req dto.ContainerCommit) error { } func (u *ContainerService) ContainerOperation(req dto.ContainerOperation) error { - var err error ctx := context.Background() client, err := docker.NewDockerClient() if err != nil { return err } - defer client.Close() taskItem, err := task.NewTaskWithOps(strings.Join(req.Names, " "), req.Operation, task.TaskScopeContainer, req.TaskID, 1) if err != nil { + _ = client.Close() return fmt.Errorf("new task for container commit failed, err: %v", err) } for _, item := range req.Names { + item := item taskItem.AddSubTask(item, func(t *task.Task) error { + unlock := containerOperationLock.lock(item) + defer unlock() + var operationErr error switch req.Operation { case constant.ContainerOpStart: - err = client.ContainerStart(ctx, item, container.StartOptions{}) + operationErr = client.ContainerStart(ctx, item, container.StartOptions{}) case constant.ContainerOpStop: - err = client.ContainerStop(ctx, item, container.StopOptions{}) + operationErr = client.ContainerStop(ctx, item, container.StopOptions{}) case constant.ContainerOpRestart: - err = client.ContainerRestart(ctx, item, container.StopOptions{}) + operationErr = client.ContainerRestart(ctx, item, container.StopOptions{}) case constant.ContainerOpKill: - err = client.ContainerKill(ctx, item, "SIGKILL") + operationErr = client.ContainerKill(ctx, item, "SIGKILL") case constant.ContainerOpPause: - err = client.ContainerPause(ctx, item) + operationErr = client.ContainerPause(ctx, item) case constant.ContainerOpUnpause: - err = client.ContainerUnpause(ctx, item) + operationErr = client.ContainerUnpause(ctx, item) case constant.ContainerOpRemove: - err = client.ContainerRemove(ctx, item, container.RemoveOptions{RemoveVolumes: true, Force: true}) + operationErr = client.ContainerRemove(ctx, item, container.RemoveOptions{RemoveVolumes: true, Force: true}) } - return err + return operationErr }, nil) } go func() { + defer client.Close() _ = taskItem.Execute() }() - return err + return nil } func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error { @@ -868,6 +769,8 @@ func (u *ContainerService) ContainerLogClean(req dto.OperationWithName) error { return err } defer client.Close() + unlock := containerOperationLock.lock(req.Name) + defer unlock() ctx := context.Background() containerItem, err := client.ContainerInspect(ctx, req.Name) if err != nil { @@ -1832,7 +1735,7 @@ func loadCpuAndMem(client *client.Client, containerItem string) dto.ContainerLis return data } -func checkPortStats(ports []dto.PortHelper) (nat.PortMap, error) { +func checkPortStats(ports []dto.PortHelper, checkInUse bool) (nat.PortMap, error) { portMap := make(nat.PortMap) if len(ports) == 0 { return portMap, nil @@ -1857,7 +1760,7 @@ func checkPortStats(ports []dto.PortHelper) (nat.PortMap, error) { portMap[nat.Port(fmt.Sprintf("%d/%s", containerStart+i, port.Protocol))] = []nat.PortBinding{bindItem} } for i := hostStart; i <= hostEnd; i++ { - if common.ScanPortWithIP(port.HostIP, i) { + if checkInUse && common.ScanPortWithIP(port.HostIP, i) { return portMap, buserr.WithDetail("ErrPortInUsed", i, nil) } } @@ -1868,7 +1771,7 @@ func checkPortStats(ports []dto.PortHelper) (nat.PortMap, error) { } else { portItem, _ = strconv.Atoi(port.HostPort) } - if common.ScanPortWithIP(port.HostIP, portItem) { + if checkInUse && common.ScanPortWithIP(port.HostIP, portItem) { return portMap, buserr.WithDetail("ErrPortInUsed", portItem, nil) } bindItem := nat.PortBinding{HostPort: strconv.Itoa(portItem), HostIP: port.HostIP} @@ -1887,7 +1790,7 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *conta } var networkConf network.NetworkingConfig - portMap, err := checkPortStats(req.ExposedPorts) + portMap, err := checkPortStats(req.ExposedPorts, isCreate) if err != nil { return nil, nil, nil, err } @@ -1915,15 +1818,21 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *conta case "host", "none", "bridge": hostConf.NetworkMode = container.NetworkMode(item.Network) } - if item.Ipv4 != "" || item.Ipv6 != "" { - networkConf.EndpointsConfig[item.Network] = &network.EndpointSettings{ - IPAMConfig: &network.EndpointIPAMConfig{ - IPv4Address: item.Ipv4, - IPv6Address: item.Ipv6, - }, MacAddress: item.MacAddr} - } else { - networkConf.EndpointsConfig[item.Network] = &network.EndpointSettings{} + endpoint := &network.EndpointSettings{ + Links: append([]string(nil), item.Links...), + Aliases: append([]string(nil), item.Aliases...), + DriverOpts: cloneStringMap(item.DriverOpts), + GwPriority: item.GwPriority, + MacAddress: item.MacAddr, + } + if item.Ipv4 != "" || item.Ipv6 != "" || len(item.LinkLocalIPs) != 0 { + endpoint.IPAMConfig = &network.EndpointIPAMConfig{ + IPv4Address: item.Ipv4, + IPv6Address: item.Ipv6, + LinkLocalIPs: append([]string(nil), item.LinkLocalIPs...), + } } + networkConf.EndpointsConfig[item.Network] = endpoint } } else { return nil, nil, nil, fmt.Errorf("please set up the network") @@ -1970,43 +1879,6 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *conta return &config, &hostConf, &networkConf, nil } -func reCreateAfterUpdate(name string, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkConf *container.NetworkSettings) { - ctx := context.Background() - - oldContainer, err := createContainerWithOldNetworks(ctx, client, config, hostConf, networkConf, name) - if err != nil { - global.LOG.Errorf("recreate after container update failed, err: %v", err) - return - } - if err := client.ContainerStart(ctx, oldContainer.ID, container.StartOptions{}); err != nil { - global.LOG.Errorf("restart after container update failed, err: %v", err) - } - global.LOG.Info("recreate after container update successful") -} - -func createContainerWithOldNetworks(ctx context.Context, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkSettings *container.NetworkSettings, name string) (container.CreateResponse, error) { - networkConf, extraNetworks := buildContainerRecoverNetworkConfig(networkSettings, hostConf) - removeUnsupportedEndpointStaticIPAM(client, networkConf, extraNetworks) - - created, err := client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name) - if err != nil { - return created, err - } - - extraNames := make([]string, 0, len(extraNetworks)) - for item := range extraNetworks { - extraNames = append(extraNames, item) - } - sort.Strings(extraNames) - for _, item := range extraNames { - if err := client.NetworkConnect(ctx, item, created.ID, extraNetworks[item]); err != nil { - _ = client.ContainerRemove(ctx, created.ID, container.RemoveOptions{Force: true}) - return created, err - } - } - return created, nil -} - func loadVolumeBinds(binds []container.MountPoint) []dto.VolumeHelper { var datas []dto.VolumeHelper for _, bind := range binds { diff --git a/agent/app/service/container_update.go b/agent/app/service/container_update.go new file mode 100644 index 000000000000..3d268de681a1 --- /dev/null +++ b/agent/app/service/container_update.go @@ -0,0 +1,657 @@ +package service + +import ( + "context" + "errors" + "fmt" + "sort" + "strings" + "sync" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/task" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/i18n" + "github.com/1Panel-dev/1Panel/agent/utils/docker" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/mount" + "github.com/docker/docker/api/types/network" + "github.com/docker/docker/client" + v1 "github.com/opencontainers/image-spec/specs-go/v1" +) + +func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error { + client, err := docker.NewDockerClient() + if err != nil { + return err + } + ctx := context.Background() + + taskItem, err := task.NewTaskWithOps(req.Name, task.TaskUpdate, task.TaskScopeContainer, req.TaskID, 1) + if err != nil { + _ = client.Close() + global.LOG.Errorf("new task for create container failed, err: %v", err) + return err + } + go func() { + defer client.Close() + taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error { + if !checkImageExist(client, req.Image) || req.ForcePull { + if err := pullImages(taskItem, client, req.Image); err != nil { + if !req.ForcePull { + return err + } + return fmt.Errorf("pull image %s failed, err: %v", req.Image, err) + } + } + return nil + }, nil) + + taskItem.AddSubTaskWithOps(task.GetTaskName(req.Name, task.TaskUpdate, task.TaskScopeContainer), func(t *task.Task) error { + t.LogStart(i18n.GetWithName("ContainerAcquireLock", req.Name)) + unlock := containerOperationLock.lock(req.Name) + defer unlock() + t.LogWithStatus(i18n.GetWithName("ContainerAcquireLock", req.Name), nil) + + oldContainer, err := client.ContainerInspect(ctx, req.Name) + if err != nil { + taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerLoadInfo"), err) + return err + } + config, hostConf, networkConf, err := loadConfigInfo(false, req, &oldContainer) + taskItem.LogWithStatus(i18n.GetMsgByKey("ContainerLoadInfo"), err) + if err != nil { + return err + } + normalizeContainerEndpointSettings(ctx, client, networkConf, nil) + + cleanupErr, err := switchContainer(ctx, client, req.Name, oldContainer, func() (container.CreateResponse, error) { + return createContainerWithDynamicIPFallback(func() (container.CreateResponse, error) { + return client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name) + }, networkConf.EndpointsConfig, oldContainer.NetworkSettings) + }, newContainerSwitchTaskLogger(t)) + if err != nil { + return fmt.Errorf("update container failed, err: %v", err) + } + if cleanupErr != nil { + taskItem.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr)) + } + return nil + }, nil, 0, 0) + + if err := taskItem.Execute(); err != nil { + global.LOG.Error(err.Error()) + } + }() + + return nil +} + +func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error { + client, err := docker.NewDockerClient() + if err != nil { + return err + } + ctx := context.Background() + taskItem, err := task.NewTaskWithOps(req.Image, task.TaskUpgrade, task.TaskScopeImage, req.TaskID, 1) + if err != nil { + _ = client.Close() + global.LOG.Errorf("new task for create container failed, err: %v", err) + return err + } + go func() { + defer client.Close() + taskItem.AddSubTask(i18n.GetWithName("ContainerImagePull", req.Image), func(t *task.Task) error { + taskItem.LogStart(i18n.GetWithName("ContainerImagePull", req.Image)) + if !checkImageExist(client, req.Image) || req.ForcePull { + if err := pullImages(taskItem, client, req.Image); err != nil { + if !req.ForcePull { + return err + } + return fmt.Errorf("pull image %s failed, err: %v", req.Image, err) + } + } + return nil + }, nil) + var upgradeErrors []error + for _, item := range req.Names { + item := item + taskItem.AddSubTaskWithIgnoreErr(i18n.GetWithName("ContainerUpgradeItem", item), func(t *task.Task) error { + t.Logf("----------------- %s -----------------", item) + t.LogStart(i18n.GetWithName("ContainerAcquireLock", item)) + unlock := containerOperationLock.lock(item) + defer unlock() + t.LogWithStatus(i18n.GetWithName("ContainerAcquireLock", item), nil) + + oldContainer, inspectErr := client.ContainerInspect(ctx, item) + t.LogWithStatus(i18n.GetWithName("ContainerLoadInfo", item), inspectErr) + if inspectErr != nil { + err := fmt.Errorf("reload container %s failed: %w", item, inspectErr) + upgradeErrors = append(upgradeErrors, err) + return err + } + config := cloneContainerConfig(oldContainer.Config) + config.Image = req.Image + hostConf := cloneContainerHostConfig(oldContainer.HostConfig) + preserveContainerVolumeMounts(hostConf, oldContainer.Mounts) + cleanupErr, err := switchContainer(ctx, client, item, oldContainer, func() (container.CreateResponse, error) { + return createContainerWithOldNetworks(ctx, client, config, hostConf, oldContainer.NetworkSettings, item) + }, newContainerSwitchTaskLogger(t)) + if err != nil { + upgradeErr := fmt.Errorf("upgrade container %s failed: %w", item, err) + upgradeErrors = append(upgradeErrors, upgradeErr) + return upgradeErr + } + if cleanupErr != nil { + t.Log(i18n.GetWithNameAndErr("ContainerCleanupWarning", containerSwitchBackupName(oldContainer.ID), cleanupErr)) + } + return nil + }) + } + taskItem.AddSubTask(i18n.GetMsgByKey("ContainerUpgradeSummary"), func(t *task.Task) error { + return errors.Join(upgradeErrors...) + }, nil) + if err := taskItem.Execute(); err != nil { + global.LOG.Error(err.Error()) + } + }() + + return nil +} + +type containerSwitchClient interface { + ContainerStop(context.Context, string, container.StopOptions) error + ContainerRename(context.Context, string, string) error + ContainerStart(context.Context, string, container.StartOptions) error + ContainerRemove(context.Context, string, container.RemoveOptions) error + ContainerInspect(context.Context, string) (container.InspectResponse, error) + NetworkConnect(context.Context, string, string, *network.EndpointSettings) error + NetworkDisconnect(context.Context, string, string, bool) error +} + +type containerOperationMutex struct { + mutex sync.Mutex + locks map[string]*containerOperationLockEntry +} + +var containerOperationLock containerOperationMutex + +type containerOperationLockEntry struct { + mutex sync.Mutex + references int +} + +func (l *containerOperationMutex) lock(names ...string) func() { + nameSet := make(map[string]struct{}, len(names)) + for _, name := range names { + if name != "" { + nameSet[name] = struct{}{} + } + } + orderedNames := make([]string, 0, len(nameSet)) + for name := range nameSet { + orderedNames = append(orderedNames, name) + } + sort.Strings(orderedNames) + if len(orderedNames) == 0 { + return func() {} + } + + l.mutex.Lock() + if l.locks == nil { + l.locks = make(map[string]*containerOperationLockEntry) + } + entries := make([]*containerOperationLockEntry, 0, len(orderedNames)) + for _, name := range orderedNames { + entry := l.locks[name] + if entry == nil { + entry = &containerOperationLockEntry{} + l.locks[name] = entry + } + entry.references++ + entries = append(entries, entry) + } + l.mutex.Unlock() + + for _, entry := range entries { + entry.mutex.Lock() + } + return func() { + for index := len(entries) - 1; index >= 0; index-- { + entries[index].mutex.Unlock() + } + l.mutex.Lock() + defer l.mutex.Unlock() + for index, name := range orderedNames { + entry := entries[index] + entry.references-- + if entry.references == 0 { + delete(l.locks, name) + } + } + } +} + +type containerNetworkAttachment struct { + name string + endpoint *network.EndpointSettings + isDynamic bool +} + +type containerSwitchLogFunc func(messageKey, containerName string, err error) + +func newContainerSwitchTaskLogger(t *task.Task) containerSwitchLogFunc { + return func(messageKey, containerName string, err error) { + t.LogWithStatus(i18n.GetWithName(messageKey, containerName), err) + } +} + +func logContainerSwitchStep(logger containerSwitchLogFunc, messageKey, containerName string, err error) { + if logger != nil { + logger(messageKey, containerName, err) + } +} + +// switchContainer keeps the stopped original container as the rollback target until the replacement starts. +func switchContainer( + ctx context.Context, + cli containerSwitchClient, + name string, + oldContainer container.InspectResponse, + createNew func() (container.CreateResponse, error), + logger containerSwitchLogFunc, +) (cleanupErr error, err error) { + if oldContainer.ID == "" { + return nil, fmt.Errorf("original container ID is empty") + } + wasRunning := oldContainer.State != nil && oldContainer.State.Running + if wasRunning && oldContainer.HostConfig != nil && oldContainer.HostConfig.AutoRemove { + return nil, fmt.Errorf("cannot safely replace container %s with auto-remove enabled", name) + } + + if wasRunning { + if err := cli.ContainerStop(ctx, oldContainer.ID, container.StopOptions{}); err != nil { + logContainerSwitchStep(logger, "ContainerStopOld", name, err) + current, inspectErr := cli.ContainerInspect(ctx, oldContainer.ID) + if inspectErr == nil && current.State != nil && !current.State.Running { + restartErr := restartOriginalContainer(ctx, cli, oldContainer.ID) + logContainerSwitchStep(logger, "ContainerRollbackRestartOld", name, restartErr) + return nil, errors.Join(fmt.Errorf("stop original container failed: %w", err), restartErr) + } + return nil, fmt.Errorf("stop original container failed: %w", err) + } + logContainerSwitchStep(logger, "ContainerStopOld", name, nil) + } + + backupName := containerSwitchBackupName(oldContainer.ID) + if err := cli.ContainerRename(ctx, oldContainer.ID, backupName); err != nil { + logContainerSwitchStep(logger, "ContainerRenameOld", name, err) + if wasRunning { + restartErr := restartOriginalContainer(ctx, cli, oldContainer.ID) + logContainerSwitchStep(logger, "ContainerRollbackRestartOld", name, restartErr) + return nil, errors.Join(fmt.Errorf("rename original container failed: %w", err), restartErr) + } + return nil, fmt.Errorf("rename original container failed: %w", err) + } + logContainerSwitchStep(logger, "ContainerRenameOld", name, nil) + disconnectedNetworks, disconnectErr := disconnectOriginalContainerNetworks(ctx, cli, oldContainer) + logContainerSwitchStep(logger, "ContainerDisconnectOld", backupName, disconnectErr) + if disconnectErr != nil { + rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, "", disconnectedNetworks, logger) + return nil, errors.Join(disconnectErr, rollbackErr) + } + + created, createErr := createNew() + logContainerSwitchStep(logger, "ContainerCreateReplacement", name, createErr) + if createErr != nil { + rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger) + return nil, errors.Join(createErr, rollbackErr) + } + if err := cli.ContainerStart(ctx, created.ID, container.StartOptions{}); err != nil { + logContainerSwitchStep(logger, "ContainerStartReplacement", name, err) + rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger) + return nil, errors.Join(fmt.Errorf("start new container failed: %w", err), rollbackErr) + } + logContainerSwitchStep(logger, "ContainerStartReplacement", name, nil) + if wasRunning { + if err := waitContainerReady(ctx, cli, created.ID); err != nil { + logContainerSwitchStep(logger, "ContainerWaitReplacement", name, err) + rollbackErr := restoreOriginalContainer(ctx, cli, oldContainer.ID, name, wasRunning, created.ID, disconnectedNetworks, logger) + return nil, errors.Join(fmt.Errorf("new container readiness check failed: %w", err), rollbackErr) + } + logContainerSwitchStep(logger, "ContainerWaitReplacement", name, nil) + } + + cleanupErr = cli.ContainerRemove(ctx, oldContainer.ID, container.RemoveOptions{Force: true, RemoveVolumes: false}) + logContainerSwitchStep(logger, "ContainerRemoveOld", backupName, cleanupErr) + return cleanupErr, nil +} + +const ( + containerStartStabilization = 10 * time.Second + containerStartPollInterval = time.Second + containerHealthCheckMinWait = 30 * time.Second + containerHealthCheckMaxWait = 10 * time.Minute +) + +func waitContainerReady(ctx context.Context, cli containerSwitchClient, containerID string) error { + info, err := cli.ContainerInspect(ctx, containerID) + if err != nil { + return err + } + if err := checkContainerRunningState(info); err != nil { + return err + } + if info.State.Health == nil { + return waitContainerStable(ctx, cli, containerID, info) + } + + timeout := containerHealthCheckTimeout(info.Config) + deadline := time.NewTimer(timeout) + ticker := time.NewTicker(time.Second) + defer deadline.Stop() + defer ticker.Stop() + for { + if info.State.Restarting || info.RestartCount != 0 { + return fmt.Errorf("container restarted %d times during startup", info.RestartCount) + } + if info.State.Health == nil { + return fmt.Errorf("container health status is unavailable") + } + switch info.State.Health.Status { + case container.Healthy: + return nil + case container.Unhealthy: + return fmt.Errorf("container health status is unhealthy") + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-deadline.C: + return fmt.Errorf("container health check timed out after %s", timeout) + case <-ticker.C: + info, err = cli.ContainerInspect(ctx, containerID) + if err != nil { + return err + } + if err := checkContainerRunningState(info); err != nil { + return err + } + } + } +} + +func waitContainerStable(ctx context.Context, cli containerSwitchClient, containerID string, initial container.InspectResponse) error { + startedAt := initial.State.StartedAt + if err := checkContainerStableState(initial, startedAt); err != nil { + return err + } + deadline := time.NewTimer(containerStartStabilization) + ticker := time.NewTicker(containerStartPollInterval) + defer deadline.Stop() + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-deadline.C: + info, err := cli.ContainerInspect(ctx, containerID) + if err != nil { + return err + } + return checkContainerStableState(info, startedAt) + case <-ticker.C: + info, err := cli.ContainerInspect(ctx, containerID) + if err != nil { + return err + } + if err := checkContainerStableState(info, startedAt); err != nil { + return err + } + } + } +} + +func checkContainerStableState(info container.InspectResponse, startedAt string) error { + if err := checkContainerRunningState(info); err != nil { + return err + } + if info.State.Restarting || info.RestartCount != 0 { + return fmt.Errorf("container restarted %d times during startup", info.RestartCount) + } + if startedAt != "" && info.State.StartedAt != startedAt { + return fmt.Errorf("container start time changed during startup") + } + return nil +} + +func checkContainerRunningState(info container.InspectResponse) error { + if info.State == nil { + return fmt.Errorf("container state is unavailable") + } + if !info.State.Running { + return fmt.Errorf("container exited with code %d: %s", info.State.ExitCode, info.State.Error) + } + return nil +} + +func containerHealthCheckTimeout(config *container.Config) time.Duration { + if config == nil || config.Healthcheck == nil { + return containerHealthCheckMinWait + } + health := config.Healthcheck + interval := health.Interval + if interval <= 0 { + interval = 30 * time.Second + } + checkTimeout := health.Timeout + if checkTimeout <= 0 { + checkTimeout = 30 * time.Second + } + retries := health.Retries + if retries <= 0 { + retries = 3 + } + timeout := health.StartPeriod + time.Duration(retries)*(interval+checkTimeout) + if timeout < containerHealthCheckMinWait { + return containerHealthCheckMinWait + } + if timeout > containerHealthCheckMaxWait { + return containerHealthCheckMaxWait + } + return timeout +} + +func preserveContainerVolumeMounts(hostConfig *container.HostConfig, oldMounts []container.MountPoint) { + if hostConfig == nil { + return + } + for _, oldMount := range oldMounts { + if oldMount.Type != mount.TypeVolume || oldMount.Name == "" || oldMount.Destination == "" { + continue + } + preserved := false + for index := range hostConfig.Mounts { + if hostConfig.Mounts[index].Target == oldMount.Destination { + hostConfig.Mounts[index].Type = mount.TypeVolume + hostConfig.Mounts[index].Source = oldMount.Name + preserved = true + } + } + for index, raw := range hostConfig.Binds { + destination, mode := containerBindDestinationAndMode(raw) + if destination != oldMount.Destination { + continue + } + hostConfig.Binds[index] = oldMount.Name + ":" + oldMount.Destination + if mode != "" { + hostConfig.Binds[index] += ":" + mode + } + preserved = true + } + if !preserved { + hostConfig.Mounts = append(hostConfig.Mounts, mount.Mount{ + Type: mount.TypeVolume, + Source: oldMount.Name, + Target: oldMount.Destination, + ReadOnly: !oldMount.RW, + }) + } + } +} + +func containerBindDestinationAndMode(raw string) (string, string) { + parts := strings.SplitN(raw, ":", 3) + switch len(parts) { + case 1: + return parts[0], "" + case 2: + return parts[1], "" + default: + return parts[1], parts[2] + } +} + +func containerSwitchBackupName(containerID string) string { + if len(containerID) > 12 { + containerID = containerID[:12] + } + return "1panel-backup-" + containerID +} + +func restartOriginalContainer(ctx context.Context, cli containerSwitchClient, containerID string) error { + if err := cli.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil { + return fmt.Errorf("restart original container failed: %w", err) + } + return nil +} + +func disconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitchClient, oldContainer container.InspectResponse) ([]containerNetworkAttachment, error) { + primary, extras := buildContainerRecoverNetworkConfig(oldContainer.NetworkSettings, oldContainer.HostConfig) + endpoints := make(map[string]*network.EndpointSettings, len(extras)+1) + if primary != nil { + for name, endpoint := range primary.EndpointsConfig { + if name != "bridge" && endpoint != nil && endpoint.IPAMConfig != nil { + endpoints[name] = endpoint + } + } + } + for name, endpoint := range extras { + if name != "bridge" && endpoint != nil && endpoint.IPAMConfig != nil { + endpoints[name] = endpoint + } + } + names := make([]string, 0, len(endpoints)) + for name := range endpoints { + names = append(names, name) + } + sort.Strings(names) + + disconnected := make([]containerNetworkAttachment, 0, len(names)) + for _, name := range names { + if err := cli.NetworkDisconnect(ctx, name, oldContainer.ID, true); err != nil { + return disconnected, fmt.Errorf("disconnect original container from network %s failed: %w", name, err) + } + disconnected = append(disconnected, containerNetworkAttachment{ + name: name, + endpoint: endpoints[name], + isDynamic: isDynamicContainerNetwork(oldContainer.NetworkSettings, name), + }) + } + return disconnected, nil +} + +func reconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitchClient, containerID string, attachments []containerNetworkAttachment) error { + var reconnectErr error + for _, attachment := range attachments { + err := cli.NetworkConnect(ctx, attachment.name, containerID, attachment.endpoint) + if err != nil && attachment.isDynamic && strings.Contains(err.Error(), unsupportedUserSpecifiedIPAddress) { + attachment.endpoint.IPAMConfig = nil + err = cli.NetworkConnect(ctx, attachment.name, containerID, attachment.endpoint) + } + if err != nil { + reconnectErr = errors.Join(reconnectErr, fmt.Errorf("reconnect original container to network %s failed: %w", attachment.name, err)) + } + } + return reconnectErr +} + +func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, oldContainerID, originalName string, wasRunning bool, newContainer string, disconnectedNetworks []containerNetworkAttachment, logger containerSwitchLogFunc) error { + var rollbackErr error + backupName := containerSwitchBackupName(oldContainerID) + if newContainer != "" { + removeErr := cli.ContainerRemove(ctx, newContainer, container.RemoveOptions{Force: true, RemoveVolumes: true}) + if client.IsErrNotFound(removeErr) { + removeErr = nil + } + logContainerSwitchStep(logger, "ContainerRollbackRemoveReplacement", originalName, removeErr) + if removeErr != nil { + rollbackErr = errors.Join(rollbackErr, fmt.Errorf("remove failed replacement container failed: %w", removeErr)) + } + } + renameErr := cli.ContainerRename(ctx, oldContainerID, originalName) + logContainerSwitchStep(logger, "ContainerRollbackRenameOld", backupName, renameErr) + if renameErr != nil { + rollbackErr = errors.Join(rollbackErr, fmt.Errorf("restore original container name failed: %w", renameErr)) + } + currentName := originalName + if renameErr != nil { + currentName = backupName + } + reconnectErr := reconnectOriginalContainerNetworks(ctx, cli, oldContainerID, disconnectedNetworks) + logContainerSwitchStep(logger, "ContainerRollbackReconnectOld", currentName, reconnectErr) + rollbackErr = errors.Join(rollbackErr, reconnectErr) + if wasRunning { + restartErr := restartOriginalContainer(ctx, cli, oldContainerID) + logContainerSwitchStep(logger, "ContainerRollbackRestartOld", currentName, restartErr) + rollbackErr = errors.Join(rollbackErr, restartErr) + } + return rollbackErr +} + +func createContainerWithOldNetworks(ctx context.Context, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkSettings *container.NetworkSettings, name string) (container.CreateResponse, error) { + networkConf, extraNetworks := buildContainerRecoverNetworkConfig(networkSettings, hostConf) + normalizeContainerEndpointSettings(ctx, client, networkConf, extraNetworks) + var primaryEndpoints map[string]*network.EndpointSettings + if networkConf != nil { + primaryEndpoints = networkConf.EndpointsConfig + } + + created, err := createContainerWithDynamicIPFallback(func() (container.CreateResponse, error) { + return client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name) + }, primaryEndpoints, networkSettings) + if err != nil { + return created, err + } + + extraNames := make([]string, 0, len(extraNetworks)) + for item := range extraNetworks { + extraNames = append(extraNames, item) + } + sort.Strings(extraNames) + for _, item := range extraNames { + err := client.NetworkConnect(ctx, item, created.ID, extraNetworks[item]) + if clearUnsupportedDynamicEndpointIPAM(err, map[string]*network.EndpointSettings{item: extraNetworks[item]}, networkSettings) { + err = client.NetworkConnect(ctx, item, created.ID, extraNetworks[item]) + } + if err != nil { + _ = client.ContainerRemove(ctx, created.ID, container.RemoveOptions{Force: true}) + return created, err + } + } + return created, nil +} + +func createContainerWithDynamicIPFallback( + create func() (container.CreateResponse, error), + endpoints map[string]*network.EndpointSettings, + networkSettings *container.NetworkSettings, +) (container.CreateResponse, error) { + for { + created, err := create() + if err == nil || created.ID != "" || !clearUnsupportedDynamicEndpointIPAM(err, endpoints, networkSettings) { + return created, err + } + } +} diff --git a/agent/cmd/server/docs/x-log.json b/agent/cmd/server/docs/x-log.json index 1b53872a4d56..42b89d672194 100644 --- a/agent/cmd/server/docs/x-log.json +++ b/agent/cmd/server/docs/x-log.json @@ -1841,6 +1841,13 @@ "formatZH": "更新虚拟机存储 [name]", "formatEN": "update VM storage [name]" }, + "/core/enterprise/vms/sync": { + "bodyKeys": [], + "paramKeys": [], + "beforeFunctions": [], + "formatZH": "同步虚拟机", + "formatEN": "sync virtual machines" + }, "/core/enterprise/vms/templates": { "bodyKeys": [ "name", diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index 8e7cbd44a7f7..6951e00035d9 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -562,6 +562,20 @@ ContainerRecreate: 'Container update failed; restoring original container' ContainerCreate: 'Create a new container {{ .name }}' ContainerCreateFailed: 'Container create failed; deleting failed container' ContainerStartCheck: 'Check container started' +ContainerAcquireLock: 'Wait for container {{ .name }} operation lock' +ContainerUpgradeItem: 'Upgrade container {{ .name }}' +ContainerUpgradeSummary: 'Summarize container upgrade results' +ContainerCleanupWarning: 'The update succeeded, but backup container {{ .name }} could not be removed. After confirming the new container is working, remove the backup container manually. Error: {{ .err }}' +ContainerStopOld: 'Stop original container {{ .name }}' +ContainerRenameOld: 'Rename original container {{ .name }} for rollback' +ContainerDisconnectOld: 'Release original container {{ .name }} network addresses' +ContainerCreateReplacement: 'Create replacement container {{ .name }}' +ContainerStartReplacement: 'Start replacement container {{ .name }}' +ContainerWaitReplacement: 'Check replacement container {{ .name }} readiness' +ContainerRollbackRestartOld: 'Restart original container {{ .name }}' +ContainerRollbackRemoveReplacement: 'Remove failed replacement container {{ .name }}' +ContainerRollbackRenameOld: 'Restore original container {{ .name }} name' +ContainerRollbackReconnectOld: 'Restore original container {{ .name }} networks' # task - image ImageBuild: 'Build image' diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index 7453321e42dd..11c4bcd5867b 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -562,6 +562,20 @@ ContainerRecreate: 'Actualización del contenedor fallida, restaurando el conten ContainerCreate: 'Crear nuevo contenedor {{ .name }}' ContainerCreateFailed: 'Falló la creación del contenedor, eliminando el contenedor fallido' ContainerStartCheck: 'Verificar si el contenedor ha iniciado' +ContainerAcquireLock: 'Esperar el bloqueo de operación del contenedor {{ .name }}' +ContainerUpgradeItem: 'Actualizar el contenedor {{ .name }}' +ContainerUpgradeSummary: 'Resumir los resultados de actualización de contenedores' +ContainerCleanupWarning: 'La actualización se completó, pero no se pudo eliminar el contenedor de respaldo {{ .name }}. Después de confirmar que el nuevo contenedor funciona, elimine manualmente el contenedor de respaldo. Error: {{ .err }}' +ContainerStopOld: 'Detener el contenedor original {{ .name }}' +ContainerRenameOld: 'Renombrar el contenedor original {{ .name }} para la reversión' +ContainerDisconnectOld: 'Liberar las direcciones de red del contenedor original {{ .name }}' +ContainerCreateReplacement: 'Crear el contenedor de reemplazo {{ .name }}' +ContainerStartReplacement: 'Iniciar el contenedor de reemplazo {{ .name }}' +ContainerWaitReplacement: 'Verificar que el contenedor de reemplazo {{ .name }} esté listo' +ContainerRollbackRestartOld: 'Reiniciar el contenedor original {{ .name }}' +ContainerRollbackRemoveReplacement: 'Eliminar el contenedor de reemplazo fallido {{ .name }}' +ContainerRollbackRenameOld: 'Restaurar el nombre del contenedor original {{ .name }}' +ContainerRollbackReconnectOld: 'Restaurar las redes del contenedor original {{ .name }}' # tarea - imagen ImageBuild: 'Construcción de imagen' diff --git a/agent/i18n/lang/fa.yaml b/agent/i18n/lang/fa.yaml index 68963849f9f3..2e8e84d0fade 100644 --- a/agent/i18n/lang/fa.yaml +++ b/agent/i18n/lang/fa.yaml @@ -562,6 +562,20 @@ ContainerRecreate: 'به‌روزرسانی کانتینر ناموفق بود؛ ContainerCreate: 'ایجاد کانتینر جدید {{ .name }}' ContainerCreateFailed: 'ایجاد کانتینر ناموفق بود؛ حذف کانتینر ناموفق' ContainerStartCheck: 'بررسی شروع کانتینر' +ContainerAcquireLock: 'انتظار برای قفل عملیات کانتینر {{ .name }}' +ContainerUpgradeItem: 'ارتقای کانتینر {{ .name }}' +ContainerUpgradeSummary: 'خلاصه نتایج ارتقای کانتینرها' +ContainerCleanupWarning: 'به‌روزرسانی موفق بود، اما کانتینر پشتیبان {{ .name }} حذف نشد. پس از اطمینان از عملکرد صحیح کانتینر جدید، کانتینر پشتیبان را به‌صورت دستی حذف کنید. خطا: {{ .err }}' +ContainerStopOld: 'توقف کانتینر اصلی {{ .name }}' +ContainerRenameOld: 'تغییر نام کانتینر اصلی {{ .name }} برای بازگردانی' +ContainerDisconnectOld: 'آزادسازی آدرس‌های شبکه کانتینر اصلی {{ .name }}' +ContainerCreateReplacement: 'ایجاد کانتینر جایگزین {{ .name }}' +ContainerStartReplacement: 'راه‌اندازی کانتینر جایگزین {{ .name }}' +ContainerWaitReplacement: 'بررسی آماده بودن کانتینر جایگزین {{ .name }}' +ContainerRollbackRestartOld: 'راه‌اندازی دوباره کانتینر اصلی {{ .name }}' +ContainerRollbackRemoveReplacement: 'حذف کانتینر جایگزین ناموفق {{ .name }}' +ContainerRollbackRenameOld: 'بازگردانی نام کانتینر اصلی {{ .name }}' +ContainerRollbackReconnectOld: 'بازگردانی شبکه‌های کانتینر اصلی {{ .name }}' # وظیفه - تصویر ImageBuild: 'ساخت تصویر' diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index 63b32928675d..e10e42599805 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -562,6 +562,20 @@ ContainerRecreate: 'コンテナの更新に失敗しました。元のコンテ ContainerCreate: '新しいコンテナ {{ .name }} を作成します' ContainerCreateFailed: 'コンテナの作成に失敗しました。失敗したコンテナを削除してください' ContainerStartCheck: 'コンテナが起動されているかどうかを確認します' +ContainerAcquireLock: 'コンテナ {{ .name }} の操作ロックを待機' +ContainerUpgradeItem: 'コンテナ {{ .name }} をアップグレード' +ContainerUpgradeSummary: 'コンテナのアップグレード結果を集計' +ContainerCleanupWarning: '更新は成功しましたが、バックアップコンテナ {{ .name }} を削除できませんでした。新しいコンテナが正常に動作していることを確認してから、バックアップコンテナを手動で削除してください。エラー: {{ .err }}' +ContainerStopOld: '元のコンテナ {{ .name }} を停止' +ContainerRenameOld: 'ロールバック用に元のコンテナ {{ .name }} の名前を変更' +ContainerDisconnectOld: '元のコンテナ {{ .name }} のネットワークアドレスを解放' +ContainerCreateReplacement: '置換コンテナ {{ .name }} を作成' +ContainerStartReplacement: '置換コンテナ {{ .name }} を起動' +ContainerWaitReplacement: '置換コンテナ {{ .name }} の準備状態を確認' +ContainerRollbackRestartOld: '元のコンテナ {{ .name }} を再起動' +ContainerRollbackRemoveReplacement: '起動に失敗した置換コンテナ {{ .name }} を削除' +ContainerRollbackRenameOld: '元のコンテナ {{ .name }} の名前を復元' +ContainerRollbackReconnectOld: '元のコンテナ {{ .name }} のネットワークを復元' # タスク - イメージ ImageBuild: 'イメージビルド' diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index 6db933f811e5..cd8803abba05 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -562,6 +562,20 @@ ContainerRecreate: '컨테이너 업데이트에 실패했습니다. 이제 원 ContainerCreate: '새로운 컨테이너 {{ .name }}를 만듭니다' ContainerCreateFailed: '컨테이너 생성에 실패했습니다. 실패한 컨테이너를 삭제하세요' ContainerStartCheck: '컨테이너가 시작되었는지 확인' +ContainerAcquireLock: '컨테이너 {{ .name }} 작업 잠금 대기' +ContainerUpgradeItem: '컨테이너 {{ .name }} 업그레이드' +ContainerUpgradeSummary: '컨테이너 업그레이드 결과 요약' +ContainerCleanupWarning: '업데이트는 성공했지만 백업 컨테이너 {{ .name }} 삭제에 실패했습니다. 새 컨테이너가 정상적으로 작동하는지 확인한 후 백업 컨테이너를 수동으로 삭제하십시오. 오류: {{ .err }}' +ContainerStopOld: '기존 컨테이너 {{ .name }} 중지' +ContainerRenameOld: '롤백을 위해 기존 컨테이너 {{ .name }} 이름 변경' +ContainerDisconnectOld: '기존 컨테이너 {{ .name }} 네트워크 주소 해제' +ContainerCreateReplacement: '대체 컨테이너 {{ .name }} 생성' +ContainerStartReplacement: '대체 컨테이너 {{ .name }} 시작' +ContainerWaitReplacement: '대체 컨테이너 {{ .name }} 준비 상태 확인' +ContainerRollbackRestartOld: '기존 컨테이너 {{ .name }} 다시 시작' +ContainerRollbackRemoveReplacement: '시작에 실패한 대체 컨테이너 {{ .name }} 제거' +ContainerRollbackRenameOld: '기존 컨테이너 {{ .name }} 이름 복원' +ContainerRollbackReconnectOld: '기존 컨테이너 {{ .name }} 네트워크 복원' # 작업 - 이미지 ImageBuild: '이미지 빌드' diff --git a/agent/i18n/lang/lo.yaml b/agent/i18n/lang/lo.yaml index 49e5576f8352..52c43523b21f 100644 --- a/agent/i18n/lang/lo.yaml +++ b/agent/i18n/lang/lo.yaml @@ -541,6 +541,20 @@ ContainerRecreate: 'ອັບເດດຄອນເທນເນີລົ້ມເ ContainerCreate: 'ສ້າງຄອນເທນເນີໃໝ່ {{ .name }}' ContainerCreateFailed: 'ສ້າງຄອນເທນເນີລົ້ມເຫຼວ; ກຳລັງລຶบคອນເທນເນີທີ່ລົ້ມເຫຼວ' ContainerStartCheck: 'ກວດສອບການເລີ່ມຕົ້ນຂອງຄອນເທນເນີ' +ContainerAcquireLock: 'ລໍຖ້າລັອກການດຳເນີນງານຄອນເທນເນີ {{ .name }}' +ContainerUpgradeItem: 'ອັບເກຣດຄອນເທນເນີ {{ .name }}' +ContainerUpgradeSummary: 'ສະຫຼຸບຜົນການອັບເກຣດຄອນເທນເນີ' +ContainerCleanupWarning: 'ອັບເດດສຳເລັດ ແຕ່ລຶບຄອນເທນເນີສຳຮອງ {{ .name }} ບໍ່ສຳເລັດ. ຫຼັງຈາກຢືນຢັນວ່າຄອນເທນເນີໃໝ່ເຮັດວຽກປົກກະຕິ ໃຫ້ລຶບຄອນເທນເນີສຳຮອງດ້ວຍຕົນເອງ. ຂໍ້ຜິດພາດ: {{ .err }}' +ContainerStopOld: 'ຢຸດຄອນເທນເນີເດີມ {{ .name }}' +ContainerRenameOld: 'ປ່ຽນຊື່ຄອນເທນເນີເດີມ {{ .name }} ສຳລັບການກູ້ຄືນ' +ContainerDisconnectOld: 'ປ່ອຍທີ່ຢູ່ເຄືອຂ່າຍຂອງຄອນເທນເນີເດີມ {{ .name }}' +ContainerCreateReplacement: 'ສ້າງຄອນເທນເນີທົດແທນ {{ .name }}' +ContainerStartReplacement: 'ເລີ່ມຄອນເທນເນີທົດແທນ {{ .name }}' +ContainerWaitReplacement: 'ກວດສອບຄອນເທນເນີທົດແທນ {{ .name }} ວ່າພ້ອມແລ້ວ' +ContainerRollbackRestartOld: 'ເລີ່ມຄອນເທນເນີເດີມ {{ .name }} ຄືນໃໝ່' +ContainerRollbackRemoveReplacement: 'ລຶບຄອນເທນເນີທົດແທນ {{ .name }} ທີ່ລົ້ມເຫຼວ' +ContainerRollbackRenameOld: 'ກູ້ຄືນຊື່ຄອນເທນເນີເດີມ {{ .name }}' +ContainerRollbackReconnectOld: 'ກູ້ຄືນເຄືອຂ່າຍຂອງຄອນເທນເນີເດີມ {{ .name }}' # task - image ImageBuild: 'ສ້າງຮູບພາບ' diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index fc60a8099191..9cbdd1d0a8a1 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -562,6 +562,20 @@ ContainerRecreate: 'Kemas kini kontena gagal, kini mula memulihkan bekas asal' ContainerCreate: 'Buat bekas baharu {{ .name }}' ContainerCreateFailed: 'Penciptaan bekas gagal, padamkan bekas yang gagal' ContainerStartCheck: 'Periksa sama ada bekas telah dimulakan' +ContainerAcquireLock: 'Tunggu kunci operasi bekas {{ .name }}' +ContainerUpgradeItem: 'Naik taraf bekas {{ .name }}' +ContainerUpgradeSummary: 'Ringkaskan hasil naik taraf bekas' +ContainerCleanupWarning: 'Kemas kini berjaya tetapi bekas sandaran {{ .name }} tidak dapat dialih keluar. Selepas memastikan bekas baharu berfungsi, alih keluar bekas sandaran secara manual. Ralat: {{ .err }}' +ContainerStopOld: 'Hentikan bekas asal {{ .name }}' +ContainerRenameOld: 'Namakan semula bekas asal {{ .name }} untuk pemulihan' +ContainerDisconnectOld: 'Lepaskan alamat rangkaian bekas asal {{ .name }}' +ContainerCreateReplacement: 'Cipta bekas ganti {{ .name }}' +ContainerStartReplacement: 'Mulakan bekas ganti {{ .name }}' +ContainerWaitReplacement: 'Periksa kesediaan bekas ganti {{ .name }}' +ContainerRollbackRestartOld: 'Mulakan semula bekas asal {{ .name }}' +ContainerRollbackRemoveReplacement: 'Alih keluar bekas ganti yang gagal {{ .name }}' +ContainerRollbackRenameOld: 'Pulihkan nama bekas asal {{ .name }}' +ContainerRollbackReconnectOld: 'Pulihkan rangkaian bekas asal {{ .name }}' # tugas - imej ImageBuild: 'Membina Imej' diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index a1090a368cb6..8d30bbc3d573 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -562,6 +562,20 @@ ContainerRecreate: 'A atualização do contêiner falhou, agora iniciando a rest ContainerCreate: 'Criar um novo contêiner {{ .name }}' ContainerCreateFailed: 'Falha na criação do contêiner, exclua o contêiner com falha' ContainerStartCheck: 'Verifique se o contêiner foi iniciado' +ContainerAcquireLock: 'Aguardar o bloqueio de operação do contêiner {{ .name }}' +ContainerUpgradeItem: 'Atualizar o contêiner {{ .name }}' +ContainerUpgradeSummary: 'Resumir os resultados da atualização de contêineres' +ContainerCleanupWarning: 'A atualização foi concluída, mas não foi possível remover o contêiner de backup {{ .name }}. Depois de confirmar que o novo contêiner está funcionando, remova manualmente o contêiner de backup. Erro: {{ .err }}' +ContainerStopOld: 'Parar o contêiner original {{ .name }}' +ContainerRenameOld: 'Renomear o contêiner original {{ .name }} para reversão' +ContainerDisconnectOld: 'Liberar os endereços de rede do contêiner original {{ .name }}' +ContainerCreateReplacement: 'Criar o contêiner substituto {{ .name }}' +ContainerStartReplacement: 'Iniciar o contêiner substituto {{ .name }}' +ContainerWaitReplacement: 'Verificar se o contêiner substituto {{ .name }} está pronto' +ContainerRollbackRestartOld: 'Reiniciar o contêiner original {{ .name }}' +ContainerRollbackRemoveReplacement: 'Remover o contêiner substituto com falha {{ .name }}' +ContainerRollbackRenameOld: 'Restaurar o nome do contêiner original {{ .name }}' +ContainerRollbackReconnectOld: 'Restaurar as redes do contêiner original {{ .name }}' # tarefa - imagem ImageBuild: 'Construção de imagem' diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index 8610b97d6dc4..6e03ddc9d807 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -562,6 +562,20 @@ ContainerRecreate: 'Обновление контейнера не удалос ContainerCreate: 'Создать новый контейнер {{ .name }}' ContainerCreateFailed: 'Создание контейнера не удалось, удалите неудавшийся контейнер' ContainerStartCheck: 'Проверить, запущен ли контейнер' +ContainerAcquireLock: 'Ожидание блокировки операций контейнера {{ .name }}' +ContainerUpgradeItem: 'Обновление контейнера {{ .name }}' +ContainerUpgradeSummary: 'Сводка результатов обновления контейнеров' +ContainerCleanupWarning: 'Обновление завершено, но удалить резервный контейнер {{ .name }} не удалось. Убедитесь, что новый контейнер работает, затем удалите резервный контейнер вручную. Ошибка: {{ .err }}' +ContainerStopOld: 'Остановка исходного контейнера {{ .name }}' +ContainerRenameOld: 'Переименование исходного контейнера {{ .name }} для отката' +ContainerDisconnectOld: 'Освобождение сетевых адресов исходного контейнера {{ .name }}' +ContainerCreateReplacement: 'Создание нового контейнера {{ .name }}' +ContainerStartReplacement: 'Запуск нового контейнера {{ .name }}' +ContainerWaitReplacement: 'Проверка готовности нового контейнера {{ .name }}' +ContainerRollbackRestartOld: 'Повторный запуск исходного контейнера {{ .name }}' +ContainerRollbackRemoveReplacement: 'Удаление неудачно созданного контейнера {{ .name }}' +ContainerRollbackRenameOld: 'Восстановление имени исходного контейнера {{ .name }}' +ContainerRollbackReconnectOld: 'Восстановление сетей исходного контейнера {{ .name }}' # задача - образ ImageBuild: 'Создание изображения' diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index 255b5e98d2a6..40ad415c2a87 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -562,6 +562,20 @@ ContainerRecreate: 'Konteyner güncellemesi başarısız, şimdi orijinal kontey ContainerCreate: 'Yeni konteyner oluştur {{ .name }}' ContainerCreateFailed: 'Konteyner oluşturma başarısız, başarısız konteyneri sil' ContainerStartCheck: 'Konteynerin başlatılıp başlatılmadığını kontrol et' +ContainerAcquireLock: '{{ .name }} konteyneri işlem kilidini bekle' +ContainerUpgradeItem: '{{ .name }} konteynerini yükselt' +ContainerUpgradeSummary: 'Konteyner yükseltme sonuçlarını özetle' +ContainerCleanupWarning: 'Güncelleme tamamlandı ancak {{ .name }} yedek konteyneri kaldırılamadı. Yeni konteynerin düzgün çalıştığını doğruladıktan sonra yedek konteyneri elle kaldırın. Hata: {{ .err }}' +ContainerStopOld: 'Eski {{ .name }} konteynerini durdur' +ContainerRenameOld: 'Geri alma için eski {{ .name }} konteynerini yeniden adlandır' +ContainerDisconnectOld: 'Eski {{ .name }} konteynerinin ağ adreslerini serbest bırak' +ContainerCreateReplacement: 'Yeni {{ .name }} konteynerini oluştur' +ContainerStartReplacement: 'Yeni {{ .name }} konteynerini başlat' +ContainerWaitReplacement: 'Yeni {{ .name }} konteynerinin hazır olduğunu kontrol et' +ContainerRollbackRestartOld: 'Eski {{ .name }} konteynerini yeniden başlat' +ContainerRollbackRemoveReplacement: 'Başlatılamayan yeni {{ .name }} konteynerini kaldır' +ContainerRollbackRenameOld: 'Eski {{ .name }} konteynerinin adını geri yükle' +ContainerRollbackReconnectOld: 'Eski {{ .name }} konteynerinin ağlarını geri yükle' # görev - imaj ImageBuild: 'İmaj Oluşturma' diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index 16a1eb720956..efdeddddcdab 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -562,6 +562,20 @@ ContainerRecreate: '容器更新失敗,現在開始復原原容器' ContainerCreate: '建立新容器{{ .name }}' ContainerCreateFailed: '容器建立失敗,刪除失敗容器' ContainerStartCheck: '檢查容器是否已啟動' +ContainerAcquireLock: '等待容器 {{ .name }} 操作鎖' +ContainerUpgradeItem: '升級容器 {{ .name }}' +ContainerUpgradeSummary: '彙總容器升級結果' +ContainerCleanupWarning: '容器更新成功,但備份容器 {{ .name }} 刪除失敗。請確認新容器運作正常後手動刪除該備份容器。錯誤:{{ .err }}' +ContainerStopOld: '停止原容器 {{ .name }}' +ContainerRenameOld: '重新命名原容器 {{ .name }} 以便回復' +ContainerDisconnectOld: '釋放原容器 {{ .name }} 的網路位址' +ContainerCreateReplacement: '建立替換容器 {{ .name }}' +ContainerStartReplacement: '啟動替換容器 {{ .name }}' +ContainerWaitReplacement: '檢查替換容器 {{ .name }} 是否就緒' +ContainerRollbackRestartOld: '重新啟動原容器 {{ .name }}' +ContainerRollbackRemoveReplacement: '刪除啟動失敗的替換容器 {{ .name }}' +ContainerRollbackRenameOld: '恢復原容器 {{ .name }} 的名稱' +ContainerRollbackReconnectOld: '恢復原容器 {{ .name }} 的網路' # 任務 - 映像 ImageBuild: '映像建置' diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index df83fca2d228..d3cdcdb1be00 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -562,6 +562,20 @@ ContainerRecreate: "容器更新失败,现在开始恢复原容器" ContainerCreate: "创建新容器 {{ .name }}" ContainerCreateFailed: "容器创建失败,删除失败容器" ContainerStartCheck: "检查容器是否已启动" +ContainerAcquireLock: "等待容器 {{ .name }} 操作锁" +ContainerUpgradeItem: "升级容器 {{ .name }}" +ContainerUpgradeSummary: "汇总容器升级结果" +ContainerCleanupWarning: "容器更新成功,但备份容器 {{ .name }} 删除失败。请确认新容器运行正常后手动删除该备份容器。错误:{{ .err }}" +ContainerStopOld: "停止原容器 {{ .name }}" +ContainerRenameOld: "重命名原容器 {{ .name }} 以便回滚" +ContainerDisconnectOld: "释放原容器 {{ .name }} 的网络地址" +ContainerCreateReplacement: "创建替换容器 {{ .name }}" +ContainerStartReplacement: "启动替换容器 {{ .name }}" +ContainerWaitReplacement: "检查替换容器 {{ .name }} 是否就绪" +ContainerRollbackRestartOld: "重新启动原容器 {{ .name }}" +ContainerRollbackRemoveReplacement: "删除启动失败的替换容器 {{ .name }}" +ContainerRollbackRenameOld: "恢复原容器 {{ .name }} 的名称" +ContainerRollbackReconnectOld: "恢复原容器 {{ .name }} 的网络" # 任务 - 镜像 ImageBuild: "镜像构建" diff --git a/core/cmd/server/docs/docs.go b/core/cmd/server/docs/docs.go index d613b146feca..b75900c2088b 100644 --- a/core/cmd/server/docs/docs.go +++ b/core/cmd/server/docs/docs.go @@ -241,6 +241,47 @@ const docTemplate = `{ ] } }, + "/ai/accounts/models/discover": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AgentAccountModelDiscoverReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.AgentAccountModel" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Discover custom provider models", + "tags": [ + "AI" + ] + } + }, "/ai/accounts/models/update": { "post": { "consumes": [ @@ -17954,6 +17995,89 @@ const docTemplate = `{ } } }, + "/hosts/diagnostics/goroutines": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RuntimeGoroutineSnapshot" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Load grouped goroutine snapshot", + "tags": [ + "RuntimeDiagnostics" + ] + } + }, + "/hosts/diagnostics/profiles": { + "post": { + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RuntimeProfileCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Capture runtime profile", + "tags": [ + "RuntimeDiagnostics" + ] + } + }, + "/hosts/diagnostics/summary": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RuntimeDiagnosticsSummary" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Load runtime diagnostics summary", + "tags": [ + "RuntimeDiagnostics" + ] + } + }, "/hosts/disks": { "get": { "description": "Get information about all disks including partitioned and unpartitioned disks", @@ -28628,6 +28752,9 @@ const docTemplate = `{ "apiType": { "type": "string" }, + "authMode": { + "type": "string" + }, "baseURL": { "type": "string" }, @@ -28648,6 +28775,9 @@ const docTemplate = `{ }, "rememberApiKey": { "type": "boolean" + }, + "verifyModel": { + "type": "string" } }, "required": [ @@ -28677,6 +28807,9 @@ const docTemplate = `{ "apiType": { "type": "string" }, + "authMode": { + "type": "string" + }, "baseUrl": { "type": "string" }, @@ -28712,33 +28845,21 @@ const docTemplate = `{ }, "verified": { "type": "boolean" + }, + "verifyModel": { + "type": "string" } }, "type": "object" }, "dto.AgentAccountModel": { "properties": { - "contextWindow": { - "type": "integer" - }, "id": { "type": "string" }, - "input": { - "items": { - "type": "string" - }, - "type": "array" - }, - "maxTokens": { - "type": "integer" - }, "name": { "type": "string" }, - "reasoning": { - "type": "boolean" - }, "recordId": { "type": "integer" } @@ -28775,6 +28896,29 @@ const docTemplate = `{ ], "type": "object" }, + "dto.AgentAccountModelDiscoverReq": { + "properties": { + "apiKey": { + "type": "string" + }, + "apiType": { + "type": "string" + }, + "baseURL": { + "type": "string" + }, + "provider": { + "type": "string" + } + }, + "required": [ + "apiKey", + "apiType", + "baseURL", + "provider" + ], + "type": "object" + }, "dto.AgentAccountModelReq": { "properties": { "accountId": { @@ -28841,6 +28985,9 @@ const docTemplate = `{ "apiType": { "type": "string" }, + "authMode": { + "type": "string" + }, "baseURL": { "type": "string" }, @@ -28858,6 +29005,9 @@ const docTemplate = `{ }, "syncAgents": { "type": "boolean" + }, + "verifyModel": { + "type": "string" } }, "required": [ @@ -28873,15 +29023,25 @@ const docTemplate = `{ "apiKey": { "type": "string" }, + "apiType": { + "type": "string" + }, + "authMode": { + "type": "string" + }, "baseURL": { "type": "string" }, + "model": { + "type": "string" + }, "provider": { "type": "string" } }, "required": [ "apiKey", + "apiType", "provider" ], "type": "object" @@ -29885,9 +30045,6 @@ const docTemplate = `{ "containerName": { "type": "string" }, - "contextWindow": { - "type": "integer" - }, "createdAt": { "type": "string" }, @@ -29900,9 +30057,6 @@ const docTemplate = `{ "id": { "type": "integer" }, - "maxTokens": { - "type": "integer" - }, "message": { "type": "string" }, @@ -32623,12 +32777,39 @@ const docTemplate = `{ }, "dto.ContainerNetwork": { "properties": { + "aliases": { + "items": { + "type": "string" + }, + "type": "array" + }, + "driverOpts": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "gwPriority": { + "type": "integer" + }, "ipv4": { "type": "string" }, "ipv6": { "type": "string" }, + "linkLocalIPs": { + "items": { + "type": "string" + }, + "type": "array" + }, + "links": { + "items": { + "type": "string" + }, + "type": "array" + }, "macAddr": { "type": "string" }, @@ -37171,11 +37352,43 @@ const docTemplate = `{ }, "type": "object" }, + "dto.ProviderAPIInfo": { + "properties": { + "apiType": { + "type": "string" + }, + "authModes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "baseUrl": { + "type": "string" + }, + "defaultAuthMode": { + "type": "string" + }, + "editableBaseUrl": { + "type": "boolean" + } + }, + "type": "object" + }, "dto.ProviderInfo": { "properties": { + "apiTypes": { + "items": { + "$ref": "#/definitions/dto.ProviderAPIInfo" + }, + "type": "array" + }, "baseUrl": { "type": "string" }, + "defaultApiType": { + "type": "string" + }, "displayName": { "type": "string" }, @@ -37193,26 +37406,11 @@ const docTemplate = `{ }, "dto.ProviderModelInfo": { "properties": { - "contextWindow": { - "type": "integer" - }, "id": { "type": "string" }, - "input": { - "items": { - "type": "string" - }, - "type": "array" - }, - "maxTokens": { - "type": "integer" - }, "name": { "type": "string" - }, - "reasoning": { - "type": "boolean" } }, "type": "object" @@ -37628,6 +37826,89 @@ const docTemplate = `{ }, "type": "object" }, + "dto.RuntimeDiagnosticsSummary": { + "properties": { + "goroutines": { + "type": "integer" + }, + "heapAlloc": { + "type": "integer" + }, + "heapObjects": { + "type": "integer" + }, + "rss": { + "type": "integer" + } + }, + "type": "object" + }, + "dto.RuntimeGoroutineGroup": { + "properties": { + "count": { + "type": "integer" + }, + "stack": { + "items": { + "type": "string" + }, + "type": "array" + }, + "state": { + "type": "string" + }, + "top": { + "type": "string" + } + }, + "type": "object" + }, + "dto.RuntimeGoroutineSnapshot": { + "properties": { + "capturedAt": { + "type": "string" + }, + "goroutines": { + "items": { + "$ref": "#/definitions/dto.RuntimeGoroutineGroup" + }, + "type": "array" + }, + "groupCount": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "truncated": { + "type": "boolean" + } + }, + "type": "object" + }, + "dto.RuntimeProfileCreate": { + "properties": { + "duration": { + "maximum": 30, + "minimum": 5, + "type": "integer" + }, + "type": { + "enum": [ + "cpu", + "heap", + "goroutine", + "mutex", + "block" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, "dto.SSHConfUpdate": { "properties": { "key": { diff --git a/core/cmd/server/docs/swagger.json b/core/cmd/server/docs/swagger.json index 40b62bc11aae..11110e45dbbf 100644 --- a/core/cmd/server/docs/swagger.json +++ b/core/cmd/server/docs/swagger.json @@ -237,6 +237,47 @@ ] } }, + "/ai/accounts/models/discover": { + "post": { + "consumes": [ + "application/json" + ], + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.AgentAccountModelDiscoverReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "items": { + "$ref": "#/definitions/dto.AgentAccountModel" + }, + "type": "array" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Discover custom provider models", + "tags": [ + "AI" + ] + } + }, "/ai/accounts/models/update": { "post": { "consumes": [ @@ -17950,6 +17991,89 @@ } } }, + "/hosts/diagnostics/goroutines": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RuntimeGoroutineSnapshot" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Load grouped goroutine snapshot", + "tags": [ + "RuntimeDiagnostics" + ] + } + }, + "/hosts/diagnostics/profiles": { + "post": { + "parameters": [ + { + "description": "request", + "in": "body", + "name": "request", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RuntimeProfileCreate" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "file" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Capture runtime profile", + "tags": [ + "RuntimeDiagnostics" + ] + } + }, + "/hosts/diagnostics/summary": { + "get": { + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.RuntimeDiagnosticsSummary" + } + } + }, + "security": [ + { + "ApiKeyAuth": [] + }, + { + "Timestamp": [] + } + ], + "summary": "Load runtime diagnostics summary", + "tags": [ + "RuntimeDiagnostics" + ] + } + }, "/hosts/disks": { "get": { "description": "Get information about all disks including partitioned and unpartitioned disks", @@ -28624,6 +28748,9 @@ "apiType": { "type": "string" }, + "authMode": { + "type": "string" + }, "baseURL": { "type": "string" }, @@ -28644,6 +28771,9 @@ }, "rememberApiKey": { "type": "boolean" + }, + "verifyModel": { + "type": "string" } }, "required": [ @@ -28673,6 +28803,9 @@ "apiType": { "type": "string" }, + "authMode": { + "type": "string" + }, "baseUrl": { "type": "string" }, @@ -28708,33 +28841,21 @@ }, "verified": { "type": "boolean" + }, + "verifyModel": { + "type": "string" } }, "type": "object" }, "dto.AgentAccountModel": { "properties": { - "contextWindow": { - "type": "integer" - }, "id": { "type": "string" }, - "input": { - "items": { - "type": "string" - }, - "type": "array" - }, - "maxTokens": { - "type": "integer" - }, "name": { "type": "string" }, - "reasoning": { - "type": "boolean" - }, "recordId": { "type": "integer" } @@ -28771,6 +28892,29 @@ ], "type": "object" }, + "dto.AgentAccountModelDiscoverReq": { + "properties": { + "apiKey": { + "type": "string" + }, + "apiType": { + "type": "string" + }, + "baseURL": { + "type": "string" + }, + "provider": { + "type": "string" + } + }, + "required": [ + "apiKey", + "apiType", + "baseURL", + "provider" + ], + "type": "object" + }, "dto.AgentAccountModelReq": { "properties": { "accountId": { @@ -28837,6 +28981,9 @@ "apiType": { "type": "string" }, + "authMode": { + "type": "string" + }, "baseURL": { "type": "string" }, @@ -28854,6 +29001,9 @@ }, "syncAgents": { "type": "boolean" + }, + "verifyModel": { + "type": "string" } }, "required": [ @@ -28869,15 +29019,25 @@ "apiKey": { "type": "string" }, + "apiType": { + "type": "string" + }, + "authMode": { + "type": "string" + }, "baseURL": { "type": "string" }, + "model": { + "type": "string" + }, "provider": { "type": "string" } }, "required": [ "apiKey", + "apiType", "provider" ], "type": "object" @@ -29881,9 +30041,6 @@ "containerName": { "type": "string" }, - "contextWindow": { - "type": "integer" - }, "createdAt": { "type": "string" }, @@ -29896,9 +30053,6 @@ "id": { "type": "integer" }, - "maxTokens": { - "type": "integer" - }, "message": { "type": "string" }, @@ -32619,12 +32773,39 @@ }, "dto.ContainerNetwork": { "properties": { + "aliases": { + "items": { + "type": "string" + }, + "type": "array" + }, + "driverOpts": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "gwPriority": { + "type": "integer" + }, "ipv4": { "type": "string" }, "ipv6": { "type": "string" }, + "linkLocalIPs": { + "items": { + "type": "string" + }, + "type": "array" + }, + "links": { + "items": { + "type": "string" + }, + "type": "array" + }, "macAddr": { "type": "string" }, @@ -37167,11 +37348,43 @@ }, "type": "object" }, + "dto.ProviderAPIInfo": { + "properties": { + "apiType": { + "type": "string" + }, + "authModes": { + "items": { + "type": "string" + }, + "type": "array" + }, + "baseUrl": { + "type": "string" + }, + "defaultAuthMode": { + "type": "string" + }, + "editableBaseUrl": { + "type": "boolean" + } + }, + "type": "object" + }, "dto.ProviderInfo": { "properties": { + "apiTypes": { + "items": { + "$ref": "#/definitions/dto.ProviderAPIInfo" + }, + "type": "array" + }, "baseUrl": { "type": "string" }, + "defaultApiType": { + "type": "string" + }, "displayName": { "type": "string" }, @@ -37189,26 +37402,11 @@ }, "dto.ProviderModelInfo": { "properties": { - "contextWindow": { - "type": "integer" - }, "id": { "type": "string" }, - "input": { - "items": { - "type": "string" - }, - "type": "array" - }, - "maxTokens": { - "type": "integer" - }, "name": { "type": "string" - }, - "reasoning": { - "type": "boolean" } }, "type": "object" @@ -37624,6 +37822,89 @@ }, "type": "object" }, + "dto.RuntimeDiagnosticsSummary": { + "properties": { + "goroutines": { + "type": "integer" + }, + "heapAlloc": { + "type": "integer" + }, + "heapObjects": { + "type": "integer" + }, + "rss": { + "type": "integer" + } + }, + "type": "object" + }, + "dto.RuntimeGoroutineGroup": { + "properties": { + "count": { + "type": "integer" + }, + "stack": { + "items": { + "type": "string" + }, + "type": "array" + }, + "state": { + "type": "string" + }, + "top": { + "type": "string" + } + }, + "type": "object" + }, + "dto.RuntimeGoroutineSnapshot": { + "properties": { + "capturedAt": { + "type": "string" + }, + "goroutines": { + "items": { + "$ref": "#/definitions/dto.RuntimeGoroutineGroup" + }, + "type": "array" + }, + "groupCount": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "truncated": { + "type": "boolean" + } + }, + "type": "object" + }, + "dto.RuntimeProfileCreate": { + "properties": { + "duration": { + "maximum": 30, + "minimum": 5, + "type": "integer" + }, + "type": { + "enum": [ + "cpu", + "heap", + "goroutine", + "mutex", + "block" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, "dto.SSHConfUpdate": { "properties": { "key": { diff --git a/core/cmd/server/docs/x-log.json b/core/cmd/server/docs/x-log.json index 1b53872a4d56..42b89d672194 100644 --- a/core/cmd/server/docs/x-log.json +++ b/core/cmd/server/docs/x-log.json @@ -1841,6 +1841,13 @@ "formatZH": "更新虚拟机存储 [name]", "formatEN": "update VM storage [name]" }, + "/core/enterprise/vms/sync": { + "bodyKeys": [], + "paramKeys": [], + "beforeFunctions": [], + "formatZH": "同步虚拟机", + "formatEN": "sync virtual machines" + }, "/core/enterprise/vms/templates": { "bodyKeys": [ "name", diff --git a/frontend/src/api/interface/container.ts b/frontend/src/api/interface/container.ts index 03971ff8d49c..7b66ffb2f331 100644 --- a/frontend/src/api/interface/container.ts +++ b/frontend/src/api/interface/container.ts @@ -156,6 +156,11 @@ export namespace Container { ipv4: string; ipv6: string; macAddr: string; + links?: Array; + aliases?: Array; + driverOpts?: Record; + gwPriority?: number; + linkLocalIPs?: Array; } export interface ContainerItemStats { sizeRw: number; diff --git a/frontend/src/lang/fu.ts b/frontend/src/lang/fu.ts index 96cd78865205..424dd0e0e305 100644 --- a/frontend/src/lang/fu.ts +++ b/frontend/src/lang/fu.ts @@ -70,7 +70,7 @@ const fuLocales: Record = { finish: 'ສຳເລັດ', }, }, - }, + }, ms: { fu: { table: { diff --git a/frontend/src/views/container/container/operate/network.vue b/frontend/src/views/container/container/operate/network.vue index fc2b0f398c1f..3c170a7a293c 100644 --- a/frontend/src/views/container/container/operate/network.vue +++ b/frontend/src/views/container/container/operate/network.vue @@ -54,7 +54,7 @@ import { Container } from '@/api/interface/container'; import { listNetwork } from '@/api/modules/container'; import { ref, onMounted } from 'vue'; -const tmpNetworks = ref([]); +const tmpNetworks = ref>([]); const networkOptions = ref(); const props = defineProps({ networks: { type: Array, default: [] }, @@ -77,24 +77,33 @@ const loadNetworkOptions = async () => { networkOptions.value = res.data; }; const handleNetworksAdd = () => { - let item = { + const item: Container.ContainerNetwork = { network: 'bridge', ipv4: '', ipv6: '', macAddr: '', + links: [], + aliases: [], + driverOpts: {}, + gwPriority: 0, + linkLocalIPs: [], }; tmpNetworks.value.push(item); }; const handleNetworksDelete = (index: number) => { tmpNetworks.value.splice(index, 1); }; -const changeNetwork = (row: any) => { - if (row.network === 'none' || row.network === 'host') { - row.ipv4 = ''; - row.ipv6 = ''; - } +const changeNetwork = (row: Container.ContainerNetwork) => { + row.ipv4 = ''; + row.ipv6 = ''; + row.macAddr = ''; + row.links = []; + row.aliases = []; + row.driverOpts = {}; + row.gwPriority = 0; + row.linkLocalIPs = []; }; -const isIpDisabled = (row: any) => { +const isIpDisabled = (row: Container.ContainerNetwork) => { return row.network === 'none' || row.network === 'host' || row.network === 'bridge'; };