You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
298 lines
6.2 KiB
Go
298 lines
6.2 KiB
Go
1 year ago
|
package main
|
||
|
|
||
|
import (
|
||
|
"archive/tar"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil"
|
||
|
"os"
|
||
|
"os/exec"
|
||
|
"path/filepath"
|
||
|
"strings"
|
||
|
"syscall"
|
||
|
"time"
|
||
|
|
||
|
"github.com/docker/docker/api/types"
|
||
|
"github.com/docker/docker/api/types/container"
|
||
|
"github.com/docker/go-connections/nat"
|
||
|
"github.com/kennygrant/sanitize"
|
||
|
"github.com/spf13/cobra"
|
||
|
)
|
||
|
|
||
|
// Backup is used to gather all of a container's metadata, so we can encode it
|
||
|
// as JSON and store it
|
||
|
type Backup struct {
|
||
|
Name string
|
||
|
Config *container.Config
|
||
|
PortMap nat.PortMap
|
||
|
Mounts []types.MountPoint
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
optLaunch = ""
|
||
|
optTar = false
|
||
|
optAll = false
|
||
|
optStopped = false
|
||
|
optVerbose = false
|
||
|
|
||
|
paths []string
|
||
|
tw *tar.Writer
|
||
|
|
||
|
backupCmd = &cobra.Command{
|
||
|
Use: "backup [container-id]",
|
||
|
Short: "creates a backup of a container",
|
||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||
|
if optAll {
|
||
|
return backupAll()
|
||
|
}
|
||
|
|
||
|
if len(args) < 1 {
|
||
|
return fmt.Errorf("backup requires the ID of a container")
|
||
|
}
|
||
|
return backup(args[0])
|
||
|
},
|
||
|
}
|
||
|
)
|
||
|
|
||
|
func collectFile(path string, info os.FileInfo, err error) error {
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if optVerbose {
|
||
|
fmt.Println("Adding", path)
|
||
|
}
|
||
|
|
||
|
paths = append(paths, path)
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func collectFileTar(path string, info os.FileInfo, err error) error {
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if info.Mode()&os.ModeSocket != 0 {
|
||
|
// ignore sockets
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
if optVerbose {
|
||
|
fmt.Println("Adding", path)
|
||
|
}
|
||
|
|
||
|
th, err := tar.FileInfoHeader(info, path)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
th.Name = path
|
||
|
if si, ok := info.Sys().(*syscall.Stat_t); ok {
|
||
|
th.Uid = int(si.Uid)
|
||
|
th.Gid = int(si.Gid)
|
||
|
}
|
||
|
|
||
|
if err := tw.WriteHeader(th); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if !info.Mode().IsRegular() {
|
||
|
return nil
|
||
|
}
|
||
|
if info.Mode().IsDir() {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
file, err := os.Open(path)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
_, err = io.Copy(tw, file)
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func backupTar(filename string, backup Backup) error {
|
||
|
b, err := json.MarshalIndent(backup, "", " ")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
// fmt.Println(string(b))
|
||
|
|
||
|
tarfile, err := os.Create(filename + ".tar")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
tw = tar.NewWriter(tarfile)
|
||
|
|
||
|
th := &tar.Header{
|
||
|
Name: "container.json",
|
||
|
Size: int64(len(b)),
|
||
|
ModTime: time.Now(),
|
||
|
AccessTime: time.Now(),
|
||
|
ChangeTime: time.Now(),
|
||
|
Mode: 0600,
|
||
|
}
|
||
|
|
||
|
if err := tw.WriteHeader(th); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if _, err := tw.Write(b); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
for _, m := range backup.Mounts {
|
||
|
// fmt.Printf("Mount (type %s) %s -> %s\n", m.Type, m.Source, m.Destination)
|
||
|
|
||
|
err := filepath.Walk(m.Source, collectFileTar)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
tw.Close()
|
||
|
fmt.Println("Created backup:", filename+".tar")
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func getFullImageName(imageName string) (string, error) {
|
||
|
// If the image already specifies a tag we can safely use as-is
|
||
|
if strings.Contains(imageName, ":") {
|
||
|
return imageName, nil
|
||
|
}
|
||
|
|
||
|
// If the used image doesn't include tag information try to find one (if it exists).
|
||
|
images, err := cli.ImageList(ctx, types.ImageListOptions{})
|
||
|
if err != nil {
|
||
|
// Couldn't get image list, abort
|
||
|
return imageName, err
|
||
|
}
|
||
|
|
||
|
for _, image := range images {
|
||
|
if (!strings.Contains(imageName, image.ID)) || len(image.RepoTags) == 0 {
|
||
|
// unrelated image or image entry doesn't have any tags, move on
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
for _, tag := range image.RepoTags {
|
||
|
// use closer matching tag if it exists
|
||
|
if !strings.Contains(tag, imageName) {
|
||
|
continue
|
||
|
}
|
||
|
return tag, nil
|
||
|
}
|
||
|
// If none of the tags matches the base image name, return the first tag
|
||
|
return image.RepoTags[0], nil
|
||
|
}
|
||
|
|
||
|
// There is no tag on the matching image, just have to go with what was provided
|
||
|
return imageName, nil
|
||
|
}
|
||
|
|
||
|
func backup(ID string) error {
|
||
|
conf, err := cli.ContainerInspect(ctx, ID)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
fmt.Printf("Creating backup of %s (%s, %s)\n", conf.Name[1:], conf.Config.Image, conf.ID[:12])
|
||
|
|
||
|
paths = []string{}
|
||
|
|
||
|
conf.Config.Image, err = getFullImageName(conf.Config.Image)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
backup := Backup{
|
||
|
Name: conf.Name,
|
||
|
PortMap: conf.HostConfig.PortBindings,
|
||
|
Config: conf.Config,
|
||
|
Mounts: conf.Mounts,
|
||
|
}
|
||
|
|
||
|
filename := sanitize.Path(fmt.Sprintf("%s-%s", conf.Config.Image, ID))
|
||
|
filename = strings.Replace(filename, "/", "_", -1)
|
||
|
if optTar {
|
||
|
return backupTar(filename, backup)
|
||
|
}
|
||
|
|
||
|
b, err := json.MarshalIndent(backup, "", " ")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
// fmt.Println(string(b))
|
||
|
|
||
|
err = ioutil.WriteFile(filename+".backup.json", b, 0600)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
for _, m := range conf.Mounts {
|
||
|
// fmt.Printf("Mount (type %s) %s -> %s\n", m.Type, m.Source, m.Destination)
|
||
|
err := filepath.Walk(m.Source, collectFile)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
filelist, err := os.Create(filename + ".backup.files")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer filelist.Close()
|
||
|
|
||
|
_, err = filelist.WriteString(filename + ".backup.json\n")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
for _, s := range paths {
|
||
|
_, err := filelist.WriteString(s + "\n")
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fmt.Println("Created backup:", filename+".backup.json")
|
||
|
|
||
|
if optLaunch != "" {
|
||
|
ol := strings.Replace(optLaunch, "%tag", filename, -1)
|
||
|
ol = strings.Replace(ol, "%list", filename+".backup.files", -1)
|
||
|
|
||
|
fmt.Println("Launching external command and waiting for it to finish:")
|
||
|
fmt.Println(ol)
|
||
|
|
||
|
l := strings.Split(ol, " ")
|
||
|
cmd := exec.Command(l[0], l[1:]...)
|
||
|
return cmd.Run()
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func backupAll() error {
|
||
|
containers, err := cli.ContainerList(ctx, types.ContainerListOptions{
|
||
|
All: optStopped,
|
||
|
})
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
|
||
|
for _, container := range containers {
|
||
|
err := backup(container.ID)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func init() {
|
||
|
backupCmd.Flags().StringVarP(&optLaunch, "launch", "l", "", "launch external program with file-list as argument")
|
||
|
backupCmd.Flags().BoolVarP(&optTar, "tar", "t", false, "create tar backups")
|
||
|
backupCmd.Flags().BoolVarP(&optAll, "all", "a", false, "backup all running containers")
|
||
|
backupCmd.Flags().BoolVarP(&optStopped, "stopped", "s", false, "in combination with --all: also backup stopped containers")
|
||
|
backupCmd.Flags().BoolVarP(&optVerbose, "verbose", "v", false, "print detailed backup progress")
|
||
|
RootCmd.AddCommand(backupCmd)
|
||
|
}
|