diff --git a/agent/app/service/backup_container.go b/agent/app/service/backup_container.go index 70e6a2012..359b4b171 100644 --- a/agent/app/service/backup_container.go +++ b/agent/app/service/backup_container.go @@ -582,6 +582,10 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T if config.Image == "" { return fmt.Errorf("container image not found in backup file") } + networkConf, extraNetworks := buildContainerRecoverNetworkConfig(recoverCtx.inspectInfo.NetworkSettings, hostConfig) + if err := normalizeContainerEndpointSettings(ctx, recoverCtx.client, networkConf, extraNetworks); err != nil { + return err + } if !checkImageExist(recoverCtx.client, config.Image) { if err := pullImages(taskItem, recoverCtx.client, config.Image); err != nil { return err @@ -596,7 +600,7 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T return err } - createRes, err := createContainerWithOldNetworks(ctx, recoverCtx.client, config, hostConfig, recoverCtx.inspectInfo.NetworkSettings, recoverCtx.targetName) + createRes, err := createContainerWithNetworks(ctx, recoverCtx.client, config, hostConfig, networkConf, extraNetworks, recoverCtx.targetName) if err != nil { return err } @@ -604,7 +608,7 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T return nil } -func normalizeContainerEndpointSettings(ctx context.Context, 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) error { if cli.NewVersionError(ctx, "1.44", "specify mac-address per network") != nil { removeEndpointMacAddresses(primary, extras) } @@ -619,11 +623,14 @@ func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client, } info, err := cli.NetworkInspect(ctx, netName, network.InspectOptions{}) if err != nil { - continue + return fmt.Errorf("inspect network %s failed: %w", netName, err) + } + if err := validateContainerEndpointStaticIP(netName, info, endpoint); err != nil { + return err } - removeUnsupportedEndpointStaticIP(netName, info, endpoint) } } + return nil } func removeEndpointMacAddresses(primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) { @@ -641,24 +648,28 @@ func removeEndpointMacAddresses(primary *network.NetworkingConfig, extras map[st } } -func removeUnsupportedEndpointStaticIP(netName string, info network.Inspect, endpoint *network.EndpointSettings) { +func validateContainerEndpointStaticIP(netName string, info network.Inspect, endpoint *network.EndpointSettings) error { if endpoint == nil || endpoint.IPAMConfig == nil { - return + return nil } - if isDefaultBridgeNetwork(netName, info) { - endpoint.IPAMConfig = nil - return + ipam := endpoint.IPAMConfig + if err := ipam.Validate(); err != nil { + return fmt.Errorf("invalid IP configuration for network %s: %w", netName, err) + } + if ipam.IPv4Address == "" && ipam.IPv6Address == "" { + return nil + } + if netName == "host" || netName == "none" || isDefaultBridgeNetwork(netName, info) { + return fmt.Errorf("network %s does not support static IP configuration", netName) } - if endpoint.IPAMConfig.IPv4Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv4Address, false) { - endpoint.IPAMConfig.IPv4Address = "" + if ipam.IPv4Address != "" && !networkSupportsStaticIP(info, ipam.IPv4Address, false) { + return fmt.Errorf("static IPv4 address %s is not in a configured subnet of network %s", ipam.IPv4Address, netName) } - if endpoint.IPAMConfig.IPv6Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv6Address, true) { - endpoint.IPAMConfig.IPv6Address = "" - } - if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" && len(endpoint.IPAMConfig.LinkLocalIPs) == 0 { - endpoint.IPAMConfig = nil + if ipam.IPv6Address != "" && !networkSupportsStaticIP(info, ipam.IPv6Address, true) { + return fmt.Errorf("static IPv6 address %s is not in a configured subnet of network %s", ipam.IPv6Address, netName) } + return nil } func isDefaultBridgeNetwork(netName string, info network.Inspect) bool { @@ -673,6 +684,7 @@ func networkSupportsStaticIP(info network.Inspect, ip string, isIPv6 bool) bool if err != nil { return false } + addr = addr.Unmap() if addr.Is6() != isIPv6 { return false } @@ -826,39 +838,6 @@ 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 1a989f28a..d65ef4f1f 100644 --- a/agent/app/service/container.go +++ b/agent/app/service/container.go @@ -536,7 +536,9 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo if err != nil { return err } - normalizeContainerEndpointSettings(ctx, client, networkConf, nil) + if err := normalizeContainerEndpointSettings(ctx, client, networkConf, nil); err != nil { + return err + } con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name) if err != nil { taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed")) @@ -646,14 +648,9 @@ func loadContainerNetworkInfo(name string, endpoint *network.EndpointSettings) d 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 - } + if name != "bridge" && endpoint.IPAMConfig != nil { + item.Ipv4 = endpoint.IPAMConfig.IPv4Address + item.Ipv6 = endpoint.IPAMConfig.IPv6Address } return item } diff --git a/agent/app/service/container_update.go b/agent/app/service/container_update.go index 2e800cc88..8e6e4e4a4 100644 --- a/agent/app/service/container_update.go +++ b/agent/app/service/container_update.go @@ -67,12 +67,12 @@ func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error { if err != nil { return err } - normalizeContainerEndpointSettings(ctx, client, networkConf, nil) + if err := normalizeContainerEndpointSettings(ctx, client, networkConf, nil); err != nil { + return err + } 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) + return client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name) }, config.Tty, t) if err != nil { return fmt.Errorf("update container failed, err: %v", err) @@ -138,8 +138,14 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error { config.Image = req.Image hostConf := cloneContainerHostConfig(oldContainer.HostConfig) preserveContainerVolumeMounts(hostConf, oldContainer.Mounts) + networkConf, extraNetworks := buildContainerRecoverNetworkConfig(oldContainer.NetworkSettings, hostConf) + if err := normalizeContainerEndpointSettings(ctx, client, networkConf, extraNetworks); err != nil { + upgradeErr := fmt.Errorf("prepare networks for container %s failed: %w", item, err) + upgradeErrors = append(upgradeErrors, upgradeErr) + return upgradeErr + } cleanupErr, err := switchContainer(ctx, client, item, oldContainer, func() (container.CreateResponse, error) { - return createContainerWithOldNetworks(ctx, client, config, hostConf, oldContainer.NetworkSettings, item) + return createContainerWithNetworks(ctx, client, config, hostConf, networkConf, extraNetworks, item) }, config.Tty, t) if err != nil { upgradeErr := fmt.Errorf("upgrade container %s failed: %w", item, err) @@ -242,9 +248,8 @@ func (l *containerOperationMutex) lock(names ...string) func() { } type containerNetworkAttachment struct { - name string - endpoint *network.EndpointSettings - isDynamic bool + name string + endpoint *network.EndpointSettings } type containerSwitchLogger interface { @@ -604,9 +609,8 @@ func disconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitc 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), + name: name, + endpoint: endpoints[name], }) } return disconnected, nil @@ -616,10 +620,6 @@ func reconnectOriginalContainerNetworks(ctx context.Context, cli containerSwitch 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)) } @@ -652,7 +652,7 @@ func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, ol reconnectErr := reconnectOriginalContainerNetworks(ctx, cli, oldContainerID, disconnectedNetworks) logContainerSwitchStep(logger, "ContainerRollbackReconnectOld", currentName, reconnectErr) rollbackErr = errors.Join(rollbackErr, reconnectErr) - if wasRunning { + if wasRunning && reconnectErr == nil { restartErr := restartOriginalContainer(ctx, cli, oldContainerID) logContainerSwitchStep(logger, "ContainerRollbackRestartOld", currentName, restartErr) rollbackErr = errors.Join(rollbackErr, restartErr) @@ -660,17 +660,8 @@ func restoreOriginalContainer(ctx context.Context, cli containerSwitchClient, ol 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) +func createContainerWithNetworks(ctx context.Context, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkConf *network.NetworkingConfig, extraNetworks map[string]*network.EndpointSettings, name string) (container.CreateResponse, error) { + created, err := client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name) if err != nil { return created, err } @@ -682,9 +673,6 @@ func createContainerWithOldNetworks(ctx context.Context, client *client.Client, 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 @@ -692,16 +680,3 @@ func createContainerWithOldNetworks(ctx context.Context, client *client.Client, } 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 - } - } -}