package service import ( "context" "encoding/json" "fmt" "io/fs" "net/netip" "os" "path" "sort" "strings" "time" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/i18n" "github.com/1Panel-dev/1Panel/agent/utils/common" dockerUtils "github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/files" "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/api/types/volume" "github.com/docker/docker/client" ) type containerBackupMeta struct { ContainerName string `json:"containerName"` ContainerID string `json:"containerID"` CreatedAt string `json:"createdAt"` Image string `json:"image"` HostConfig *container.HostConfig `json:"hostConfig,omitempty"` Config *container.Config `json:"config,omitempty"` Mounts []containerMountBackup `json:"mounts"` } type containerMountBackup struct { Type string `json:"type"` Name string `json:"name,omitempty"` Source string `json:"source,omitempty"` Destination string `json:"destination"` Mode string `json:"mode,omitempty"` RW bool `json:"rw"` Propagation string `json:"propagation,omitempty"` BackupPath string `json:"backupPath,omitempty"` Status string `json:"status"` Message string `json:"message,omitempty"` } type containerBackupContext struct { containerName string backupDir string fileName string secret string filePath string tmpDir string mountRoot string wasRunning bool stopped bool fileOp files.FileOp inspectInfo container.InspectResponse meta containerBackupMeta } type containerRecoverContext struct { req dto.CommonRecover targetName string fileOp files.FileOp client *client.Client tmpDir string meta containerBackupMeta inspectInfo container.InspectResponse shouldStart bool createdContainerID string } func (u *BackupService) ContainerBackup(req dto.CommonBackup) error { timeNow := time.Now().Format(constant.DateTimeSlimLayout) + common.RandStrAndNum(5) fileName := req.FileName if fileName == "" { fileName = fmt.Sprintf("%s_%s.tar.gz", req.Name, timeNow) } if !strings.HasSuffix(fileName, ".tar.gz") { fileName += ".tar.gz" } itemDir := fmt.Sprintf("container/%s", req.Name) backupDir := path.Join(global.Dir.LocalBackupDir, itemDir) record := &model.BackupRecord{ Type: req.Type, Name: req.Name, SourceAccountIDs: "1", DownloadAccountID: 1, FileDir: itemDir, FileName: fileName, TaskID: req.TaskID, Status: constant.StatusWaiting, Description: req.Description, } if err := backupRepo.CreateRecord(record); err != nil { global.LOG.Errorf("save backup record failed, err: %v", err) return err } if err := handleContainerBackup(req.Name, nil, record.ID, backupDir, fileName, req.TaskID, req.Secret, req.StopBefore); err != nil { markBackupFailed(record.ID, err) return err } return nil } func (u *BackupService) ContainerRecover(req dto.CommonRecover) error { return handleContainerRecover(req, nil) } func handleContainerBackup(containerName string, parentTask *task.Task, recordID uint, backupDir, fileName, taskID, secret string, stopBefore bool) error { var ( err error backupTask *task.Task ) backupCtx, err := newContainerBackupContext(containerName, backupDir, fileName, secret) if err != nil { return err } backupTask = parentTask if backupTask == nil { backupTask, err = task.NewTaskWithOps(containerName, task.TaskBackup, task.TaskScopeBackup, taskID, 1) if err != nil { return err } } if stopBefore { backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupStop"), func(t *task.Task) error { return stepStopContainerForBackup(backupCtx) }, func(t *task.Task) { _ = stepStartContainerAfterBackup(backupCtx) }, 3, time.Hour) } backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupPrepare"), func(t *task.Task) error { t.Logf("------------------ %s ------------------", containerName) return stepPrepareContainerBackup(backupCtx) }, nil, 3, time.Hour) backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupInspect"), func(t *task.Task) error { return stepBackupContainerInspect(backupCtx) }, nil, 3, time.Hour) backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupMounts"), func(t *task.Task) error { return stepBackupContainerMounts(backupCtx) }, nil, 3, time.Hour) backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupMeta"), func(t *task.Task) error { return stepWriteContainerMeta(backupCtx) }, nil, 3, time.Hour) backupTask.AddSubTaskWithOps(task.GetTaskName(containerName, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return stepPackContainerBackup(backupCtx) }, nil, 3, time.Hour) if stopBefore { backupTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerBackupStart"), func(t *task.Task) error { return stepStartContainerAfterBackup(backupCtx) }, nil, 3, time.Hour) } if parentTask != nil { return nil } go func() { defer backupCtx.close() if err := backupTask.Execute(); err != nil { markBackupFailed(recordID, err) return } backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) }() return nil } func handleContainerRecover(req dto.CommonRecover, parentTask *task.Task) error { var ( err error recoverTask *task.Task recoverCtx *containerRecoverContext ) recoverTask = parentTask if recoverTask == nil { if isImportRecover(req) { taskName := i18n.GetMsgByKey("TaskImport") + i18n.GetMsgByKey("Container") recoverTask, err = task.NewTask(taskName, task.TaskImport, task.TaskScopeBackup, req.TaskID, 1) if err != nil { return err } } else { recoverTask, err = task.NewTaskWithOps("container", task.TaskRecover, task.TaskScopeBackup, req.TaskID, 1) if err != nil { return err } } } timeout := loadRecoverTimeout(req.Timeout) logName := strings.TrimSpace(req.Name) if logName == "" && req.File != "" { logName = strings.TrimSuffix(path.Base(req.File), ".tar.gz") } if logName == "" { logName = "container" } recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverPrepare"), func(t *task.Task) error { ctx, err := newContainerRecoverContext(req) if err != nil { return err } recoverCtx = ctx t.Logf("------------------ %s ------------------", logName) if err := stepPrepareContainerRecover(recoverCtx); err != nil { recoverCtx.close() recoverCtx = nil return err } return nil }, func(t *task.Task) { if recoverCtx != nil { recoverCtx.close() recoverCtx = nil } }, 3, timeout) recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverExtract"), func(t *task.Task) error { return stepExtractContainerRecover(recoverCtx) }, nil, 3, timeout) recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverParse"), func(t *task.Task) error { return stepLoadContainerRecoverData(recoverCtx) }, nil, 3, timeout) recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverCreate"), func(t *task.Task) error { return stepRecreateContainer(recoverCtx, t) }, nil, 3, timeout) recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverMounts"), func(t *task.Task) error { return stepRestoreContainerMounts(recoverCtx) }, nil, 3, timeout) recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverStart"), func(t *task.Task) error { return stepStartRecoveredContainer(recoverCtx) }, nil, 3, timeout) recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("ContainerRecoverCleanup"), func(t *task.Task) error { if recoverCtx != nil { recoverCtx.close() recoverCtx = nil } return nil }, nil, 0, timeout) if parentTask != nil { return nil } go func() { _ = recoverTask.Execute() }() return nil } func loadRecoverTimeout(timeout int) time.Duration { switch timeout { case -1: return 0 case 0: return 3 * time.Hour default: return time.Duration(timeout) * time.Second } } func isImportRecover(req dto.CommonRecover) bool { return req.BackupRecordID == 0 } func newContainerBackupContext(containerName, backupDir, fileName, secret string) (*containerBackupContext, error) { dockerClient, err := dockerUtils.NewDockerClient() if err != nil { return nil, err } defer func() { _ = dockerClient.Close() }() inspectInfo, err := dockerClient.ContainerInspect(context.Background(), containerName) if err != nil { return nil, err } filePath := path.Join(backupDir, fileName) tmpDir := path.Join(path.Dir(filePath), strings.TrimSuffix(path.Base(filePath), ".tar.gz")) backupCtx := &containerBackupContext{ containerName: containerName, backupDir: backupDir, fileName: fileName, secret: secret, filePath: filePath, tmpDir: tmpDir, mountRoot: path.Join(tmpDir, "mounts"), wasRunning: inspectInfo.State != nil && inspectInfo.State.Running, fileOp: files.NewFileOp(), inspectInfo: inspectInfo, meta: containerBackupMeta{ ContainerName: containerName, ContainerID: inspectInfo.ID, CreatedAt: time.Now().Format(constant.DateTimeLayout), Image: inspectInfo.Config.Image, HostConfig: inspectInfo.HostConfig, Config: inspectInfo.Config, Mounts: make([]containerMountBackup, 0), }, } return backupCtx, nil } func newContainerRecoverContext(req dto.CommonRecover) (*containerRecoverContext, error) { dockerClient, err := dockerUtils.NewDockerClient() if err != nil { return nil, err } tmpDir := path.Join(path.Dir(req.File), strings.TrimSuffix(path.Base(req.File), ".tar.gz")) ctx := &containerRecoverContext{ req: req, targetName: req.Name, fileOp: files.NewFileOp(), client: dockerClient, tmpDir: tmpDir, meta: containerBackupMeta{ Mounts: make([]containerMountBackup, 0), }, } return ctx, nil } func (c *containerBackupContext) close() { if c.tmpDir != "" { _ = os.RemoveAll(c.tmpDir) } } func (c *containerRecoverContext) close() { if c.client != nil { _ = c.client.Close() } if c.tmpDir != "" { _ = os.RemoveAll(c.tmpDir) } } func stepPrepareContainerBackup(backupCtx *containerBackupContext) error { if err := os.MkdirAll(backupCtx.backupDir, os.ModePerm); err != nil { return fmt.Errorf("mkdir %s failed, err: %v", backupCtx.backupDir, err) } _ = os.RemoveAll(backupCtx.tmpDir) if err := os.MkdirAll(backupCtx.mountRoot, os.ModePerm); err != nil { return err } return nil } func stepStopContainerForBackup(backupCtx *containerBackupContext) error { if !backupCtx.wasRunning || backupCtx.stopped { return nil } dockerClient, err := dockerUtils.NewDockerClient() if err != nil { return err } defer func() { _ = dockerClient.Close() }() if err := dockerClient.ContainerStop(context.Background(), backupCtx.inspectInfo.ID, container.StopOptions{}); err != nil { return err } backupCtx.stopped = true return nil } func stepStartContainerAfterBackup(backupCtx *containerBackupContext) error { if !backupCtx.stopped { return nil } dockerClient, err := dockerUtils.NewDockerClient() if err != nil { return err } defer func() { _ = dockerClient.Close() }() if err := dockerClient.ContainerStart(context.Background(), backupCtx.inspectInfo.ID, container.StartOptions{}); err != nil { return err } backupCtx.stopped = false return nil } func stepBackupContainerInspect(backupCtx *containerBackupContext) error { inspectBytes, err := json.MarshalIndent(backupCtx.inspectInfo, "", " ") if err != nil { return err } if err := backupCtx.fileOp.SaveFile(path.Join(backupCtx.tmpDir, "inspect.json"), string(inspectBytes), fs.ModePerm); err != nil { return err } if backupCtx.inspectInfo.NetworkSettings != nil { networkBytes, err := json.MarshalIndent(backupCtx.inspectInfo.NetworkSettings, "", " ") if err != nil { return err } if err := backupCtx.fileOp.SaveFile(path.Join(backupCtx.tmpDir, "network.json"), string(networkBytes), fs.ModePerm); err != nil { return err } } return nil } func stepBackupContainerMounts(backupCtx *containerBackupContext) error { var ( dockerClient *client.Client clientErr error ) ensureClient := func() (*client.Client, error) { if dockerClient != nil || clientErr != nil { return dockerClient, clientErr } dockerClient, clientErr = dockerUtils.NewDockerClient() return dockerClient, clientErr } defer func() { if dockerClient != nil { _ = dockerClient.Close() } }() for i, item := range backupCtx.inspectInfo.Mounts { mountMeta := containerMountBackup{ Type: string(item.Type), Name: item.Name, Source: item.Source, Destination: item.Destination, Mode: item.Mode, RW: item.RW, Propagation: string(item.Propagation), Status: "skipped", } mountDirName := fmt.Sprintf("%02d_%s", i, sanitizeContainerMountName(item.Destination)) mountDir := path.Join(backupCtx.mountRoot, mountDirName) mountMeta.BackupPath = path.Join("mounts", mountDirName, "data") switch item.Type { case mount.TypeBind, mount.TypeVolume: if item.Source == "" { mountMeta.Message = "empty source" backupCtx.meta.Mounts = append(backupCtx.meta.Mounts, mountMeta) continue } sourceInfo, statErr := os.Stat(item.Source) if statErr != nil { mountMeta.Message = statErr.Error() backupCtx.meta.Mounts = append(backupCtx.meta.Mounts, mountMeta) continue } dataDir := path.Join(mountDir, "data") if err := os.MkdirAll(dataDir, os.ModePerm); err != nil { return err } if sourceInfo.IsDir() { if err := backupCtx.fileOp.CopyDirWithNewName(item.Source, dataDir, "."); err != nil { return err } } else { if err := backupCtx.fileOp.CopyFile(item.Source, dataDir); err != nil { return err } } if item.Type == mount.TypeVolume && item.Name != "" { cli, err := ensureClient() if err != nil { return err } volumeInfo, volumeErr := cli.VolumeInspect(context.Background(), item.Name) if volumeErr == nil { volumeBytes, volumeMarshalErr := json.MarshalIndent(volumeInfo, "", " ") if volumeMarshalErr == nil { _ = backupCtx.fileOp.SaveFile(path.Join(mountDir, "volume.json"), string(volumeBytes), fs.ModePerm) } } } mountMeta.Status = "backed_up" default: mountMeta.Message = "mount type not supported for data backup" } backupCtx.meta.Mounts = append(backupCtx.meta.Mounts, mountMeta) } return nil } func stepWriteContainerMeta(backupCtx *containerBackupContext) error { metaBytes, err := json.MarshalIndent(backupCtx.meta, "", " ") if err != nil { return err } if err := backupCtx.fileOp.SaveFile(path.Join(backupCtx.tmpDir, "meta.json"), string(metaBytes), fs.ModePerm); err != nil { return err } return nil } func stepPackContainerBackup(backupCtx *containerBackupContext) error { if err := backupCtx.fileOp.TarGzCompressPro(true, backupCtx.tmpDir, backupCtx.filePath, backupCtx.secret, ""); err != nil { return err } return nil } func stepPrepareContainerRecover(recoverCtx *containerRecoverContext) error { if !recoverCtx.fileOp.Stat(recoverCtx.req.File) { return buserr.WithName("ErrFileNotFound", recoverCtx.req.File) } _ = os.RemoveAll(recoverCtx.tmpDir) return nil } func stepExtractContainerRecover(recoverCtx *containerRecoverContext) error { return recoverCtx.fileOp.TarGzExtractPro(recoverCtx.req.File, path.Dir(recoverCtx.req.File), recoverCtx.req.Secret) } func stepLoadContainerRecoverData(recoverCtx *containerRecoverContext) error { if err := loadContainerRecoverMeta(recoverCtx); err != nil { return err } if err := loadContainerRecoverInspect(recoverCtx); err != nil { return err } if recoverCtx.targetName == "" { recoverCtx.targetName = strings.TrimPrefix(recoverCtx.inspectInfo.Name, "/") } if recoverCtx.targetName == "" { recoverCtx.targetName = recoverCtx.meta.ContainerName } if recoverCtx.targetName == "" { return fmt.Errorf("container name not found in recover request or backup file") } if recoverCtx.inspectInfo.Config == nil { recoverCtx.inspectInfo.Config = recoverCtx.meta.Config } if recoverCtx.inspectInfo.HostConfig == nil { recoverCtx.inspectInfo.HostConfig = recoverCtx.meta.HostConfig } if recoverCtx.inspectInfo.Config == nil { return fmt.Errorf("container config not found in backup file") } if recoverCtx.inspectInfo.HostConfig == nil { recoverCtx.inspectInfo.HostConfig = &container.HostConfig{} } recoverCtx.shouldStart = recoverCtx.inspectInfo.State != nil && recoverCtx.inspectInfo.State.Running return nil } func loadContainerRecoverMeta(recoverCtx *containerRecoverContext) error { metaPath := path.Join(recoverCtx.tmpDir, "meta.json") if !recoverCtx.fileOp.Stat(metaPath) { return nil } metaBytes, err := os.ReadFile(metaPath) if err != nil { return err } if err := json.Unmarshal(metaBytes, &recoverCtx.meta); err != nil { return fmt.Errorf("unmarshal meta.json failed, err: %v", err) } return nil } func loadContainerRecoverInspect(recoverCtx *containerRecoverContext) error { inspectPath := path.Join(recoverCtx.tmpDir, "inspect.json") if !recoverCtx.fileOp.Stat(inspectPath) { return fmt.Errorf("inspect.json not found in backup file") } inspectBytes, err := os.ReadFile(inspectPath) if err != nil { return err } if err := json.Unmarshal(inspectBytes, &recoverCtx.inspectInfo); err != nil { return fmt.Errorf("unmarshal inspect.json failed, err: %v", err) } return nil } func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.Task) error { ctx := context.Background() if err := ensureContainerRecoverNetworks(recoverCtx); err != nil { return err } if err := ensureContainerRecoverVolumes(recoverCtx); err != nil { return err } config := cloneContainerConfig(recoverCtx.inspectInfo.Config) hostConfig := cloneContainerHostConfig(recoverCtx.inspectInfo.HostConfig) if config.Image == "" { config.Image = recoverCtx.meta.Image } if config.Image == "" { return fmt.Errorf("container image not found in backup file") } if !checkImageExist(recoverCtx.client, config.Image) { if err := pullImages(taskItem, recoverCtx.client, config.Image); err != nil { return err } } if _, err := recoverCtx.client.ContainerInspect(ctx, recoverCtx.targetName); err == nil { if err := recoverCtx.client.ContainerRemove(ctx, recoverCtx.targetName, container.RemoveOptions{Force: true, RemoveVolumes: false}); err != nil { return err } } else if !client.IsErrNotFound(err) { return err } createRes, err := createContainerWithOldNetworks(ctx, recoverCtx.client, config, hostConfig, recoverCtx.inspectInfo.NetworkSettings, recoverCtx.targetName) if err != nil { return err } recoverCtx.createdContainerID = createRes.ID return nil } 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 { 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) } } } func removeEndpointMacAddresses(primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) { if primary != nil { for _, endpoint := range primary.EndpointsConfig { if endpoint != nil { endpoint.MacAddress = "" } } } for _, endpoint := range extras { if endpoint != nil { endpoint.MacAddress = "" } } } func removeUnsupportedEndpointStaticIP(netName string, info network.Inspect, endpoint *network.EndpointSettings) { if endpoint == nil || endpoint.IPAMConfig == nil { return } if isDefaultBridgeNetwork(netName, info) { endpoint.IPAMConfig = nil return } if endpoint.IPAMConfig.IPv4Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv4Address, false) { endpoint.IPAMConfig.IPv4Address = "" } 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 } } func isDefaultBridgeNetwork(netName string, info network.Inspect) bool { return info.Driver == "bridge" && (netName == "bridge" || info.Name == "bridge") } func networkSupportsStaticIP(info network.Inspect, ip string, isIPv6 bool) bool { if ip == "" { return true } addr, err := netip.ParseAddr(ip) if err != nil { return false } if addr.Is6() != isIPv6 { return false } for _, item := range info.IPAM.Config { if item.Subnet == "" { continue } subnet, err := netip.ParsePrefix(item.Subnet) if err != nil || subnet.Addr().Is6() != isIPv6 { continue } if subnet.Contains(addr) { return true } } return false } func ensureContainerRecoverNetworks(recoverCtx *containerRecoverContext) error { if recoverCtx.inspectInfo.NetworkSettings == nil { return nil } for netName := range recoverCtx.inspectInfo.NetworkSettings.Networks { if netName == "" || netName == "bridge" || netName == "host" || netName == "none" { continue } if _, err := recoverCtx.client.NetworkInspect(context.Background(), netName, network.InspectOptions{}); err != nil { if !client.IsErrNotFound(err) { return err } if _, err := recoverCtx.client.NetworkCreate(context.Background(), netName, network.CreateOptions{Driver: "bridge"}); err != nil { return err } } } return nil } func ensureContainerRecoverVolumes(recoverCtx *containerRecoverContext) error { for _, item := range recoverCtx.meta.Mounts { if item.Type != string(mount.TypeVolume) || item.Name == "" { continue } if _, err := recoverCtx.client.VolumeInspect(context.Background(), item.Name); err == nil { continue } else if !client.IsErrNotFound(err) { return err } createOptions := volume.CreateOptions{Name: item.Name} if item.BackupPath != "" { volumeMetaPath := path.Join(recoverCtx.tmpDir, path.Dir(item.BackupPath), "volume.json") if recoverCtx.fileOp.Stat(volumeMetaPath) { volumeBytes, readErr := os.ReadFile(volumeMetaPath) if readErr != nil { return readErr } var volumeInfo volume.Volume if unmarshalErr := json.Unmarshal(volumeBytes, &volumeInfo); unmarshalErr != nil { return unmarshalErr } if volumeInfo.Driver != "" { createOptions.Driver = volumeInfo.Driver } if len(volumeInfo.Options) != 0 { createOptions.DriverOpts = volumeInfo.Options } if len(volumeInfo.Labels) != 0 { createOptions.Labels = volumeInfo.Labels } } } if _, err := recoverCtx.client.VolumeCreate(context.Background(), createOptions); err != nil { return err } } return nil } func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettings, hostConfig *container.HostConfig) (*network.NetworkingConfig, map[string]*network.EndpointSettings) { extraNetworks := make(map[string]*network.EndpointSettings) if hostConfig != nil { networkMode := string(hostConfig.NetworkMode) if networkMode == "host" || networkMode == "none" { return nil, extraNetworks } } if networkSettings == nil || len(networkSettings.Networks) == 0 { return nil, extraNetworks } primaryName := "" if hostConfig != nil { networkMode := string(hostConfig.NetworkMode) if networkMode != "" && networkMode != "default" && networkMode != "bridge" { if _, ok := networkSettings.Networks[networkMode]; ok { primaryName = networkMode } } } if primaryName == "" { if _, ok := networkSettings.Networks["bridge"]; ok { primaryName = "bridge" } else { names := make([]string, 0, len(networkSettings.Networks)) for name := range networkSettings.Networks { names = append(names, name) } sort.Strings(names) if len(names) > 0 { primaryName = names[0] } } } config := &network.NetworkingConfig{EndpointsConfig: make(map[string]*network.EndpointSettings)} for name, endpoint := range networkSettings.Networks { if name == "host" || name == "none" { continue } 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, 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 { config.EndpointsConfig[name] = endpointSetting } else { extraNetworks[name] = endpointSetting } } if len(config.EndpointsConfig) == 0 { return nil, extraNetworks } 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{} } item := *config if len(config.Env) != 0 { item.Env = append([]string(nil), config.Env...) } if len(config.Cmd) != 0 { item.Cmd = append([]string(nil), config.Cmd...) } if len(config.Entrypoint) != 0 { item.Entrypoint = append([]string(nil), config.Entrypoint...) } if len(config.Labels) != 0 { labels := make(map[string]string, len(config.Labels)) for key, val := range config.Labels { labels[key] = val } item.Labels = labels } if len(config.Volumes) != 0 { volumes := make(map[string]struct{}, len(config.Volumes)) for key, val := range config.Volumes { volumes[key] = val } item.Volumes = volumes } return &item } func cloneContainerHostConfig(hostConfig *container.HostConfig) *container.HostConfig { if hostConfig == nil { return &container.HostConfig{} } item := *hostConfig if len(hostConfig.Binds) != 0 { item.Binds = append([]string(nil), hostConfig.Binds...) } if len(hostConfig.DNS) != 0 { item.DNS = append([]string(nil), hostConfig.DNS...) } if len(hostConfig.ExtraHosts) != 0 { item.ExtraHosts = append([]string(nil), hostConfig.ExtraHosts...) } if len(hostConfig.Mounts) != 0 { item.Mounts = append([]mount.Mount(nil), hostConfig.Mounts...) } return &item } func stepRestoreContainerMounts(recoverCtx *containerRecoverContext) error { currentContainer := recoverCtx.createdContainerID if currentContainer == "" { currentContainer = recoverCtx.targetName } currentInspect, err := recoverCtx.client.ContainerInspect(context.Background(), currentContainer) if err != nil { return err } currentMounts := make(map[string]container.MountPoint, len(currentInspect.Mounts)) for _, item := range currentInspect.Mounts { currentMounts[item.Destination] = item } for _, item := range recoverCtx.meta.Mounts { if item.Status != "backed_up" || item.BackupPath == "" || !item.RW { continue } backupPath := path.Join(recoverCtx.tmpDir, item.BackupPath) if !recoverCtx.fileOp.Stat(backupPath) { continue } sourcePath := item.Source if currentMount, ok := currentMounts[item.Destination]; ok { if currentMount.Source != "" { sourcePath = currentMount.Source } if item.Type == string(mount.TypeVolume) && item.Name == "" { item.Name = currentMount.Name } } if sourcePath == "" && item.Type == string(mount.TypeVolume) && item.Name != "" { volumeInfo, volumeErr := recoverCtx.client.VolumeInspect(context.Background(), item.Name) if volumeErr != nil { return volumeErr } sourcePath = volumeInfo.Mountpoint } if sourcePath == "" { continue } if err := restoreContainerMountData(recoverCtx.fileOp, backupPath, sourcePath); err != nil { return err } } return nil } func restoreContainerMountData(fileOp files.FileOp, backupPath, sourcePath string) error { if sourcePath == "/" { return fmt.Errorf("invalid mount source path /") } entries, err := os.ReadDir(backupPath) if err != nil { return err } if len(entries) == 1 && !entries[0].IsDir() && entries[0].Name() == path.Base(sourcePath) { if err := os.MkdirAll(path.Dir(sourcePath), os.ModePerm); err != nil { return err } _ = os.RemoveAll(sourcePath) if err := fileOp.CopyFile(path.Join(backupPath, entries[0].Name()), path.Dir(sourcePath)); err != nil { return err } return nil } _ = os.RemoveAll(sourcePath) if err := os.MkdirAll(sourcePath, os.ModePerm); err != nil { return err } if err := fileOp.CopyDirWithNewName(backupPath, sourcePath, "."); err != nil { return err } return nil } func stepStartRecoveredContainer(recoverCtx *containerRecoverContext) error { if !recoverCtx.shouldStart { return nil } containerID := recoverCtx.createdContainerID if containerID == "" { containerID = recoverCtx.targetName } return recoverCtx.client.ContainerStart(context.Background(), containerID, container.StartOptions{}) } func sanitizeContainerMountName(in string) string { name := strings.TrimSpace(in) name = strings.Trim(name, "/") name = strings.ReplaceAll(name, "/", "_") name = strings.ReplaceAll(name, ":", "_") if name == "" { return "root" } return name }