first commit
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"> </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)
|
||||||
|
}
|
Binary file not shown.
@ -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…
Reference in New Issue