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

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)
}