mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/1Panel-dev/1Panel.git
synced 2026-09-21 00:24:12 +08:00
1016 lines
31 KiB
Go
1016 lines
31 KiB
Go
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
|
|
}
|