package swarm import ( "fmt" "log" "os" "os/exec" "path/filepath" "strings" ) // ArchiveVolumes tars each Docker volume belonging to stackName into archiveDir // and removes the volumes afterwards. Archive layout: // // {archiveDir}/{stackName}/{volumeName}.tar.gz func (c *Client) ArchiveVolumes(stackName, archiveDir string) error { vols, err := c.listVolumes(stackName) if err != nil { return fmt.Errorf("list volumes: %w", err) } if len(vols) == 0 { log.Printf("archive: no volumes found for %s, nothing to archive", stackName) return nil } dir := filepath.Join(archiveDir, stackName) if err := os.MkdirAll(dir, 0755); err != nil { return fmt.Errorf("create archive dir: %w", err) } for _, vol := range vols { tarName := vol + ".tar.gz" log.Printf("archive: backing up volume %s -> %s/%s", vol, dir, tarName) cmd := exec.Command("docker", "run", "--rm", "-v", vol+":/data:ro", "-v", dir+":/backup", "alpine", "tar", "czf", "/backup/"+tarName, "-C", "/data", ".", ) cmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost) if out, err := cmd.CombinedOutput(); err != nil { log.Printf("archive: failed to tar %s: %s: %v", vol, strings.TrimSpace(string(out)), err) continue } rmCmd := exec.Command("docker", "volume", "rm", vol) rmCmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost) if out, err := rmCmd.CombinedOutput(); err != nil { log.Printf("archive: failed to remove volume %s: %s: %v", vol, strings.TrimSpace(string(out)), err) } } log.Printf("archive: completed for %s (%d volumes)", stackName, len(vols)) return nil } // RestoreVolumes recreates Docker volumes from a previous archive. No-op if // no archive directory exists for the stack (fresh customer). func (c *Client) RestoreVolumes(stackName, archiveDir string) error { dir := filepath.Join(archiveDir, stackName) entries, err := os.ReadDir(dir) if err != nil { if os.IsNotExist(err) { return nil } return fmt.Errorf("read archive dir: %w", err) } restored := 0 for _, e := range entries { name := e.Name() if !strings.HasSuffix(name, ".tar.gz") { continue } vol := strings.TrimSuffix(name, ".tar.gz") log.Printf("restore: extracting %s -> volume %s", name, vol) createCmd := exec.Command("docker", "volume", "create", vol) createCmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost) if out, err := createCmd.CombinedOutput(); err != nil { log.Printf("restore: volume create %s failed: %s: %v", vol, strings.TrimSpace(string(out)), err) continue } cmd := exec.Command("docker", "run", "--rm", "-v", vol+":/data", "-v", dir+":/backup:ro", "alpine", "tar", "xzf", "/backup/"+name, "-C", "/data", ) cmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost) if out, err := cmd.CombinedOutput(); err != nil { log.Printf("restore: extract %s failed: %s: %v", name, strings.TrimSpace(string(out)), err) continue } os.Remove(filepath.Join(dir, name)) restored++ } if restored > 0 { os.Remove(dir) log.Printf("restore: completed for %s (%d volumes)", stackName, restored) } return nil } func (c *Client) listVolumes(stackName string) ([]string, error) { cmd := exec.Command("docker", "volume", "ls", "--filter", "name="+stackName+"_", "--format", "{{.Name}}", ) cmd.Env = append(os.Environ(), "DOCKER_HOST="+c.cfg.DockerHost) out, err := cmd.CombinedOutput() if err != nil { return nil, fmt.Errorf("docker volume ls: %s: %w", strings.TrimSpace(string(out)), err) } var vols []string for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { if line != "" { vols = append(vols, line) } } return vols, nil }