first commit

main
Ben 1 year ago
commit 273bde620f

@ -0,0 +1,58 @@
#
# Copyright (C) 2006-2014 OpenWrt.org
#
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-docker-backup
LUCI_PKGARCH:=all
LUCI_DEPENDS:=+lsblk +docker +luci-lib-taskd
include $(TOPDIR)/feeds/luci/luci.mk
define Package/luci-app-docker-backup/conffiles
/root/etc/config/docker-backup
SECTION:=luci
CATEGORY:=LuCI
SUBMENU:=3. Applications
TITLE:=LuCI Docker container module by PrivateRouter
endef
define Build/Prepare
endef
define Build/Configure
endef
define Build/Compile
endef
define Package/install
$(INSTALL_DIR) $(1)./root/etc/config/
$(INSTALL_DIR) $(1)./root/etc/uci-defaults/
$(INSTALL_DIR) $(1)./root/usr/libexec/apps/docker-backup/
$(INSTALL_DIR) $(1)./root/usr/share/rpcd/acl.d/
$(INSTALL_DIR) $(1)./etc/config # add this line
$(INSTALL_BIN) ./root/etc/config/docker-backup $(1)/etc/config/docker-backup
$(INSTALL_BIN) ./root/etc/uci-defaults/luci-app-docker-backup $(1)/etc/uci-defaults/luci-app-docker-backup
$(INSTALL_BIN) ./root/usr/libexec/apps/docker-backup/docker-backup.sh $(1)/usr/libexec/apps/docker-backup/docker-backup.sh
$(INSTALL_BIN) ./root/usr/libexec/apps/docker-backup/docker-backup $(1)/usr/libexec/apps/docker-backup/docker-backup
$(INSTALL_DATA) ./root/usr/libexec/apps/docker-backup/restore.go $(1)/usr/libexec/apps/docker-backup/restore.go
$(INSTALL_DATA) ./root/usr/libexec/apps/docker-backup/main.go $(1)/usr/libexec/apps/docker-backup/main.go
$(INSTALL_DATA) ./root/usr/libexec/apps/docker-backup/go.sum $(1)/usr/libexec/apps/docker-backup/go.sum
$(INSTALL_DATA) ./root/usr/libexec/apps/docker-backup/go.mod $(1)/usr/libexec/apps/docker-backup/go.mod
$(INSTALL_DATA) ./root/usr/libexec/apps/docker-backup/backup.go $(1)/usr/libexec/apps/docker-backup/backup.go
$(INSTALL_DATA) ./luasrc/model/cbi/luci-app-docker-backup/luci-app-docker-backup.lua $(1)/usr/lib/lua/luci/model/cbi/luci-app-docker-backup/luci-app-docker-backup.lua
$(INSTALL_DATA) ./luasrc/controller/docker-backup.lua $(1)/usr/lib/lua/luci/controller/docker-backup.lua
$(INSTALL_DATA) ./luasrc/view/docker-backup/status.htm $(1)/usr/lib/lua/luci/view/docker-backup/status.htm
$(INSTALL_DATA) ./luasrc/model/docker-backup.lua $(1)/usr/lib/lua/luci/model/docker-backup.lua
$(INSTALL_DATA) ./luasrc/model/cbi/docker-backup.lua $(1)/usr/lib/lua/luci/model/cbi/docker-backup.lua
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-docker-backup.json $(1)/usr/share/rpcd/acl.d/luci-app-docker-backup.json
endef
$(eval $(call BuildPackage,$(PKG_NAME)))

@ -0,0 +1,7 @@
module("luci.controller.docker-backup", package.seeall)
function index()
entry({"admin", "apps", "docker-backup"}, alias("admin", "apps", "docker-backup", "config"), _("docker-backup"), 30).dependent = true
entry({"admin", "apps", "docker-backup", "config"}, cbi("docker-backup"))
end

@ -0,0 +1,50 @@
--[[
LuCI - Lua Configuration Interface
]]--
local taskd = require "luci.model.tasks"
local dockerbackup_model = require "luci.model.docker-backup"
local m, s, o
m = taskd.docker_map("docker-backup", "docker-backup", "/usr/libexec/apps/docker-backup/docker-backup.sh",
translate("docker-backup"),
translate("Docker-Backup helps you easily backup and restore docker containers on any device running docker. Make sure images are pulled before restoring a container."))
s = m:section(TypedSection, "docker-backup", translate("Setup"),
translate("The default backup and restore directory is /opt/docker2/compose/docker-backup. Be sure to look for backup files in this location and place restore files in same location unless otherwise marked.")
.. "<br>" .. translate("Backup or restore a docker container ID:"))
s.addremove=false
s.anonymous=true
o = s:option(Flag, "backup", translate("Enable Backup"), translate("Check only if you wish to back up a docker container ID or all docker containers."))
o.default = 0
o.rmempty = false
o = s:option(Flag, "restore", translate("Enable Restore"), translate("Check only if you wish to restore a docker container from a given location."))
o.default = 0
o.rmempty = false
o = s:option(Value, "restore_container", translate("Restore Container ID").."<b>*</b>")
o.default = "container"
o.datatype = "string"
o:depends("backup", 0)
o = s:option(Value, "backup_container", translate("Backup Container ID").."<b>*</b>")
o.default = "container"
o.datatype = "string"
o:depends("restore", 0)
local blocks = dockerbackup_model.blocks()
local home = dockerbackup_model.home()
o = s:option(Value, "config_path", translate("Config path").."<b>*</b>")
o.default = "/opt/docker2/compose/docker-backup/"
o.rmempty = false
o.datatype = "string"
for _, val in pairs(blocks) do
o:value(val, val)
end
o.default = home
return m

@ -0,0 +1,55 @@
local util = require "luci.util"
local jsonc = require "luci.jsonc"
local dockerbackup = {}
dockerbackup.blocks = function()
local f = io.popen("lsblk -s -f -b -o NAME,FSSIZE,MOUNTPOINT --json", "r")
local vals = {}
if f then
local ret = f:read("*all")
f:close()
local obj = jsonc.parse(ret)
for _, val in pairs(obj["blockdevices"]) do
local fsize = val["fssize"]
if fsize ~= nil and string.len(fsize) > 10 and val["mountpoint"] then
-- fsize > 1G
vals[#vals+1] = val["mountpoint"]
end
end
end
return vals
end
dockerbackup.home = function()
local uci = require "luci.model.uci".cursor()
local home_dirs = {}
home_dirs["main_dir"] = uci:get_first("quickstart", "main", "main_dir", "/root")
home_dirs["Configs"] = uci:get_first("quickstart", "main", "conf_dir", home_dirs["main_dir"].."/Configs")
home_dirs["Public"] = uci:get_first("quickstart", "main", "pub_dir", home_dirs["main_dir"].."/Public")
home_dirs["Downloads"] = uci:get_first("quickstart", "main", "dl_dir", home_dirs["Public"].."/Downloads")
home_dirs["Caches"] = uci:get_first("quickstart", "main", "tmp_dir", home_dirs["main_dir"].."/Caches")
return home_dirs
end
dockerbackup.find_paths = function(blocks, home_dirs, path_name)
local default_path = ''
local configs = {}
default_path = home_dirs[path_name] .. "/docker-backup"
if #blocks == 0 then
table.insert(configs, default_path)
else
for _, val in pairs(blocks) do
table.insert(configs, val .. "/" .. path_name .. "/docker-backup")
end
local without_conf_dir = "/root/" .. path_name .. "/docker-backup"
if default_path == without_conf_dir then
default_path = configs[1]
end
end
return configs, default_path
end
return dockerbackup

@ -0,0 +1,31 @@
<%
local util = require "luci.util"
local container_status = util.trim(util.exec("/usr/libexec/apps/docker-backup/docker-backup.sh status"))
local container_install = (string.len(container_status) > 0)
local container_running = container_status == "running"
-%>
<div class="cbi-value">
<label class="cbi-value-title"><%:Status%></label>
<div class="cbi-value-field">
<% if container_running then %>
<button class="cbi-button cbi-button-success" disabled="true"><%:docker-backup is running%></button>
<% else %>
<button class="cbi-button cbi-button-negative" disabled="true"><%:docker-backup is not running%></button>
<% end %>
</div>
</div>
<%
if container_running then
local port=util.trim(util.exec("/usr/libexec/apps/docker-backup.sh port"))
if port == "" then
port="80"
end
-%>
<div class="cbi-value cbi-value-last">
<label class="cbi-value-title">&nbsp;</label>
<div class="cbi-value-field">
<input type="button" class="btn cbi-button cbi-button-apply" name="start" value="<%:Open docker-backup%>" onclick="window.open('http://'+location.hostname+':<%=port%>/', '_blank')">
</div>
</div>
<% end %>

@ -0,0 +1,7 @@
config docker-backup
option 'backup' '1'
option 'restore' '0'
option 'image' 'default'
option 'restore_container' ''
option 'backup_container' ''
option 'config_path' '/opt/docker2/compose/docker-backup/'

@ -0,0 +1,9 @@
#!/bin/sh
image_name=`uci get docker-backup.@docker-backup[0].image 2>/dev/null`
touch /etc/config/docker-backup
uci -q batch <<-EOF >/dev/null
set docker-backup.@docker-backup[0].image="default"
commit docker-backup
EOF
exit 0

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Christian Muehlhaeuser
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,98 @@
docker-backup
=============
[![Latest Release](https://img.shields.io/github/release/muesli/docker-backup.svg)](https://github.com/muesli/docker-backup/releases)
[![Build Status](https://github.com/muesli/docker-backup/workflows/build/badge.svg)](https://github.com/muesli/docker-backup/actions)
[![Go ReportCard](https://goreportcard.com/badge/muesli/docker-backup)](https://goreportcard.com/report/muesli/docker-backup)
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://pkg.go.dev/github.com/muesli/docker-backup)
A tool to create & restore complete, self-contained backups of Docker containers
# What's the issue
Docker services usually have a bunch of volatile data volumes that need to be
backed up. Backing up an entire (file)system is easy, but often enough you just
want to create a backup of a single (or a few) containers, maybe to restore them
on another system later.
Some services, such as databases, also need to be aware (flushed/synced/paused)
of an impending backup. The backup should be run on the Docker host, as you
don't want to have a backup client configured & running in every single
container either, since this would add a lot of maintenance & administration
overhead.
`docker-backup` directly connects to Docker, analyzes a container's mounts &
volumes, and generates a list of dirs & files that need to be backed up on the
host system. This also collects all the metadata information associated with a
container, so it can be restored or cloned on a different host, including its
port-mappings and data volumes.
The generated list can either be fed to an existing backup solution or
`docker-backup` can directly create a `.tar` image of your container, so you can
simply copy it to another machine.
## Installation
`docker-backup` requires Go 1.11 or higher. Make sure you have a working Go
environment. See the [install instructions](https://golang.org/doc/install.html).
`docker-backup` works with Docker hosts running Docker 18.02 (API version 1.36)
and newer.
### Packages
- Arch Linux: [docker-backup](https://aur.archlinux.org/packages/docker-backup/)
### From source
git clone https://github.com/muesli/docker-backup.git
cd docker-backup
go build
Run `docker-backup --help` to see a full list of options.
## Usage
### Creating a Backup
To backup a single container start `docker-backup` with the `backup` command and
supply the ID of the container:
docker-backup backup <container ID>
This will create a `.json` file with the container's metadata, as well as a file
containing all the volumes that need to be backed up with an external tool like
[restic](https://restic.net/) or [borgbackup](https://www.borgbackup.org/).
If you want to directly create a `.tar` file containing all the container's
data, simply run:
docker-backup backup --tar <container ID>
You can also backup all running containers on the host with the `--all` flag:
docker-backup backup --all
To backup all containers (regardless of their current running state), run:
docker-backup backup --all --stopped
With the help of `--launch` you can directly launch a backup program with the
generated file-list supplied as an argument:
docker-backup backup --all --launch "restic -r /dest backup --password-file pwfile --tag %tag --files-from %list"
### Restoring a Backup
To restore a container, run `docker-backup` with the `restore` command:
docker-backup restore <backup file>
`docker-backup` will automatically detect whether you supplied a `.tar` or
`.json` file and restore the container, including all its port-mappings and data
volumes.
If you want to start the container once the restore has finished, add the
`--start` flag:
docker-backup restore --start <backup file>

@ -0,0 +1,297 @@
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)
}

@ -0,0 +1,73 @@
#!/bin/sh
echo $PATH
ACTION=${1}
shift 1
usage() {
echo "usage: $0 sub-command"
echo "where sub-command is one of:"
echo " install Install the docker-backup"
}
do_install() {
local backup=`uci get docker-backup.@docker-backup[0].backup 2>/dev/null`
local restore=`uci get docker-backup.@docker-backup[0].restore 2>/dev/null`
local restore_container=`uci get docker-backup.@docker-backup[0].restore_container 2>/dev/null`
local backup_container=`uci get docker-backup.@docker-backup[0].backup_container 2>/dev/null`
local config_path=`uci get docker-backup.@docker-backup[0].config_path 2>/dev/null`
if [ "$backup" = "1" ]; then
echo "backing up containers!"
/usr/libexec/apps/docker-backup/docker-backup backup "$backup_container"
mv *.backup.files /opt/docker2/compose/docker-backup
mv *.backup.json /opt/docker2/compose/docker-backup
mv *.tar /opt/docker2/compose/docker-backup
fi
if [ "$restore" = "1" ]; then
echo "restoring containers!"
result=`/usr/libexec/apps/docker-backup/docker-backup restore ${config_path}"$restore_container" 2>&1`
if echo "$result" | grep -q "No such image"; then
image_name=$(echo "$result" | sed -n 's/.*No such image: \(.*\)\s*/\1/p')
echo "Pulling missing image: $image_name"
/usr/bin/docker pull $image_name
while ! /usr/bin/docker image inspect $image_name >/dev/null 2>&1; do
echo "Waiting for image to download..."
sleep 1
done
fi
/usr/libexec/apps/docker-backup/docker-backup restore ${config_path}"$restore_container"
if [ $? -eq 0 ]; then
echo "Restore successful"
else
echo "Restore failed"
exit 1
fi
fi
}
case ${ACTION} in
"install")
do_install
;;
"upgrade")
do_install
;;
"rm")
opkg remove luci-app-docker-backup
;;
"start" | "stop" | "restart")
do_install
;;
"status")
do_install
;;
"port")
do_install
;;
*)
usage
exit 1
;;
esac

@ -0,0 +1,26 @@
module github.com/muesli/docker-backup
go 1.12
require (
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/Microsoft/go-winio v0.4.16 // indirect
github.com/docker/distribution v2.7.1+incompatible // indirect
github.com/docker/docker v0.7.3-0.20190503020752-619df5a8f60f
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.4.0 // indirect
github.com/gogo/protobuf v1.2.1 // indirect
github.com/google/go-cmp v0.5.5 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/kennygrant/sanitize v1.2.4
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3 // indirect
golang.org/x/net v0.0.0-20190502183928-7f726cade0ab // indirect
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
google.golang.org/grpc v1.20.1 // indirect
gotest.tools v2.2.0+incompatible // indirect
)

@ -0,0 +1,81 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v0.7.3-0.20190503020752-619df5a8f60f h1:Dtk1lVB9XfLuYUW+4mkWslWOBexBdVHD6IlsWu9R4nE=
github.com/docker/docker v0.7.3-0.20190503020752-619df5a8f60f/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190502183928-7f726cade0ab h1:9RfW3ktsOZxgo9YNbBAjq1FWzc/igwEcUzZz8IXgSbk=
golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3 h1:7TYNF4UdlohbFwpNH04CoPMp1cHUZgO1Ebq5r2hIjfo=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

@ -0,0 +1,45 @@
/*
* A tool to create & restore full backups of Docker containers
* Copyright (c) 2019, Christian Muehlhaeuser <muesli@gmail.com>
*
* For license see LICENSE
*/
package main
import (
"context"
"fmt"
"os"
"github.com/docker/docker/client"
"github.com/spf13/cobra"
)
var (
cli *client.Client
ctx = context.Background()
// RootCmd is the core command used for cli-arg parsing
RootCmd = &cobra.Command{
Use: "docker-backup",
Short: "docker-backup creates or restores backups of Docker containers",
SilenceErrors: true,
SilenceUsage: true,
}
)
func main() {
var err error
// cli, err = client.NewEnvClient()
// cli, err = client.NewClientWithOpts(client.FromEnv)
cli, err = client.NewClientWithOpts(client.WithVersion("1.36"))
if err != nil {
panic(err)
}
if err := RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(-1)
}
}

@ -0,0 +1,236 @@
package main
import (
"archive/tar"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/spf13/cobra"
)
var (
optStart = false
restoreCmd = &cobra.Command{
Use: "restore <backup file>",
Short: "restores a backup of a container",
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("restore requires a .json or .tar backup")
}
if strings.HasSuffix(args[0], ".json") {
return restore(args[0])
} else if strings.HasSuffix(args[0], ".tar") {
return restoreTar(args[0])
}
return fmt.Errorf("Unknown file type, please provide a .tar or .json file")
},
}
)
func restoreTar(filename string) error {
tarfile, err := os.Open(filename)
if err != nil {
return err
}
defer tarfile.Close()
tr := tar.NewReader(tarfile)
var b []byte
for {
th, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
switch th.Name {
case "container.json":
var err error
b, err = ioutil.ReadAll(tr)
if err != nil {
return err
}
}
}
var backup Backup
err = json.Unmarshal(b, &backup)
if err != nil {
return err
}
id, err := createContainer(backup)
if err != nil {
return err
}
conf, err := cli.ContainerInspect(ctx, id)
if err != nil {
return err
}
tt := map[string]string{}
for _, oldPath := range backup.Mounts {
for _, hostPath := range conf.Mounts {
if oldPath.Destination == hostPath.Destination {
tt[oldPath.Source] = hostPath.Source
break
}
}
}
if _, err := tarfile.Seek(0, 0); err != nil {
return err
}
tr = tar.NewReader(tarfile)
for {
th, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if th.Name == "container.json" {
continue
}
path := th.Name
fmt.Println("Restoring:", path)
for k, v := range tt {
if strings.HasPrefix(path, k) {
path = v + path[len(k):]
}
}
if th.Typeflag == tar.TypeDir {
if err := os.MkdirAll(path, os.FileMode(th.Mode)); err != nil {
return err
}
} else {
file, err := os.Create(path)
if err != nil {
return err
}
if _, err := io.Copy(file, tr); err != nil {
return err
}
file.Close()
}
if err := os.Chmod(path, os.FileMode(th.Mode)); err != nil {
return err
}
if err := os.Chown(path, th.Uid, th.Gid); err != nil {
return err
}
fmt.Println("Created as:", path)
}
if optStart {
return startContainer(id)
}
return nil
}
func restore(filename string) error {
var backup Backup
b, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
err = json.Unmarshal(b, &backup)
if err != nil {
return err
}
id, err := createContainer(backup)
if err != nil {
return err
}
if optStart {
return startContainer(id)
}
return nil
}
func createContainer(backup Backup) (string, error) {
nameparts := strings.Split(backup.Name, "/")
name := nameparts[len(nameparts)-1]
fmt.Println("Restoring Container:", name)
_, _, err := cli.ImageInspectWithRaw(ctx, backup.Config.Image)
if err != nil {
fmt.Println("Pulling Image:", backup.Config.Image)
_, err := cli.ImagePull(ctx, backup.Config.Image, types.ImagePullOptions{})
if err != nil {
return "", err
}
}
// io.Copy(os.Stdout, reader)
resp, err := cli.ContainerCreate(ctx, backup.Config, &container.HostConfig{
PortBindings: backup.PortMap,
}, nil, name)
if err != nil {
return "", err
}
fmt.Println("Created Container with ID:", resp.ID)
for _, m := range backup.Mounts {
fmt.Printf("Old Mount (type %s) %s -> %s\n", m.Type, m.Source, m.Destination)
}
conf, err := cli.ContainerInspect(ctx, resp.ID)
if err != nil {
return "", err
}
for _, m := range conf.Mounts {
fmt.Printf("New Mount (type %s) %s -> %s\n", m.Type, m.Source, m.Destination)
}
return resp.ID, nil
}
func startContainer(id string) error {
fmt.Println("Starting container:", id[:12])
err := cli.ContainerStart(ctx, id, types.ContainerStartOptions{})
if err != nil {
return err
}
/*
statusCh, errCh := cli.ContainerWait(ctx, id, container.WaitConditionNotRunning)
select {
case err := <-errCh:
if err != nil {
return err
}
case <-statusCh:
}
out, err := cli.ContainerLogs(ctx, id, types.ContainerLogsOptions{ShowStdout: true})
if err != nil {
return err
}
io.Copy(os.Stdout, out)
*/
return nil
}
func init() {
restoreCmd.Flags().BoolVarP(&optStart, "start", "s", false, "start restored container")
RootCmd.AddCommand(restoreCmd)
}

@ -0,0 +1,11 @@
{
"luci-app-docker-backup": {
"description": "Grant UCI access for luci-app-docker-backup",
"read": {
"uci": [ "docker-backup" ]
},
"write": {
"uci": [ "docker-backup" ]
}
}
}
Loading…
Cancel
Save