From efb52aa8067243ade72add80f45a58be9167d5dd Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sat, 21 Aug 2021 19:05:49 +0200 Subject: [PATCH 01/33] try porting docker related parts to golang --- Dockerfile | 15 +- go.mod | 26 ++ go.sum | 944 ++++++++++++++++++++++++++++++++++++++++++++++ src/entrypoint.sh | 13 +- src/main.go | 100 +++++ 5 files changed, 1082 insertions(+), 16 deletions(-) create mode 100644 go.mod create mode 100644 go.sum create mode 100644 src/main.go diff --git a/Dockerfile b/Dockerfile index 632ed20..883fe74 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,8 +2,11 @@ # SPDX-License-Identifier: MPL-2.0 FROM golang:1.17-alpine as builder -ARG MC_VERSION=RELEASE.2021-06-13T17-48-22Z -RUN go install -ldflags "-X github.com/minio/mc/cmd.ReleaseTag=$MC_VERSION" github.com/minio/mc@$MC_VERSION + +WORKDIR /app +COPY go.mod go.sum ./ +COPY src/main.go ./src/main.go +RUN go build -o backup src/main.go FROM alpine:3.14 @@ -13,11 +16,9 @@ RUN apk add --update ca-certificates docker openrc gnupg RUN update-ca-certificates RUN rc-update add docker boot -COPY --from=builder /go/bin/mc /usr/bin/mc -RUN mc --version +COPY --from=builder /app/backup /usr/bin/backup -COPY src/backup.sh src/entrypoint.sh /root/ -RUN chmod +x backup.sh && mv backup.sh /usr/bin/backup \ - && chmod +x entrypoint.sh +COPY src/entrypoint.sh /root/ +RUN chmod +x entrypoint.sh ENTRYPOINT ["/root/entrypoint.sh"] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1f25e3c --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module github.com/offen/docker-volume-backup + +go 1.17 + +require github.com/docker/docker v20.10.8+incompatible + +require ( + github.com/Microsoft/go-winio v0.4.17 // indirect + github.com/containerd/containerd v1.5.5 // indirect + github.com/docker/distribution v2.7.1+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.0 // indirect + github.com/joho/godotenv v1.3.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sirupsen/logrus v1.8.1 // indirect + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect + golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 // indirect + google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect + google.golang.org/grpc v1.33.2 // indirect + google.golang.org/protobuf v1.26.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..be3af4c --- /dev/null +++ b/go.sum @@ -0,0 +1,944 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +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/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= +github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= +github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17-0.20210324224401-5516f17a5958/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/go-winio v0.4.17 h1:iT12IBVClFevaf8PuVyi3UmZOVh4OqnaLxDTW2O6j3w= +github.com/Microsoft/go-winio v0.4.17/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= +github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= +github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= +github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= +github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= +github.com/Microsoft/hcsshim v0.8.18/go.mod h1:+w2gRZ5ReXQhFOrvSQeNfhrYB/dg3oDwTOcER2fw4I4= +github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= +github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= +github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= +github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= +github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= +github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/aufs v1.0.0/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= +github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/btrfs v1.0.0/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= +github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= +github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/cgroups v1.0.1/go.mod h1:0SJrPIenamHDcZhEcJMNBB85rHcUsw4f25ZfBiPYRkU= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= +github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= +github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= +github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= +github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= +github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= +github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= +github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= +github.com/containerd/containerd v1.5.5 h1:q1gxsZsGZ8ddVe98yO6pR21b5xQSMiR61lD0W96pgQo= +github.com/containerd/containerd v1.5.5/go.mod h1:oSTh0QpT1w6jYcGmbiSbxv9OSQYaa88mPyWIuU79zyo= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= +github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= +github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= +github.com/containerd/continuity v0.1.0/go.mod h1:ICJu0PwR54nI0yPEnJ6jcS+J7CZAUXrLh8lPo2knzsM= +github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/fifo v1.0.0/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= +github.com/containerd/go-cni v1.0.2/go.mod h1:nrNABBHzu0ZwCug9Ije8hL2xBCYh/pjfMb1aZGrrohk= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= +github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/go-runc v1.0.0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= +github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= +github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= +github.com/containerd/imgcrypt v1.1.1/go.mod h1:xpLnwiQmEUJPvQoAapeb2SNCxz7Xr6PJrXQb0Dpc4ms= +github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= +github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/nri v0.1.0/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= +github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= +github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= +github.com/containerd/typeurl v1.0.2/go.mod h1:9trJWW2sRlGub4wZJRTW83VtbOLS6hwcDZXTn6oPz9s= +github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= +github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= +github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v0.0.0-20210324211415-d5c4544f0433/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containerd/zfs v1.0.0/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= +github.com/containernetworking/plugins v0.9.1/go.mod h1:xP/idU2ldlzN6m4p5LmGiwRDjeJr6FLK6vuiUwoH7P8= +github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= +github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= +github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= +github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= +github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= +github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= +github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= +github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +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 v20.10.8+incompatible h1:RVqD337BgQicVCzYrrlhLDWhq6OAD2PJDUg2LsEUvKM= +github.com/docker/docker v20.10.8+incompatible/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-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +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/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= +github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I= +github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd h1:aY7OQNf2XqY/JQ6qREWamhI/81os/agb2BAGpcx5yWI= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +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/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= +github.com/opencontainers/runc v1.0.1/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= +github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= +github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= +github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= +github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= +github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= +github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= +github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201117170446-d9b008d0a637/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 h1:dXfMednGJh/SUUFjTLsWJz3P+TQt9qnR11GgeI3vWKs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a h1:pOwg4OoaRYScjmR4LlLgdtnyoHYTSAVhhqe5uPdpII8= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.2 h1:EQyQC3sa8M+p6Ulc8yy9SWSS2GVwyRc83gAbG8lrl4o= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= +k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ= +k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8= +k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc= +k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= +k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM= +k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q= +k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= +k8s.io/client-go v0.20.4/go.mod h1:LiMv25ND1gLUdBeYxBIwKpkSC5IsozMMmOOeSJboP+k= +k8s.io/client-go v0.20.6/go.mod h1:nNQMnOvEUEsOzRRFIIkdmYOjAZrC8bgq0ExboWSU1I0= +k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= +k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI= +k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= +k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= +k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/cri-api v0.20.6/go.mod h1:ew44AjNXwyn1s0U4xCKGodU7J1HzBeZ1MpGrpa5r8Yc= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.0.3/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/src/entrypoint.sh b/src/entrypoint.sh index 6d2f271..e2a3c59 100644 --- a/src/entrypoint.sh +++ b/src/entrypoint.sh @@ -9,7 +9,8 @@ set -e # Write cronjob env to file, fill in sensible defaults, and read them back in -cat < env.sh +mkdir -p /etc/backup +cat < /etc/backup.env BACKUP_SOURCES="${BACKUP_SOURCES:-/backup}" BACKUP_CRON_EXPRESSION="${BACKUP_CRON_EXPRESSION:-@daily}" BACKUP_FILENAME="${BACKUP_FILENAME:-backup-%Y-%m-%dT%H-%M-%S.tar.gz}" @@ -28,14 +29,8 @@ GPG_PASSPHRASE="${GPG_PASSPHRASE:-}" MC_GLOBAL_OPTIONS="${MC_GLOBAL_OPTIONS:-}" EOF -chmod a+x env.sh -source env.sh - -if [ ! -z "$AWS_ACCESS_KEY_ID" ] && [ ! -z "$AWS_SECRET_ACCESS_KEY" ]; then - mc $MC_GLOBAL_OPTIONS alias set backup-target \ - "$AWS_ENDPOINT_PROTO://$AWS_ENDPOINT" \ - "$AWS_ACCESS_KEY_ID" "$AWS_SECRET_ACCESS_KEY" -fi +chmod a+x /etc/backup.env +source /etc/backup.env # Add our cron entry, and direct stdout & stderr to Docker commands stdout echo "Installing cron.d entry with expression $BACKUP_CRON_EXPRESSION." diff --git a/src/main.go b/src/main.go new file mode 100644 index 0000000..931ed5a --- /dev/null +++ b/src/main.go @@ -0,0 +1,100 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/api/types/swarm" + "github.com/docker/docker/client" + "github.com/joho/godotenv" +) + +func main() { + if err := godotenv.Load("/etc/backup.env"); err != nil { + panic(err) + } + + ctx := context.Background() + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + panic(err) + } + + socketExists, err := fileExists("/var/run/docker.sock") + if err != nil { + panic(err) + } + + var containersToStop []types.Container + if socketExists { + containersToStop, err = cli.ContainerList(ctx, types.ContainerListOptions{ + Quiet: true, + Filters: filters.NewArgs(filters.KeyValuePair{ + Key: "label", + Value: fmt.Sprintf("docker-volume-backup.stop-during-backup=%s", os.Getenv("BACKUP_STOP_CONTAINER_LABEL")), + }), + }) + if err != nil { + panic(err) + } + fmt.Printf("Stopping %d containers\n", len(containersToStop)) + } + + if len(containersToStop) != 0 { + fmt.Println("Stopping containers") + for _, container := range containersToStop { + if err := cli.ContainerStop(ctx, container.ID, nil); err != nil { + panic(err) + } + } + } + + fmt.Println("Creating backup") + // TODO: Implement backup + + if len(containersToStop) != 0 { + fmt.Println("Starting containers/services back up") + servicesRequiringUpdate := map[string]struct{}{} + for _, container := range containersToStop { + if swarmServiceName, ok := container.Labels["com.docker.swarm.service.name"]; ok { + servicesRequiringUpdate[swarmServiceName] = struct{}{} + continue + } + if err := cli.ContainerStart(ctx, container.ID, types.ContainerStartOptions{}); err != nil { + panic(err) + } + } + + if len(servicesRequiringUpdate) != 0 { + services, _ := cli.ServiceList(ctx, types.ServiceListOptions{}) + for serviceName := range servicesRequiringUpdate { + var serviceMatch swarm.Service + for _, service := range services { + if service.Spec.Name == serviceName { + serviceMatch = service + break + } + } + if serviceMatch.ID == "" { + panic(fmt.Sprintf("Couldn't find service with name %s", serviceName)) + } + serviceMatch.Spec.TaskTemplate.ForceUpdate = 1 + cli.ServiceUpdate( + ctx, serviceMatch.ID, + serviceMatch.Version, serviceMatch.Spec, types.ServiceUpdateOptions{}, + ) + } + } + } +} + +func fileExists(location string) (bool, error) { + _, err := os.Stat(location) + if err != nil && err != os.ErrNotExist { + return false, err + } + return err == nil, nil +} From 8b110fd7897283394d7abf0cb786ad9285b564ce Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sat, 21 Aug 2021 19:26:42 +0200 Subject: [PATCH 02/33] scaffold script flow --- Dockerfile | 4 +- src/main.go | 215 ++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 159 insertions(+), 60 deletions(-) diff --git a/Dockerfile b/Dockerfile index 883fe74..65090fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,9 +12,7 @@ FROM alpine:3.14 WORKDIR /root -RUN apk add --update ca-certificates docker openrc gnupg -RUN update-ca-certificates -RUN rc-update add docker boot +RUN apk add --update ca-certificates COPY --from=builder /app/backup /usr/bin/backup diff --git a/src/main.go b/src/main.go index 931ed5a..6e86036 100644 --- a/src/main.go +++ b/src/main.go @@ -2,8 +2,10 @@ package main import ( "context" + "errors" "fmt" "os" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -13,82 +15,181 @@ import ( ) func main() { - if err := godotenv.Load("/etc/backup.env"); err != nil { + s := &script{} + defer func() { + for _, thunk := range s.cleanupTasks { + thunk() + } + }() + + s.lock() + defer s.unlock() + + if err := s.init(); err != nil { panic(err) } - ctx := context.Background() - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { + if err := s.stopContainers(); err != nil { panic(err) } + if err := s.takeBackup(); err != nil { + panic(err) + } + + if err := s.restartContainers(); err != nil { + panic(err) + } + + if err := s.encryptBackup(); err != nil { + panic(err) + } + + if err := s.copyBackup(); err != nil { + panic(err) + } + + if err := s.cleanBackup(); err != nil { + panic(err) + } + + if err := s.pruneBackups(); err != nil { + panic(err) + } +} + +type script struct { + ctx context.Context + cli *client.Client + stoppedContainers []types.Container + file string + cleanupTasks []func() +} + +func (s *script) lock() {} +func (s *script) unlock() {} + +func (s *script) init() error { + s.ctx = context.Background() + if timeout, ok := os.LookupEnv("BACKUP_TIMEOUT_DURATION"); ok { + d, err := time.ParseDuration(timeout) + if err != nil { + return fmt.Errorf("init: error parsing given timeout duration: %w", err) + } + withTimeout, cancelFunc := context.WithTimeout(context.Background(), d) + s.ctx = withTimeout + s.cleanupTasks = append(s.cleanupTasks, cancelFunc) + } + + if err := godotenv.Load("/etc/backup.env"); err != nil { + return fmt.Errorf("init: failed to load env file: %w", err) + } + socketExists, err := fileExists("/var/run/docker.sock") if err != nil { - panic(err) + return fmt.Errorf("init: error checking whether docker.sock is available: %w", err) } - - var containersToStop []types.Container if socketExists { - containersToStop, err = cli.ContainerList(ctx, types.ContainerListOptions{ - Quiet: true, - Filters: filters.NewArgs(filters.KeyValuePair{ - Key: "label", - Value: fmt.Sprintf("docker-volume-backup.stop-during-backup=%s", os.Getenv("BACKUP_STOP_CONTAINER_LABEL")), - }), - }) + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { + return fmt.Errorf("init: failied to create docker client") + } + s.cli = cli + } + return nil +} + +func (s *script) stopContainers() error { + if s.cli == nil { + return nil + } + stoppedContainers, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{ + Quiet: true, + Filters: filters.NewArgs(filters.KeyValuePair{ + Key: "label", + Value: fmt.Sprintf("docker-volume-backup.stop-during-backup=%s", os.Getenv("BACKUP_STOP_CONTAINER_LABEL")), + }), + }) + + s.stoppedContainers = stoppedContainers + if err != nil { + return fmt.Errorf("stopContainers: error querying for containers to stop: %w", err) + } + fmt.Printf("Stopping %d containers\n", len(s.stoppedContainers)) + + if len(s.stoppedContainers) != 0 { + fmt.Println("Stopping containers") + for _, container := range s.stoppedContainers { + if err := s.cli.ContainerStop(s.ctx, container.ID, nil); err != nil { + return fmt.Errorf("stopContainers: error stopping container %s: %w", container.Names[0], err) + } + } + } + return nil +} + +func (s *script) takeBackup() error { + return errors.New("takeBackup: not implemented yet") +} + +func (s *script) restartContainers() error { + fmt.Println("Starting containers/services back up") + servicesRequiringUpdate := map[string]struct{}{} + for _, container := range s.stoppedContainers { + if swarmServiceName, ok := container.Labels["com.docker.swarm.service.name"]; ok { + servicesRequiringUpdate[swarmServiceName] = struct{}{} + continue + } + if err := s.cli.ContainerStart(s.ctx, container.ID, types.ContainerStartOptions{}); err != nil { panic(err) } - fmt.Printf("Stopping %d containers\n", len(containersToStop)) } - if len(containersToStop) != 0 { - fmt.Println("Stopping containers") - for _, container := range containersToStop { - if err := cli.ContainerStop(ctx, container.ID, nil); err != nil { - panic(err) - } - } - } - - fmt.Println("Creating backup") - // TODO: Implement backup - - if len(containersToStop) != 0 { - fmt.Println("Starting containers/services back up") - servicesRequiringUpdate := map[string]struct{}{} - for _, container := range containersToStop { - if swarmServiceName, ok := container.Labels["com.docker.swarm.service.name"]; ok { - servicesRequiringUpdate[swarmServiceName] = struct{}{} - continue - } - if err := cli.ContainerStart(ctx, container.ID, types.ContainerStartOptions{}); err != nil { - panic(err) - } - } - - if len(servicesRequiringUpdate) != 0 { - services, _ := cli.ServiceList(ctx, types.ServiceListOptions{}) - for serviceName := range servicesRequiringUpdate { - var serviceMatch swarm.Service - for _, service := range services { - if service.Spec.Name == serviceName { - serviceMatch = service - break - } + if len(servicesRequiringUpdate) != 0 { + services, _ := s.cli.ServiceList(s.ctx, types.ServiceListOptions{}) + for serviceName := range servicesRequiringUpdate { + var serviceMatch swarm.Service + for _, service := range services { + if service.Spec.Name == serviceName { + serviceMatch = service + break } - if serviceMatch.ID == "" { - panic(fmt.Sprintf("Couldn't find service with name %s", serviceName)) - } - serviceMatch.Spec.TaskTemplate.ForceUpdate = 1 - cli.ServiceUpdate( - ctx, serviceMatch.ID, - serviceMatch.Version, serviceMatch.Spec, types.ServiceUpdateOptions{}, - ) } + if serviceMatch.ID == "" { + return fmt.Errorf("restartContainers: Couldn't find service with name %s", serviceName) + } + serviceMatch.Spec.TaskTemplate.ForceUpdate = 1 + s.cli.ServiceUpdate( + s.ctx, serviceMatch.ID, + serviceMatch.Version, serviceMatch.Spec, types.ServiceUpdateOptions{}, + ) } } + return nil +} + +func (s *script) encryptBackup() error { + _, ok := os.LookupEnv("GPG_PASSPHRASE") + if !ok { + return nil + } + return errors.New("encryptBackup: not implemented yet") +} + +func (s *script) copyBackup() error { + return errors.New("copyBackup: not implemented yet") +} + +func (s *script) cleanBackup() error { + return errors.New("cleanBackup: not implemented yet") +} + +func (s *script) pruneBackups() error { + _, ok := os.LookupEnv("BACKUP_RETENTION_DAYS") + if !ok { + return nil + } + return errors.New("pruneBackups: not implemented yet") } func fileExists(location string) (bool, error) { From 0c6ac0578934e0026e398c2acb9e68a7c1b3d182 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sat, 21 Aug 2021 21:26:27 +0200 Subject: [PATCH 03/33] implement copy to remote storage --- go.mod | 20 +++++++ go.sum | 29 ++++++++++ src/main.go | 152 ++++++++++++++++++++++++++++++++-------------------- 3 files changed, 144 insertions(+), 57 deletions(-) diff --git a/go.mod b/go.mod index 1f25e3c..7ecd836 100644 --- a/go.mod +++ b/go.mod @@ -10,17 +10,37 @@ require ( github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.0 // indirect + github.com/google/uuid v1.2.0 // indirect github.com/joho/godotenv v1.3.0 // indirect + github.com/json-iterator/go v1.1.10 // indirect + github.com/klauspost/cpuid v1.3.1 // indirect + github.com/minio/md5-simd v1.1.0 // indirect + github.com/minio/minio-go v6.0.14+incompatible + github.com/minio/minio-go/v7 v7.0.12 // indirect + github.com/minio/sha256-simd v0.1.1 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/rs/xid v1.2.1 // indirect github.com/sirupsen/logrus v1.8.1 // indirect + golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 // indirect + golang.org/x/text v0.3.4 // indirect google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect google.golang.org/grpc v1.33.2 // indirect google.golang.org/protobuf v1.26.0 // indirect + gopkg.in/ini.v1 v1.57.0 // indirect +) + +require ( + github.com/go-ini/ini v1.25.4 // indirect + github.com/walle/targz v0.0.0-20140417120357-57fe4206da5a // indirect ) diff --git a/go.sum b/go.sum index be3af4c..1399033 100644 --- a/go.sum +++ b/go.sum @@ -232,6 +232,7 @@ github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNE github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= @@ -253,6 +254,7 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.25.4 h1:Mujh4R/dH6YL8bxuISne3xX2+qcQ9p0IxKAP6ExWoUo= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -333,6 +335,7 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4 github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -372,6 +375,7 @@ github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqx github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -383,6 +387,9 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= +github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -405,7 +412,16 @@ github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vq github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4= +github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= +github.com/minio/minio-go v6.0.14+incompatible h1:fnV+GD28LeqdN6vT2XdGKW8Qe/IfjJDswNVuni6km9o= +github.com/minio/minio-go v6.0.14+incompatible/go.mod h1:7guKYtitv8dktvNUGrhzmNlA5wrAABTQXCoesZdFQO8= +github.com/minio/minio-go/v7 v7.0.12 h1:/4pxUdwn9w0QEryNkrrWaodIESPRX+NxpO0Q6hVdaAA= +github.com/minio/minio-go/v7 v7.0.12/go.mod h1:S23iSP5/gbMwtxeY5FM71R+TkAYyzEdoNEDDwpt8yWs= +github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= +github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= @@ -416,8 +432,10 @@ github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGq github.com/moby/term v0.0.0-20200312100748-672ec06f55cd h1:aY7OQNf2XqY/JQ6qREWamhI/81os/agb2BAGpcx5yWI= github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= @@ -509,6 +527,8 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -525,6 +545,7 @@ github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -568,6 +589,8 @@ github.com/vishvananda/netlink v1.1.1-0.20201029203352-d40f9887b852/go.mod h1:tw github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/walle/targz v0.0.0-20140417120357-57fe4206da5a h1:6cKSHLRphD9Fo1LJlISiulvgYCIafJ3QfKLimPYcAGc= +github.com/walle/targz v0.0.0-20140417120357-57fe4206da5a/go.mod h1:nccQrXCnc5SjsThFLmL7hYbtT/mHJcuolPifzY5vJqE= github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -605,6 +628,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -731,6 +756,7 @@ golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -746,6 +772,7 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 h1:dXfMednGJh/SUUFjTLsWJz3P+TQt9qnR11GgeI3vWKs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -881,6 +908,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= +gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= diff --git a/src/main.go b/src/main.go index 6e86036..2471f83 100644 --- a/src/main.go +++ b/src/main.go @@ -4,66 +4,42 @@ import ( "context" "errors" "fmt" + "io" "os" - "time" + "path" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" "github.com/joho/godotenv" + minio "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/walle/targz" ) func main() { s := &script{} - defer func() { - for _, thunk := range s.cleanupTasks { - thunk() - } - }() s.lock() defer s.unlock() - if err := s.init(); err != nil { - panic(err) - } - - if err := s.stopContainers(); err != nil { - panic(err) - } - - if err := s.takeBackup(); err != nil { - panic(err) - } - - if err := s.restartContainers(); err != nil { - panic(err) - } - - if err := s.encryptBackup(); err != nil { - panic(err) - } - - if err := s.copyBackup(); err != nil { - panic(err) - } - - if err := s.cleanBackup(); err != nil { - panic(err) - } - - if err := s.pruneBackups(); err != nil { - panic(err) - } + must(s.init)() + must(s.stopContainers)() + must(s.takeBackup)() + must(s.restartContainers)() + must(s.encryptBackup)() + must(s.copyBackup)() + must(s.cleanBackup)() + must(s.pruneBackups)() } type script struct { ctx context.Context cli *client.Client + mc *minio.Client stoppedContainers []types.Container file string - cleanupTasks []func() } func (s *script) lock() {} @@ -71,15 +47,6 @@ func (s *script) unlock() {} func (s *script) init() error { s.ctx = context.Background() - if timeout, ok := os.LookupEnv("BACKUP_TIMEOUT_DURATION"); ok { - d, err := time.ParseDuration(timeout) - if err != nil { - return fmt.Errorf("init: error parsing given timeout duration: %w", err) - } - withTimeout, cancelFunc := context.WithTimeout(context.Background(), d) - s.ctx = withTimeout - s.cleanupTasks = append(s.cleanupTasks, cancelFunc) - } if err := godotenv.Load("/etc/backup.env"); err != nil { return fmt.Errorf("init: failed to load env file: %w", err) @@ -96,6 +63,21 @@ func (s *script) init() error { } s.cli = cli } + + if bucket := os.Getenv("AWS_S3_BUCKET_NAME"); bucket != "" { + mc, err := minio.New(os.Getenv("AWS_ENDPOINT"), &minio.Options{ + Creds: credentials.NewStaticV4( + os.Getenv("AWS_ACCESS_KEY_ID"), + os.Getenv("AWS_SECRET_ACCESS_KEY"), + "", + ), + Secure: os.Getenv("AWS_ENDPOINT_PROTO") == "https", + }) + if err != nil { + return fmt.Errorf("init: error setting up minio client: %w", err) + } + s.mc = mc + } return nil } @@ -111,13 +93,12 @@ func (s *script) stopContainers() error { }), }) - s.stoppedContainers = stoppedContainers if err != nil { return fmt.Errorf("stopContainers: error querying for containers to stop: %w", err) } - fmt.Printf("Stopping %d containers\n", len(s.stoppedContainers)) + fmt.Printf("Stopping %d containers\n", len(stoppedContainers)) - if len(s.stoppedContainers) != 0 { + if len(stoppedContainers) != 0 { fmt.Println("Stopping containers") for _, container := range s.stoppedContainers { if err := s.cli.ContainerStop(s.ctx, container.ID, nil); err != nil { @@ -125,11 +106,21 @@ func (s *script) stopContainers() error { } } } + + s.stoppedContainers = stoppedContainers return nil } func (s *script) takeBackup() error { - return errors.New("takeBackup: not implemented yet") + file := os.Getenv("BACKUP_FILENAME") + if file == "" { + return errors.New("takeBackup: BACKUP_FILENAME not given") + } + s.file = file + if err := targz.Compress(os.Getenv("BACKUP_SOURCES"), s.file); err != nil { + return fmt.Errorf("takeBackup: error compressing backup folder: %w", err) + } + return nil } func (s *script) restartContainers() error { @@ -169,24 +160,43 @@ func (s *script) restartContainers() error { } func (s *script) encryptBackup() error { - _, ok := os.LookupEnv("GPG_PASSPHRASE") - if !ok { + key := os.Getenv("GPG_PASSPHRASE") + if key == "" { return nil } return errors.New("encryptBackup: not implemented yet") } func (s *script) copyBackup() error { - return errors.New("copyBackup: not implemented yet") + _, name := path.Split(s.file) + if bucket := os.Getenv("AWS_S3_BUCKET_NAME"); bucket != "" { + _, err := s.mc.FPutObject(s.ctx, bucket, name, s.file, minio.PutObjectOptions{ + ContentType: "application/tar+gzip", + }) + if err != nil { + return fmt.Errorf("copyBackup: error uploading backup to remote storage: %w", err) + } + } + if archive := os.Getenv("BACKUP_ARCHIVE"); archive != "" { + if _, err := os.Stat(archive); !os.IsNotExist(err) { + if err := copy(s.file, path.Join(archive, name)); err != nil { + return fmt.Errorf("copyBackup: error copying file to local archive: %w", err) + } + } + } + return nil } func (s *script) cleanBackup() error { - return errors.New("cleanBackup: not implemented yet") + if err := os.Remove(s.file); err != nil { + return fmt.Errorf("cleanBackup: error removing file: %w", err) + } + return nil } func (s *script) pruneBackups() error { - _, ok := os.LookupEnv("BACKUP_RETENTION_DAYS") - if !ok { + retention := os.Getenv("BACKUP_RETENTION_DAYS") + if retention == "" { return nil } return errors.New("pruneBackups: not implemented yet") @@ -199,3 +209,31 @@ func fileExists(location string) (bool, error) { } return err == nil, nil } + +func must(f func() error) func() { + return func() { + if err := f(); err != nil { + panic(err) + } + } +} + +func copy(src, dst string) error { + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, in) + if err != nil { + return err + } + return out.Close() +} From 4d9482a8b4555e57dfd2edb493c397c8c4af6835 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 22 Aug 2021 11:02:10 +0200 Subject: [PATCH 04/33] implement lock file to ensure backup runs mutually exclusive --- src/main.go | 83 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 68 insertions(+), 15 deletions(-) diff --git a/src/main.go b/src/main.go index 2471f83..93064d6 100644 --- a/src/main.go +++ b/src/main.go @@ -6,7 +6,10 @@ import ( "fmt" "io" "os" + "os/exec" "path" + "strings" + "time" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" @@ -21,17 +24,25 @@ import ( func main() { s := &script{} - s.lock() + must(s.lock)() defer s.unlock() must(s.init)() + fmt.Println("Successfully initialized resources.") must(s.stopContainers)() + fmt.Println("Successfully stopped containers.") must(s.takeBackup)() + fmt.Println("Successfully took backup.") must(s.restartContainers)() + fmt.Println("Successfully restarted containers.") must(s.encryptBackup)() + fmt.Println("Successfully encrypted backup.") must(s.copyBackup)() + fmt.Println("Successfully copied backup.") must(s.cleanBackup)() - must(s.pruneBackups)() + fmt.Println("Successfully cleaned local backup.") + must(s.pruneOldBackups)() + fmt.Println("Successfully pruned old backup.") } type script struct { @@ -39,11 +50,28 @@ type script struct { cli *client.Client mc *minio.Client stoppedContainers []types.Container + releaseLock func() error file string } -func (s *script) lock() {} -func (s *script) unlock() {} +func (s *script) lock() error { + lf, err := os.OpenFile("/var/dockervolumebackup.lock", os.O_CREATE, os.ModeAppend) + if err != nil { + return fmt.Errorf("lock: error opening lock file: %w", err) + } + s.releaseLock = lf.Close + return nil +} + +func (s *script) unlock() error { + if err := s.releaseLock(); err != nil { + return fmt.Errorf("unlock: error releasing file lock: %w", err) + } + if err := os.Remove("/var/dockervolumebackup.lock"); err != nil { + return fmt.Errorf("unlock: error removing lock file: %w", err) + } + return nil +} func (s *script) init() error { s.ctx = context.Background() @@ -85,7 +113,14 @@ func (s *script) stopContainers() error { if s.cli == nil { return nil } - stoppedContainers, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{ + allContainers, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{ + Quiet: true, + }) + if err != nil { + return fmt.Errorf("stopContainers: error querying for containers: %w", err) + } + + containersToStop, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{ Quiet: true, Filters: filters.NewArgs(filters.KeyValuePair{ Key: "label", @@ -96,9 +131,9 @@ func (s *script) stopContainers() error { if err != nil { return fmt.Errorf("stopContainers: error querying for containers to stop: %w", err) } - fmt.Printf("Stopping %d containers\n", len(stoppedContainers)) + fmt.Printf("Stopping %d out of %d running containers\n", len(containersToStop), len(allContainers)) - if len(stoppedContainers) != 0 { + if len(containersToStop) != 0 { fmt.Println("Stopping containers") for _, container := range s.stoppedContainers { if err := s.cli.ContainerStop(s.ctx, container.ID, nil); err != nil { @@ -107,15 +142,21 @@ func (s *script) stopContainers() error { } } - s.stoppedContainers = stoppedContainers + s.stoppedContainers = containersToStop return nil } func (s *script) takeBackup() error { - file := os.Getenv("BACKUP_FILENAME") - if file == "" { + if os.Getenv("BACKUP_FILENAME") == "" { return errors.New("takeBackup: BACKUP_FILENAME not given") } + + outBytes, err := exec.Command("date", fmt.Sprintf("+%s", os.Getenv("BACKUP_FILENAME"))).Output() + if err != nil { + return fmt.Errorf("takeBackup: error formatting filename template: %w", err) + } + file := fmt.Sprintf("/tmp/%s", strings.TrimSpace(string(outBytes))) + s.file = file if err := targz.Compress(os.Getenv("BACKUP_SOURCES"), s.file); err != nil { return fmt.Errorf("takeBackup: error compressing backup folder: %w", err) @@ -124,7 +165,6 @@ func (s *script) takeBackup() error { } func (s *script) restartContainers() error { - fmt.Println("Starting containers/services back up") servicesRequiringUpdate := map[string]struct{}{} for _, container := range s.stoppedContainers { if swarmServiceName, ok := container.Labels["com.docker.swarm.service.name"]; ok { @@ -156,6 +196,8 @@ func (s *script) restartContainers() error { ) } } + + s.stoppedContainers = []types.Container{} return nil } @@ -194,17 +236,28 @@ func (s *script) cleanBackup() error { return nil } -func (s *script) pruneBackups() error { +func (s *script) pruneOldBackups() error { retention := os.Getenv("BACKUP_RETENTION_DAYS") if retention == "" { return nil } - return errors.New("pruneBackups: not implemented yet") + + sleepFor, err := time.ParseDuration(os.Getenv("BACKUP_PRUNING_LEEWAY")) + if err != nil { + return fmt.Errorf("pruneBackups: error parsing given leeway value: %w", err) + } + time.Sleep(sleepFor) + + if bucket := os.Getenv("AWS_S3_BUCKET_NAME"); bucket != "" { + } + if archive := os.Getenv("BACKUP_ARCHIVE"); archive != "" { + } + return nil } func fileExists(location string) (bool, error) { _, err := os.Stat(location) - if err != nil && err != os.ErrNotExist { + if err != nil && !os.IsNotExist(err) { return false, err } return err == nil, nil @@ -229,10 +282,10 @@ func copy(src, dst string) error { if err != nil { return err } - defer out.Close() _, err = io.Copy(out, in) if err != nil { + out.Close() return err } return out.Close() From 78e4e3813bcf67964cffabf44202ec1e7b9853cb Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 22 Aug 2021 14:00:21 +0200 Subject: [PATCH 05/33] implement deletion of local backups --- src/main.go | 38 +++++++++++++++++++++++++++++++-- test/compose/docker-compose.yml | 4 ++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/main.go b/src/main.go index 93064d6..dfc68a6 100644 --- a/src/main.go +++ b/src/main.go @@ -8,6 +8,8 @@ import ( "os" "os/exec" "path" + "path/filepath" + "strconv" "strings" "time" @@ -134,7 +136,6 @@ func (s *script) stopContainers() error { fmt.Printf("Stopping %d out of %d running containers\n", len(containersToStop), len(allContainers)) if len(containersToStop) != 0 { - fmt.Println("Stopping containers") for _, container := range s.stoppedContainers { if err := s.cli.ContainerStop(s.ctx, container.ID, nil); err != nil { return fmt.Errorf("stopContainers: error stopping container %s: %w", container.Names[0], err) @@ -241,16 +242,49 @@ func (s *script) pruneOldBackups() error { if retention == "" { return nil } - + retentionDays, err := strconv.Atoi(retention) + if err != nil { + return fmt.Errorf("pruneOldBackups: error parsing BACKUP_RETENTION_DAYS as int: %w", err) + } sleepFor, err := time.ParseDuration(os.Getenv("BACKUP_PRUNING_LEEWAY")) if err != nil { return fmt.Errorf("pruneBackups: error parsing given leeway value: %w", err) } time.Sleep(sleepFor) + deadline := time.Now().AddDate(0, 0, -retentionDays) + if bucket := os.Getenv("AWS_S3_BUCKET_NAME"); bucket != "" { } if archive := os.Getenv("BACKUP_ARCHIVE"); archive != "" { + matches, err := filepath.Glob( + path.Join(archive, fmt.Sprintf("%s%s", os.Getenv("BACKUP_PRUNING_PREFIX"), "*")), + ) + if err != nil { + return fmt.Errorf("pruneOldBackups: error looking up matching files: %w", err) + } + + var candidates []os.FileInfo + for _, match := range matches { + fi, err := os.Stat(match) + if err != nil { + return fmt.Errorf("pruneOldBackups: error calling stat on file %s: %w", match, err) + } + + if fi.ModTime().Before(deadline) { + candidates = append(candidates, fi) + } + } + + if len(candidates) != len(matches) { + for _, candidate := range candidates { + if err := os.Remove(candidate.Name()); err != nil { + return fmt.Errorf("pruneOldBackups: error deleting file %s: %w", candidate.Name(), err) + } + } + } else if len(matches) != 0 { + fmt.Println("Refusing to delete all backups. Check your configuration.") + } } return nil } diff --git a/test/compose/docker-compose.yml b/test/compose/docker-compose.yml index 1646ce6..eaef3d5 100644 --- a/test/compose/docker-compose.yml +++ b/test/compose/docker-compose.yml @@ -23,12 +23,12 @@ services: AWS_ENDPOINT: minio:9000 AWS_ENDPOINT_PROTO: http AWS_S3_BUCKET_NAME: backup - BACKUP_FILENAME: test.tar.gz + # BACKUP_FILENAME: test.tar.gz BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ? BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7} BACKUP_PRUNING_LEEWAY: 5s - BACKUP_PRUNING_PREFIX: test GPG_PASSPHRASE: 1234secret + # BACKUP_PRUNING_PREFIX: test volumes: - ./local:/archive - app_data:/backup/app_data:ro From f2739b583e83d77d186f6f5f08e0b0f3ec494e0f Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 22 Aug 2021 14:44:33 +0200 Subject: [PATCH 06/33] add gpg encryption --- go.mod | 4 ++-- go.sum | 4 ++++ src/main.go | 41 ++++++++++++++++++++++++++++++--- test/compose/docker-compose.yml | 2 +- 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 7ecd836..f58a015 100644 --- a/go.mod +++ b/go.mod @@ -30,9 +30,9 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/rs/xid v1.2.1 // indirect github.com/sirupsen/logrus v1.8.1 // indirect - golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect + golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect - golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 // indirect + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect golang.org/x/text v0.3.4 // indirect google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a // indirect google.golang.org/grpc v1.33.2 // indirect diff --git a/go.sum b/go.sum index 1399033..c61d375 100644 --- a/go.sum +++ b/go.sum @@ -631,6 +631,8 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -772,6 +774,8 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 h1:dXfMednGJh/SUUFjTLsWJz3P+TQt9qnR11GgeI3vWKs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/src/main.go b/src/main.go index dfc68a6..456a56f 100644 --- a/src/main.go +++ b/src/main.go @@ -1,10 +1,12 @@ package main import ( + "bytes" "context" "errors" "fmt" "io" + "io/ioutil" "os" "os/exec" "path" @@ -21,6 +23,7 @@ import ( minio "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/walle/targz" + "golang.org/x/crypto/openpgp" ) func main() { @@ -203,11 +206,43 @@ func (s *script) restartContainers() error { } func (s *script) encryptBackup() error { - key := os.Getenv("GPG_PASSPHRASE") - if key == "" { + passphrase := os.Getenv("GPG_PASSPHRASE") + if passphrase == "" { return nil } - return errors.New("encryptBackup: not implemented yet") + + buf := bytes.NewBuffer(nil) + _, name := path.Split(s.file) + pt, err := openpgp.SymmetricallyEncrypt(buf, []byte(passphrase), &openpgp.FileHints{ + IsBinary: true, + FileName: name, + }, nil) + if err != nil { + return fmt.Errorf("encryptBackup: error encrypting backup file: %w", err) + } + + unencrypted, err := ioutil.ReadFile(s.file) + if err != nil { + pt.Close() + return fmt.Errorf("encryptBackup: error reading unencrypted backup file: %w", err) + } + _, err = pt.Write(unencrypted) + if err != nil { + pt.Close() + return fmt.Errorf("encryptBackup: error writing backup contents: %w", err) + } + pt.Close() + + gpgFile := fmt.Sprintf("%s.gpg", s.file) + if err := ioutil.WriteFile(gpgFile, buf.Bytes(), os.ModeAppend); err != nil { + return fmt.Errorf("encryptBackup: error writing encrypted version of backup: %w", err) + } + + if err := os.Remove(s.file); err != nil { + return fmt.Errorf("encryptBackup: error removing unencrpyted backup: %w", err) + } + s.file = gpgFile + return nil } func (s *script) copyBackup() error { diff --git a/test/compose/docker-compose.yml b/test/compose/docker-compose.yml index eaef3d5..26dbbb9 100644 --- a/test/compose/docker-compose.yml +++ b/test/compose/docker-compose.yml @@ -23,7 +23,7 @@ services: AWS_ENDPOINT: minio:9000 AWS_ENDPOINT_PROTO: http AWS_S3_BUCKET_NAME: backup - # BACKUP_FILENAME: test.tar.gz + BACKUP_FILENAME: test.tar.gz BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ? BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7} BACKUP_PRUNING_LEEWAY: 5s From 8c99ec0bdf9c449183e26da5c91586f82ebd195e Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 22 Aug 2021 15:04:44 +0200 Subject: [PATCH 07/33] implement pruning from remote storage --- src/main.go | 81 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 12 deletions(-) diff --git a/src/main.go b/src/main.go index 456a56f..5e1887f 100644 --- a/src/main.go +++ b/src/main.go @@ -141,7 +141,11 @@ func (s *script) stopContainers() error { if len(containersToStop) != 0 { for _, container := range s.stoppedContainers { if err := s.cli.ContainerStop(s.ctx, container.ID, nil); err != nil { - return fmt.Errorf("stopContainers: error stopping container %s: %w", container.Names[0], err) + return fmt.Errorf( + "stopContainers: error stopping container %s: %w", + container.Names[0], + err, + ) } } } @@ -255,6 +259,7 @@ func (s *script) copyBackup() error { return fmt.Errorf("copyBackup: error uploading backup to remote storage: %w", err) } } + if archive := os.Getenv("BACKUP_ARCHIVE"); archive != "" { if _, err := os.Stat(archive); !os.IsNotExist(err) { if err := copy(s.file, path.Join(archive, name)); err != nil { @@ -290,34 +295,86 @@ func (s *script) pruneOldBackups() error { deadline := time.Now().AddDate(0, 0, -retentionDays) if bucket := os.Getenv("AWS_S3_BUCKET_NAME"); bucket != "" { + candidates := s.mc.ListObjects(s.ctx, bucket, minio.ListObjectsOptions{ + WithMetadata: true, + Prefix: os.Getenv("BACKUP_PRUNING_PREFIX"), + }) + + var matches []minio.ObjectInfo + for candidate := range candidates { + if candidate.LastModified.Before(deadline) { + matches = append(matches, candidate) + } + } + + if len(matches) != len(candidates) { + objectsCh := make(chan minio.ObjectInfo) + go func() { + for _, candidate := range matches { + objectsCh <- candidate + } + }() + errChan := s.mc.RemoveObjects(s.ctx, bucket, objectsCh, minio.RemoveObjectsOptions{}) + var errors []error + for result := range errChan { + if result.Err != nil { + errors = append(errors, result.Err) + } + } + + if len(errors) != 0 { + return fmt.Errorf( + "pruneOldBackups: %d errors removing files from remote storage: %w", + len(errors), + errors[0], + ) + } + } else if len(candidates) != 0 { + fmt.Println("Refusing to delete all backups. Check your configuration.") + } } + if archive := os.Getenv("BACKUP_ARCHIVE"); archive != "" { - matches, err := filepath.Glob( + candidates, err := filepath.Glob( path.Join(archive, fmt.Sprintf("%s%s", os.Getenv("BACKUP_PRUNING_PREFIX"), "*")), ) if err != nil { - return fmt.Errorf("pruneOldBackups: error looking up matching files: %w", err) + return fmt.Errorf( + "pruneOldBackups: error looking up matching files, starting with: %w", err, + ) } - var candidates []os.FileInfo - for _, match := range matches { - fi, err := os.Stat(match) + var matches []os.FileInfo + for _, candidate := range candidates { + fi, err := os.Stat(candidate) if err != nil { - return fmt.Errorf("pruneOldBackups: error calling stat on file %s: %w", match, err) + return fmt.Errorf( + "pruneOldBackups: error calling stat on file %s: %w", + candidate, + err, + ) } if fi.ModTime().Before(deadline) { - candidates = append(candidates, fi) + matches = append(matches, fi) } } - if len(candidates) != len(matches) { - for _, candidate := range candidates { + if len(matches) != len(candidates) { + var errors []error + for _, candidate := range matches { if err := os.Remove(candidate.Name()); err != nil { - return fmt.Errorf("pruneOldBackups: error deleting file %s: %w", candidate.Name(), err) + errors = append(errors, err) } } - } else if len(matches) != 0 { + if len(errors) != 0 { + return fmt.Errorf( + "pruneOldBackups: %d errors deleting local files, starting with: %w", + len(errors), + errors[0], + ) + } + } else if len(candidates) != 0 { fmt.Println("Refusing to delete all backups. Check your configuration.") } } From 67499d776cc642e3bbaaec89e1f435be3ae3540b Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 22 Aug 2021 15:52:47 +0200 Subject: [PATCH 08/33] refactor deferred cleanup actions to always run --- src/main.go | 244 +++++++++++++++++++++++++++++----------------------- 1 file changed, 138 insertions(+), 106 deletions(-) diff --git a/src/main.go b/src/main.go index 5e1887f..7997927 100644 --- a/src/main.go +++ b/src/main.go @@ -27,19 +27,21 @@ import ( ) func main() { - s := &script{} + unlock, err := lock() + if err != nil { + panic(err) + } + defer unlock() - must(s.lock)() - defer s.unlock() + s := &script{} must(s.init)() fmt.Println("Successfully initialized resources.") - must(s.stopContainers)() - fmt.Println("Successfully stopped containers.") - must(s.takeBackup)() + err = s.stopContainersAndRun(s.takeBackup) + if err != nil { + panic(err) + } fmt.Println("Successfully took backup.") - must(s.restartContainers)() - fmt.Println("Successfully restarted containers.") must(s.encryptBackup)() fmt.Println("Successfully encrypted backup.") must(s.copyBackup)() @@ -47,37 +49,38 @@ func main() { must(s.cleanBackup)() fmt.Println("Successfully cleaned local backup.") must(s.pruneOldBackups)() - fmt.Println("Successfully pruned old backup.") + fmt.Println("Successfully pruned old backups.") } type script struct { - ctx context.Context - cli *client.Client - mc *minio.Client - stoppedContainers []types.Container - releaseLock func() error - file string + ctx context.Context + cli *client.Client + mc *minio.Client + file string + bucket string } -func (s *script) lock() error { - lf, err := os.OpenFile("/var/dockervolumebackup.lock", os.O_CREATE, os.ModeAppend) +// lock opens a lock file without releasing it and returns a function that +// can be called once the lock shall be released again. +func lock() (func() error, error) { + lockfile := "/var/dockervolumebackup.lock" + lf, err := os.OpenFile(lockfile, os.O_CREATE, os.ModeAppend) if err != nil { - return fmt.Errorf("lock: error opening lock file: %w", err) + return nil, fmt.Errorf("lock: error opening lock file: %w", err) } - s.releaseLock = lf.Close - return nil -} - -func (s *script) unlock() error { - if err := s.releaseLock(); err != nil { - return fmt.Errorf("unlock: error releasing file lock: %w", err) - } - if err := os.Remove("/var/dockervolumebackup.lock"); err != nil { - return fmt.Errorf("unlock: error removing lock file: %w", err) - } - return nil + return func() error { + if err := lf.Close(); err != nil { + return fmt.Errorf("lock: error releasing file lock: %w", err) + } + if err := os.Remove(lockfile); err != nil { + return fmt.Errorf("lock: error removing lock file: %w", err) + } + return nil + }, nil } +// init creates all resources needed for the script to perform actions against +// remote resources like the Docker engine or remote storage locations. func (s *script) init() error { s.ctx = context.Background() @@ -85,11 +88,8 @@ func (s *script) init() error { return fmt.Errorf("init: failed to load env file: %w", err) } - socketExists, err := fileExists("/var/run/docker.sock") - if err != nil { - return fmt.Errorf("init: error checking whether docker.sock is available: %w", err) - } - if socketExists { + _, err := os.Stat("/var/run/docker.sock") + if !os.IsNotExist(err) { cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return fmt.Errorf("init: failied to create docker client") @@ -98,6 +98,7 @@ func (s *script) init() error { } if bucket := os.Getenv("AWS_S3_BUCKET_NAME"); bucket != "" { + s.bucket = bucket mc, err := minio.New(os.Getenv("AWS_ENDPOINT"), &minio.Options{ Creds: credentials.NewStaticV4( os.Getenv("AWS_ACCESS_KEY_ID"), @@ -111,10 +112,18 @@ func (s *script) init() error { } s.mc = mc } + file := os.Getenv("BACKUP_FILENAME") + if file == "" { + return errors.New("init: BACKUP_FILENAME not given") + } + s.file = path.Join("/tmp", file) return nil } -func (s *script) stopContainers() error { +// stopContainersAndRun stops all Docker containers that are marked as to being +// stopped during the backup and runs the given thunk. After returning, it makes +// sure containers are being restarted if required. +func (s *script) stopContainersAndRun(thunk func() error) error { if s.cli == nil { return nil } @@ -122,93 +131,117 @@ func (s *script) stopContainers() error { Quiet: true, }) if err != nil { - return fmt.Errorf("stopContainers: error querying for containers: %w", err) + return fmt.Errorf("stopContainersAndRun: error querying for containers: %w", err) } containersToStop, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{ Quiet: true, Filters: filters.NewArgs(filters.KeyValuePair{ - Key: "label", - Value: fmt.Sprintf("docker-volume-backup.stop-during-backup=%s", os.Getenv("BACKUP_STOP_CONTAINER_LABEL")), + Key: "label", + Value: fmt.Sprintf( + "docker-volume-backup.stop-during-backup=%s", + os.Getenv("BACKUP_STOP_CONTAINER_LABEL"), + ), }), }) if err != nil { - return fmt.Errorf("stopContainers: error querying for containers to stop: %w", err) + return fmt.Errorf("stopContainersAndRun: error querying for containers to stop: %w", err) } fmt.Printf("Stopping %d out of %d running containers\n", len(containersToStop), len(allContainers)) + var stoppedContainers []types.Container + var errors []error if len(containersToStop) != 0 { - for _, container := range s.stoppedContainers { + for _, container := range containersToStop { if err := s.cli.ContainerStop(s.ctx, container.ID, nil); err != nil { - return fmt.Errorf( - "stopContainers: error stopping container %s: %w", - container.Names[0], - err, - ) + errors = append(errors, err) + } else { + stoppedContainers = append(stoppedContainers, container) } } } - s.stoppedContainers = containersToStop - return nil -} + defer func() error { + servicesRequiringUpdate := map[string]struct{}{} -func (s *script) takeBackup() error { - if os.Getenv("BACKUP_FILENAME") == "" { - return errors.New("takeBackup: BACKUP_FILENAME not given") + var restartErrors []error + for _, container := range stoppedContainers { + if swarmServiceName, ok := container.Labels["com.docker.swarm.service.name"]; ok { + servicesRequiringUpdate[swarmServiceName] = struct{}{} + continue + } + if err := s.cli.ContainerStart(s.ctx, container.ID, types.ContainerStartOptions{}); err != nil { + restartErrors = append(restartErrors, err) + } + } + + if len(servicesRequiringUpdate) != 0 { + services, _ := s.cli.ServiceList(s.ctx, types.ServiceListOptions{}) + for serviceName := range servicesRequiringUpdate { + var serviceMatch swarm.Service + for _, service := range services { + if service.Spec.Name == serviceName { + serviceMatch = service + break + } + } + if serviceMatch.ID == "" { + return fmt.Errorf("stopContainersAndRun: Couldn't find service with name %s", serviceName) + } + serviceMatch.Spec.TaskTemplate.ForceUpdate = 1 + _, err := s.cli.ServiceUpdate( + s.ctx, serviceMatch.ID, + serviceMatch.Version, serviceMatch.Spec, types.ServiceUpdateOptions{}, + ) + if err != nil { + restartErrors = append(restartErrors, err) + } + } + } + + if len(restartErrors) != 0 { + return fmt.Errorf( + "stopContainersAndRun: %d error(s) restarting containers and services: %w", + len(restartErrors), + err, + ) + } + return nil + }() + + var stopErr error + if len(errors) != 0 { + stopErr = fmt.Errorf( + "stopContainersAndRun: %d errors stopping containers: %w", + len(errors), + err, + ) + } + if stopErr != nil { + return stopErr } - outBytes, err := exec.Command("date", fmt.Sprintf("+%s", os.Getenv("BACKUP_FILENAME"))).Output() + return thunk() +} + +// takeBackup creates a tar archive of the configured backup location and +// saves it to disk. +func (s *script) takeBackup() error { + outBytes, err := exec.Command("date", fmt.Sprintf("+%s", s.file)).Output() if err != nil { return fmt.Errorf("takeBackup: error formatting filename template: %w", err) } - file := fmt.Sprintf("/tmp/%s", strings.TrimSpace(string(outBytes))) - - s.file = file + s.file = strings.TrimSpace(string(outBytes)) if err := targz.Compress(os.Getenv("BACKUP_SOURCES"), s.file); err != nil { return fmt.Errorf("takeBackup: error compressing backup folder: %w", err) } return nil } -func (s *script) restartContainers() error { - servicesRequiringUpdate := map[string]struct{}{} - for _, container := range s.stoppedContainers { - if swarmServiceName, ok := container.Labels["com.docker.swarm.service.name"]; ok { - servicesRequiringUpdate[swarmServiceName] = struct{}{} - continue - } - if err := s.cli.ContainerStart(s.ctx, container.ID, types.ContainerStartOptions{}); err != nil { - panic(err) - } - } - - if len(servicesRequiringUpdate) != 0 { - services, _ := s.cli.ServiceList(s.ctx, types.ServiceListOptions{}) - for serviceName := range servicesRequiringUpdate { - var serviceMatch swarm.Service - for _, service := range services { - if service.Spec.Name == serviceName { - serviceMatch = service - break - } - } - if serviceMatch.ID == "" { - return fmt.Errorf("restartContainers: Couldn't find service with name %s", serviceName) - } - serviceMatch.Spec.TaskTemplate.ForceUpdate = 1 - s.cli.ServiceUpdate( - s.ctx, serviceMatch.ID, - serviceMatch.Version, serviceMatch.Spec, types.ServiceUpdateOptions{}, - ) - } - } - - s.stoppedContainers = []types.Container{} - return nil -} - +// encryptBackup encrypts the backup file using PGP and the configured passphrase. +// In case no passphrase is given it returns early, leaving the backup file +// untouched. func (s *script) encryptBackup() error { passphrase := os.Getenv("GPG_PASSPHRASE") if passphrase == "" { @@ -249,10 +282,12 @@ func (s *script) encryptBackup() error { return nil } +// copyBackup makes sure the backup file is copied to both local and remote locations +// as per the given configuration. func (s *script) copyBackup() error { _, name := path.Split(s.file) - if bucket := os.Getenv("AWS_S3_BUCKET_NAME"); bucket != "" { - _, err := s.mc.FPutObject(s.ctx, bucket, name, s.file, minio.PutObjectOptions{ + if s.bucket != "" { + _, err := s.mc.FPutObject(s.ctx, s.bucket, name, s.file, minio.PutObjectOptions{ ContentType: "application/tar+gzip", }) if err != nil { @@ -270,6 +305,7 @@ func (s *script) copyBackup() error { return nil } +// cleanBackup removes the backup file from disk. func (s *script) cleanBackup() error { if err := os.Remove(s.file); err != nil { return fmt.Errorf("cleanBackup: error removing file: %w", err) @@ -277,6 +313,9 @@ func (s *script) cleanBackup() error { return nil } +// pruneOldBackups rotates away backups from local and remote storages using +// the given configuration. In case the given configuration would delete all +// backups, it does nothing instead. func (s *script) pruneOldBackups() error { retention := os.Getenv("BACKUP_RETENTION_DAYS") if retention == "" { @@ -294,8 +333,8 @@ func (s *script) pruneOldBackups() error { deadline := time.Now().AddDate(0, 0, -retentionDays) - if bucket := os.Getenv("AWS_S3_BUCKET_NAME"); bucket != "" { - candidates := s.mc.ListObjects(s.ctx, bucket, minio.ListObjectsOptions{ + if s.bucket != "" { + candidates := s.mc.ListObjects(s.ctx, s.bucket, minio.ListObjectsOptions{ WithMetadata: true, Prefix: os.Getenv("BACKUP_PRUNING_PREFIX"), }) @@ -313,8 +352,9 @@ func (s *script) pruneOldBackups() error { for _, candidate := range matches { objectsCh <- candidate } + close(objectsCh) }() - errChan := s.mc.RemoveObjects(s.ctx, bucket, objectsCh, minio.RemoveObjectsOptions{}) + errChan := s.mc.RemoveObjects(s.ctx, s.bucket, objectsCh, minio.RemoveObjectsOptions{}) var errors []error for result := range errChan { if result.Err != nil { @@ -381,14 +421,6 @@ func (s *script) pruneOldBackups() error { return nil } -func fileExists(location string) (bool, error) { - _, err := os.Stat(location) - if err != nil && !os.IsNotExist(err) { - return false, err - } - return err == nil, nil -} - func must(f func() error) func() { return func() { if err := f(); err != nil { From 435583168b6f1e99c23b65954898fd918416eb91 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 22 Aug 2021 16:41:06 +0200 Subject: [PATCH 09/33] add logging --- go.mod | 17 +++++------- go.sum | 9 +++--- src/main.go | 80 ++++++++++++++++++++++++++++++++++++----------------- 3 files changed, 65 insertions(+), 41 deletions(-) diff --git a/go.mod b/go.mod index f58a015..771befb 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,13 @@ module github.com/offen/docker-volume-backup go 1.17 -require github.com/docker/docker v20.10.8+incompatible +require ( + github.com/docker/docker v20.10.8+incompatible + github.com/joho/godotenv v1.3.0 + github.com/minio/minio-go/v7 v7.0.12 + github.com/walle/targz v0.0.0-20140417120357-57fe4206da5a + golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 +) require ( github.com/Microsoft/go-winio v0.4.17 // indirect @@ -14,12 +20,9 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.0 // indirect github.com/google/uuid v1.2.0 // indirect - github.com/joho/godotenv v1.3.0 // indirect github.com/json-iterator/go v1.1.10 // indirect github.com/klauspost/cpuid v1.3.1 // indirect github.com/minio/md5-simd v1.1.0 // indirect - github.com/minio/minio-go v6.0.14+incompatible - github.com/minio/minio-go/v7 v7.0.12 // indirect github.com/minio/sha256-simd v0.1.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -30,7 +33,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/rs/xid v1.2.1 // indirect github.com/sirupsen/logrus v1.8.1 // indirect - golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect golang.org/x/text v0.3.4 // indirect @@ -39,8 +41,3 @@ require ( google.golang.org/protobuf v1.26.0 // indirect gopkg.in/ini.v1 v1.57.0 // indirect ) - -require ( - github.com/go-ini/ini v1.25.4 // indirect - github.com/walle/targz v0.0.0-20140417120357-57fe4206da5a // indirect -) diff --git a/go.sum b/go.sum index c61d375..6e3eade 100644 --- a/go.sum +++ b/go.sum @@ -254,7 +254,6 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-ini/ini v1.25.4 h1:Mujh4R/dH6YL8bxuISne3xX2+qcQ9p0IxKAP6ExWoUo= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -340,6 +339,7 @@ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.2 h1:zoNxOV7WjqXptQOVngLmcSQgXmgk4NMz1HibBchjl/I= @@ -379,6 +379,7 @@ github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -414,8 +415,6 @@ github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182aff github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4= github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= -github.com/minio/minio-go v6.0.14+incompatible h1:fnV+GD28LeqdN6vT2XdGKW8Qe/IfjJDswNVuni6km9o= -github.com/minio/minio-go v6.0.14+incompatible/go.mod h1:7guKYtitv8dktvNUGrhzmNlA5wrAABTQXCoesZdFQO8= github.com/minio/minio-go/v7 v7.0.12 h1:/4pxUdwn9w0QEryNkrrWaodIESPRX+NxpO0Q6hVdaAA= github.com/minio/minio-go/v7 v7.0.12/go.mod h1:S23iSP5/gbMwtxeY5FM71R+TkAYyzEdoNEDDwpt8yWs= github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= @@ -543,8 +542,10 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= @@ -629,7 +630,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -772,7 +772,6 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 h1:dXfMednGJh/SUUFjTLsWJz3P+TQt9qnR11GgeI3vWKs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/src/main.go b/src/main.go index 7997927..148ad3d 100644 --- a/src/main.go +++ b/src/main.go @@ -22,6 +22,7 @@ import ( "github.com/joho/godotenv" minio "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/sirupsen/logrus" "github.com/walle/targz" "golang.org/x/crypto/openpgp" ) @@ -36,28 +37,24 @@ func main() { s := &script{} must(s.init)() - fmt.Println("Successfully initialized resources.") err = s.stopContainersAndRun(s.takeBackup) if err != nil { panic(err) } - fmt.Println("Successfully took backup.") must(s.encryptBackup)() - fmt.Println("Successfully encrypted backup.") must(s.copyBackup)() - fmt.Println("Successfully copied backup.") must(s.cleanBackup)() - fmt.Println("Successfully cleaned local backup.") must(s.pruneOldBackups)() - fmt.Println("Successfully pruned old backups.") } type script struct { - ctx context.Context - cli *client.Client - mc *minio.Client - file string - bucket string + ctx context.Context + cli *client.Client + mc *minio.Client + logger *logrus.Logger + file string + bucket string + archive string } // lock opens a lock file without releasing it and returns a function that @@ -83,6 +80,8 @@ func lock() (func() error, error) { // remote resources like the Docker engine or remote storage locations. func (s *script) init() error { s.ctx = context.Background() + s.logger = logrus.New() + s.logger.SetOutput(os.Stdout) if err := godotenv.Load("/etc/backup.env"); err != nil { return fmt.Errorf("init: failed to load env file: %w", err) @@ -92,7 +91,7 @@ func (s *script) init() error { if !os.IsNotExist(err) { cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { - return fmt.Errorf("init: failied to create docker client") + return fmt.Errorf("init: failed to create docker client") } s.cli = cli } @@ -112,11 +111,13 @@ func (s *script) init() error { } s.mc = mc } + file := os.Getenv("BACKUP_FILENAME") if file == "" { return errors.New("init: BACKUP_FILENAME not given") } s.file = path.Join("/tmp", file) + s.archive = os.Getenv("BACKUP_ARCHIVE") return nil } @@ -148,7 +149,7 @@ func (s *script) stopContainersAndRun(thunk func() error) error { if err != nil { return fmt.Errorf("stopContainersAndRun: error querying for containers to stop: %w", err) } - fmt.Printf("Stopping %d out of %d running containers\n", len(containersToStop), len(allContainers)) + s.logger.Infof("Stopping %d out of %d running containers\n", len(containersToStop), len(allContainers)) var stoppedContainers []types.Container var errors []error @@ -207,6 +208,7 @@ func (s *script) stopContainersAndRun(thunk func() error) error { err, ) } + s.logger.Infof("Successfully restarted %d containers.", len(stoppedContainers)) return nil }() @@ -236,6 +238,7 @@ func (s *script) takeBackup() error { if err := targz.Compress(os.Getenv("BACKUP_SOURCES"), s.file); err != nil { return fmt.Errorf("takeBackup: error compressing backup folder: %w", err) } + s.logger.Infof("Successfully created backup from %s at %s", os.Getenv("BACKUP_SOURCES"), s.file) return nil } @@ -279,6 +282,7 @@ func (s *script) encryptBackup() error { return fmt.Errorf("encryptBackup: error removing unencrpyted backup: %w", err) } s.file = gpgFile + s.logger.Info("Successfully encrypted backup using given passphrase.") return nil } @@ -293,14 +297,16 @@ func (s *script) copyBackup() error { if err != nil { return fmt.Errorf("copyBackup: error uploading backup to remote storage: %w", err) } + s.logger.Infof("Successfully uploaded backup %s to bucket %s", s.file, s.bucket) } - if archive := os.Getenv("BACKUP_ARCHIVE"); archive != "" { - if _, err := os.Stat(archive); !os.IsNotExist(err) { - if err := copy(s.file, path.Join(archive, name)); err != nil { + if s.archive != "" { + if _, err := os.Stat(s.archive); !os.IsNotExist(err) { + if err := copy(s.file, path.Join(s.archive, name)); err != nil { return fmt.Errorf("copyBackup: error copying file to local archive: %w", err) } } + s.logger.Infof("Successfully stored copy of backup %s in local archive %s", s.file, s.archive) } return nil } @@ -310,6 +316,7 @@ func (s *script) cleanBackup() error { if err := os.Remove(s.file); err != nil { return fmt.Errorf("cleanBackup: error removing file: %w", err) } + s.logger.Info("Successfully cleaned local backup.") return nil } @@ -329,8 +336,10 @@ func (s *script) pruneOldBackups() error { if err != nil { return fmt.Errorf("pruneBackups: error parsing given leeway value: %w", err) } + s.logger.Infof("Sleeping for %s before pruning backups.", os.Getenv("BACKUP_PRUNING_LEEWAY")) time.Sleep(sleepFor) + s.logger.Infof("Trying to prune backups older than %d days now.", retentionDays) deadline := time.Now().AddDate(0, 0, -retentionDays) if s.bucket != "" { @@ -340,17 +349,22 @@ func (s *script) pruneOldBackups() error { }) var matches []minio.ObjectInfo + var lenCandidates int for candidate := range candidates { + lenCandidates++ + if candidate.Err != nil { + return fmt.Errorf("pruneOldBackups: error looking up candidates from remote storage: %w", candidate.Err) + } if candidate.LastModified.Before(deadline) { matches = append(matches, candidate) } } - if len(matches) != len(candidates) { + if len(matches) != 0 && len(matches) != lenCandidates { objectsCh := make(chan minio.ObjectInfo) go func() { - for _, candidate := range matches { - objectsCh <- candidate + for _, match := range matches { + objectsCh <- match } close(objectsCh) }() @@ -369,14 +383,21 @@ func (s *script) pruneOldBackups() error { errors[0], ) } - } else if len(candidates) != 0 { - fmt.Println("Refusing to delete all backups. Check your configuration.") + s.logger.Infof( + "Successfully pruned %d out of %d remote backups as their age exceeded the configured retention period.", + len(matches), + lenCandidates, + ) + } else if len(matches) != 0 && len(matches) == lenCandidates { + s.logger.Warnf("The current configuration would delete all %d remote backups. Refusing to do so.", len(matches)) + } else { + s.logger.Info("No remote backups were pruned.") } } - if archive := os.Getenv("BACKUP_ARCHIVE"); archive != "" { + if s.archive != "" { candidates, err := filepath.Glob( - path.Join(archive, fmt.Sprintf("%s%s", os.Getenv("BACKUP_PRUNING_PREFIX"), "*")), + path.Join(s.archive, fmt.Sprintf("%s*", os.Getenv("BACKUP_PRUNING_PREFIX"))), ) if err != nil { return fmt.Errorf( @@ -400,7 +421,7 @@ func (s *script) pruneOldBackups() error { } } - if len(matches) != len(candidates) { + if len(matches) != 0 && len(matches) != len(candidates) { var errors []error for _, candidate := range matches { if err := os.Remove(candidate.Name()); err != nil { @@ -414,8 +435,15 @@ func (s *script) pruneOldBackups() error { errors[0], ) } - } else if len(candidates) != 0 { - fmt.Println("Refusing to delete all backups. Check your configuration.") + s.logger.Infof( + "Successfully pruned %d out of %d local backups as their age exceeded the configured retention period.", + len(matches), + len(candidates), + ) + } else if len(matches) != 0 && len(matches) == len(candidates) { + s.logger.Warnf("The current configuration would delete all %d local backups. Refusing to do so.", len(matches)) + } else { + s.logger.Info("No local backups were pruned.") } } return nil From da9458724f118263fa81b6bb94a85763d04644c0 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 22 Aug 2021 18:07:32 +0200 Subject: [PATCH 10/33] adapt repo layout to go --- Dockerfile | 6 +- {src => cmd/backup}/main.go | 18 +-- src/entrypoint.sh => entrypoint.sh | 0 src/backup.sh | 181 ----------------------------- 4 files changed, 13 insertions(+), 192 deletions(-) rename {src => cmd/backup}/main.go (96%) rename src/entrypoint.sh => entrypoint.sh (100%) delete mode 100644 src/backup.sh diff --git a/Dockerfile b/Dockerfile index 65090fb..8e6a603 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,8 @@ FROM golang:1.17-alpine as builder WORKDIR /app COPY go.mod go.sum ./ -COPY src/main.go ./src/main.go -RUN go build -o backup src/main.go +COPY cmd/backup/main.go ./cmd/backup/main.go +RUN go build -o backup cmd/backup/main.go FROM alpine:3.14 @@ -16,7 +16,7 @@ RUN apk add --update ca-certificates COPY --from=builder /app/backup /usr/bin/backup -COPY src/entrypoint.sh /root/ +COPY ./entrypoint.sh /root/ RUN chmod +x entrypoint.sh ENTRYPOINT ["/root/entrypoint.sh"] diff --git a/src/main.go b/cmd/backup/main.go similarity index 96% rename from src/main.go rename to cmd/backup/main.go index 148ad3d..f7dc2b4 100644 --- a/src/main.go +++ b/cmd/backup/main.go @@ -1,3 +1,6 @@ +// Copyright 2021 - Offen Authors +// SPDX-License-Identifier: MPL-2.0 + package main import ( @@ -28,7 +31,7 @@ import ( ) func main() { - unlock, err := lock() + unlock, err := lock("/var/dockervolumebackup.lock") if err != nil { panic(err) } @@ -57,10 +60,9 @@ type script struct { archive string } -// lock opens a lock file without releasing it and returns a function that -// can be called once the lock shall be released again. -func lock() (func() error, error) { - lockfile := "/var/dockervolumebackup.lock" +// lock opens a lockfile, keeping it open until the caller invokes the returned +// release func. +func lock(lockfile string) (func() error, error) { lf, err := os.OpenFile(lockfile, os.O_CREATE, os.ModeAppend) if err != nil { return nil, fmt.Errorf("lock: error opening lock file: %w", err) @@ -316,7 +318,7 @@ func (s *script) cleanBackup() error { if err := os.Remove(s.file); err != nil { return fmt.Errorf("cleanBackup: error removing file: %w", err) } - s.logger.Info("Successfully cleaned local backup.") + s.logger.Info("Successfully cleaned up local artifacts.") return nil } @@ -391,7 +393,7 @@ func (s *script) pruneOldBackups() error { } else if len(matches) != 0 && len(matches) == lenCandidates { s.logger.Warnf("The current configuration would delete all %d remote backups. Refusing to do so.", len(matches)) } else { - s.logger.Info("No remote backups were pruned.") + s.logger.Infof("None of %d remote backups were pruned.", lenCandidates) } } @@ -443,7 +445,7 @@ func (s *script) pruneOldBackups() error { } else if len(matches) != 0 && len(matches) == len(candidates) { s.logger.Warnf("The current configuration would delete all %d local backups. Refusing to do so.", len(matches)) } else { - s.logger.Info("No local backups were pruned.") + s.logger.Infof("None of %d local backups were pruned.", len(candidates)) } } return nil diff --git a/src/entrypoint.sh b/entrypoint.sh similarity index 100% rename from src/entrypoint.sh rename to entrypoint.sh diff --git a/src/backup.sh b/src/backup.sh deleted file mode 100644 index 08b600f..0000000 --- a/src/backup.sh +++ /dev/null @@ -1,181 +0,0 @@ -#!/bin/sh - -# Copyright 2021 - Offen Authors -# SPDX-License-Identifier: MPL-2.0 - -# Portions of this file are taken from github.com/futurice/docker-volume-backup -# See NOTICE for information about authors and licensing. - -source env.sh - -function info { - echo -e "\n[INFO] $1\n" -} - -info "Preparing backup" -DOCKER_SOCK="/var/run/docker.sock" - -if [ -S "$DOCKER_SOCK" ]; then - TEMPFILE="$(mktemp)" - docker ps -q \ - --filter "label=docker-volume-backup.stop-during-backup=$BACKUP_STOP_CONTAINER_LABEL" \ - > "$TEMPFILE" - CONTAINERS_TO_STOP="$(cat $TEMPFILE | tr '\n' ' ')" - CONTAINERS_TO_STOP_TOTAL="$(cat $TEMPFILE | wc -l)" - CONTAINERS_TOTAL="$(docker ps -q | wc -l)" - rm "$TEMPFILE" - echo "$CONTAINERS_TOTAL containers running on host in total." - echo "$CONTAINERS_TO_STOP_TOTAL containers marked to be stopped during backup." -else - CONTAINERS_TO_STOP_TOTAL="0" - CONTAINERS_TOTAL="0" - echo "Cannot access \"$DOCKER_SOCK\", won't look for containers to stop." -fi - -if [ "$CONTAINERS_TO_STOP_TOTAL" != "0" ]; then - info "Stopping containers" - docker stop $CONTAINERS_TO_STOP -fi - -info "Creating backup" -BACKUP_FILENAME="$(date +"$BACKUP_FILENAME")" -tar -czvf "$BACKUP_FILENAME" $BACKUP_SOURCES # allow the var to expand, in case we have multiple sources - -if [ ! -z "$GPG_PASSPHRASE" ]; then - info "Encrypting backup" - gpg --symmetric --cipher-algo aes256 --batch --passphrase "$GPG_PASSPHRASE" \ - -o "${BACKUP_FILENAME}.gpg" $BACKUP_FILENAME - rm $BACKUP_FILENAME - BACKUP_FILENAME="${BACKUP_FILENAME}.gpg" -fi - -if [ "$CONTAINERS_TO_STOP_TOTAL" != "0" ]; then - info "Starting containers/services back up" - # The container might be part of a stack when running in swarm mode, so - # its parent service needs to be restarted instead once backup is finished. - SERVICES_REQUIRING_UPDATE="" - for CONTAINER_ID in $CONTAINERS_TO_STOP; do - SWARM_SERVICE_NAME=$( - docker inspect \ - --format "{{ index .Config.Labels \"com.docker.swarm.service.name\" }}" \ - $CONTAINER_ID - ) - if [ -z "$SWARM_SERVICE_NAME" ]; then - echo "Restarting $(docker start $CONTAINER_ID)" - else - echo "Removing $(docker rm $CONTAINER_ID)" - # Multiple containers might belong to the same service, so they will - # be restarted only after all names are known. - SERVICES_REQUIRING_UPDATE="${SERVICES_REQUIRING_UPDATE} ${SWARM_SERVICE_NAME}" - fi - done - - if [ -n "$SERVICES_REQUIRING_UPDATE" ]; then - for SERVICE_NAME in $(echo -n "$SERVICES_REQUIRING_UPDATE" | tr ' ' '\n' | sort -u); do - docker service update --force $SERVICE_NAME - done - fi -fi - -copy_backup () { - mc cp $MC_GLOBAL_OPTIONS "$BACKUP_FILENAME" "$1" -} - -if [ ! -z "$AWS_S3_BUCKET_NAME" ]; then - info "Uploading backup to remote storage" - echo "Will upload to bucket \"$AWS_S3_BUCKET_NAME\"." - copy_backup "backup-target/$AWS_S3_BUCKET_NAME" - echo "Upload finished." -fi - -if [ -d "$BACKUP_ARCHIVE" ]; then - info "Copying backup to local archive" - echo "Will copy to \"$BACKUP_ARCHIVE\"." - copy_backup "$BACKUP_ARCHIVE" - echo "Finished copying." -fi - -if [ -f "$BACKUP_FILENAME" ]; then - info "Cleaning up" - rm -vf "$BACKUP_FILENAME" -fi - -info "Backup finished" -echo "Will wait for next scheduled backup." - -probe_expired () { - local target=$1 - local is_local=$2 - if [ -z "$is_local" ]; then - if [ ! -z "$BACKUP_PRUNING_PREFIX" ]; then - target="${target}/${BACKUP_PRUNING_PREFIX}" - fi - mc rm $MC_GLOBAL_OPTIONS --fake --recursive --force \ - --older-than "${BACKUP_RETENTION_DAYS}d" \ - "$target" - else - find $target -name "${BACKUP_PRUNING_PREFIX:-*}" -type f -mtime "+${BACKUP_RETENTION_DAYS}" - fi -} - -probe_all () { - local target=$1 - local is_local=$2 - if [ -z "$is_local" ]; then - if [ ! -z "$BACKUP_PRUNING_PREFIX" ]; then - target="${target}/${BACKUP_PRUNING_PREFIX}" - fi - mc ls $MC_GLOBAL_OPTIONS "$target" - else - find $target -name "${BACKUP_PRUNING_PREFIX:-*}" -type f - fi -} - -delete () { - local target=$1 - local is_local=$2 - if [ -z "$is_local" ]; then - if [ ! -z "$BACKUP_PRUNING_PREFIX" ]; then - target="${target}/${BACKUP_PRUNING_PREFIX}" - fi - mc rm $MC_GLOBAL_OPTIONS --recursive --force \ - --older-than "${BACKUP_RETENTION_DAYS}d" \ - "$target" - else - find $target -name "${BACKUP_PRUNING_PREFIX:-*}" -type f -mtime "+${BACKUP_RETENTION_DAYS}" -delete - fi -} - -prune () { - local target=$1 - local is_local=$2 - rule_applies_to=$(probe_expired "$target" "$is_local" | wc -l) - if [ "$rule_applies_to" == "0" ]; then - echo "No backups found older than the configured retention period of ${BACKUP_RETENTION_DAYS} days." - echo "Doing nothing." - else - total=$(probe_all "$target" "$is_local" | wc -l) - - if [ "$rule_applies_to" == "$total" ]; then - echo "Using a retention of ${BACKUP_RETENTION_DAYS} days would prune all currently existing backups, will not continue." - echo "If this is what you want, please remove files manually instead of using this script." - else - delete "$target" "$is_local" - echo "Successfully pruned ${rule_applies_to} backups older than ${BACKUP_RETENTION_DAYS} days." - fi - fi -} - -if [ ! -z "$BACKUP_RETENTION_DAYS" ]; then - info "Pruning old backups" - echo "Sleeping ${BACKUP_PRUNING_LEEWAY} before checking eligibility." - sleep "$BACKUP_PRUNING_LEEWAY" - if [ ! -z "$AWS_S3_BUCKET_NAME" ]; then - info "Pruning old backups from remote storage" - prune "backup-target/$AWS_S3_BUCKET_NAME" - fi - if [ -d "$BACKUP_ARCHIVE" ]; then - info "Pruning old backups from local archive" - prune "$BACKUP_ARCHIVE" "local" - fi -fi From 188c14c00f7630f4e6b29ccd227775765edb0760 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 22 Aug 2021 19:26:34 +0200 Subject: [PATCH 11/33] add insecure option, update docs --- .editorconfig | 21 +++++++++++++++++++++ README.md | 26 +++++++++++++------------- cmd/backup/main.go | 2 +- entrypoint.sh | 3 +-- 4 files changed, 36 insertions(+), 16 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..28252b2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,21 @@ +# Copyright 2020 - Offen Authors +# SPDX-License-Identifier: Apache-2.0 + +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.go] +indent_style = tab diff --git a/README.md b/README.md index 5b14b02..df032b5 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ Backup targets, schedule and retention are configured in environment variables: # template expression. BACKUP_CRON_EXPRESSION="0 2 * * *" +# Format verbs will be replaced as in the `date` command. Omitting them +# will result in the same filename for every backup run, which means previous +# versions will be overwritten. BACKUP_FILENAME="backup-%Y-%m-%dT%H-%M-%S.tar.gz" ########### BACKUP STORAGE @@ -39,6 +42,12 @@ AWS_S3_BUCKET_NAME="" # AWS_ENDPOINT_PROTO="https" +# Setting this variable to any value will disable verification of +# SSL certificates. You shouldn't use this unless you use self-signed +# certificates for your remote storage backend. + +# AWS_ENDPOINT_INSECURE="true" + # In addition to backing up you can also store backups locally. Pass in # a local path to store your backups here if needed. You likely want to # mount a local folder or Docker volume into that location when running @@ -65,10 +74,10 @@ AWS_S3_BUCKET_NAME="" # In case the duration a backup takes fluctuates noticeably in your setup # you can adjust this setting to make sure there are no race conditions -# between the backup finishing and the pruning not deleting backups that +# between the backup finishing and the rotation not deleting backups that # sit on the very edge of the time window. Set this value to a duration # that is expected to be bigger than the maximum difference of backups. -# Valid values have a suffix of (s)econds, (m)inutes, (h)ours, or (d)ays. +# Valid values have a suffix of (s)econds, (m)inutes or (h)ours. # BACKUP_PRUNING_LEEWAY="10m" @@ -96,15 +105,6 @@ AWS_S3_BUCKET_NAME="" # override this default by specifying a different value here. # BACKUP_STOP_CONTAINER_LABEL="service1" - -########### MINIO CLIENT CONFIGURATION - -# Pass these additional flags to all MinIO client `mc` invocations. -# This can be used for example to pass `--insecure` when using self -# signed certificates, or passing `--debug` to gain insights on -# unexpected behavior. - -# MC_GLOBAL_OPTIONS="" ``` ## Example in a docker-compose setup @@ -177,8 +177,8 @@ docker exec backup This image is heavily inspired by the `futurice/docker-volume-backup`. We decided to publish this image as a simpler and more lightweight alternative because of the following requirements: - The original image is based on `ubuntu`, making it very heavy. This version is roughly 1/3 in compressed size. -- This image makes use of the MinIO client `mc` instead of the full blown AWS CLI for uploading backups. -- The original image proposed to handle backup rotation through AWS S3 lifecycle policies. This image adds the option to rotate old backups through the same script so this functionality can also be offered for non-AWS storage backends like MinIO. +- The original image uses a shell script, when this is written in Go. +- The original image proposed to handle backup rotation through AWS S3 lifecycle policies. This image adds the option to rotate away old backups through the same command so this functionality can also be offered for non-AWS storage backends like MinIO. Local backups can also be pruned once they reach a certain age. - InfluxDB specific functionality was removed. - `arm64` and `arm/v7` architectures are supported. - Docker in Swarm mode is supported. diff --git a/cmd/backup/main.go b/cmd/backup/main.go index f7dc2b4..f479a21 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -106,7 +106,7 @@ func (s *script) init() error { os.Getenv("AWS_SECRET_ACCESS_KEY"), "", ), - Secure: os.Getenv("AWS_ENDPOINT_PROTO") == "https", + Secure: os.Getenv("AWS_ENDPOINT_INSECURE") == "" && os.Getenv("AWS_ENDPOINT_PROTO") == "https", }) if err != nil { return fmt.Errorf("init: error setting up minio client: %w", err) diff --git a/entrypoint.sh b/entrypoint.sh index e2a3c59..2469ee7 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -24,10 +24,9 @@ BACKUP_STOP_CONTAINER_LABEL="${BACKUP_STOP_CONTAINER_LABEL:-true}" AWS_S3_BUCKET_NAME="${AWS_S3_BUCKET_NAME:-}" AWS_ENDPOINT="${AWS_ENDPOINT:-s3.amazonaws.com}" AWS_ENDPOINT_PROTO="${AWS_ENDPOINT_PROTO:-https}" +AWS_ENDPOINT_INSECURE="${AWS_ENDPOINT_INSECURE:-}" GPG_PASSPHRASE="${GPG_PASSPHRASE:-}" - -MC_GLOBAL_OPTIONS="${MC_GLOBAL_OPTIONS:-}" EOF chmod a+x /etc/backup.env source /etc/backup.env From d195e8967ff71ac3406799ab6149699296dc46f2 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 22 Aug 2021 19:37:48 +0200 Subject: [PATCH 12/33] improve logging messages --- .editorconfig | 3 -- cmd/backup/main.go | 106 +++++++++++++++++++++++++-------------------- 2 files changed, 59 insertions(+), 50 deletions(-) diff --git a/.editorconfig b/.editorconfig index 28252b2..40963ff 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,3 @@ -# Copyright 2020 - Offen Authors -# SPDX-License-Identifier: Apache-2.0 - # EditorConfig is awesome: http://EditorConfig.org # top-most EditorConfig file diff --git a/cmd/backup/main.go b/cmd/backup/main.go index f479a21..f6bfdc4 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -31,41 +31,37 @@ import ( ) func main() { - unlock, err := lock("/var/dockervolumebackup.lock") - if err != nil { - panic(err) - } + unlock := lock("/var/dockervolumebackup.lock") defer unlock() s := &script{} - - must(s.init)() - err = s.stopContainersAndRun(s.takeBackup) - if err != nil { - panic(err) - } - must(s.encryptBackup)() - must(s.copyBackup)() - must(s.cleanBackup)() - must(s.pruneOldBackups)() + s.must(s.init()) + s.must(s.stopContainersAndRun(s.takeBackup)) + s.must(s.encryptBackup()) + s.must(s.copyBackup()) + s.must(s.cleanBackup()) + s.must(s.pruneOldBackups()) } type script struct { - ctx context.Context - cli *client.Client - mc *minio.Client - logger *logrus.Logger - file string - bucket string - archive string + ctx context.Context + cli *client.Client + mc *minio.Client + logger *logrus.Logger + file string + bucket string + archive string + sources string + passphrase string } -// lock opens a lockfile, keeping it open until the caller invokes the returned -// release func. -func lock(lockfile string) (func() error, error) { +// lock opens a lockfile at the given location, keeping it locked until the +// caller invokes the returned release func. When invoked while the file is +// still locked the function panics. +func lock(lockfile string) func() error { lf, err := os.OpenFile(lockfile, os.O_CREATE, os.ModeAppend) if err != nil { - return nil, fmt.Errorf("lock: error opening lock file: %w", err) + panic(err) } return func() error { if err := lf.Close(); err != nil { @@ -75,7 +71,7 @@ func lock(lockfile string) (func() error, error) { return fmt.Errorf("lock: error removing lock file: %w", err) } return nil - }, nil + } } // init creates all resources needed for the script to perform actions against @@ -120,6 +116,8 @@ func (s *script) init() error { } s.file = path.Join("/tmp", file) s.archive = os.Getenv("BACKUP_ARCHIVE") + s.sources = os.Getenv("BACKUP_SOURCES") + s.passphrase = os.Getenv("GPG_PASSPHRASE") return nil } @@ -137,21 +135,27 @@ func (s *script) stopContainersAndRun(thunk func() error) error { return fmt.Errorf("stopContainersAndRun: error querying for containers: %w", err) } + containerLabel := fmt.Sprintf( + "docker-volume-backup.stop-during-backup=%s", + os.Getenv("BACKUP_STOP_CONTAINER_LABEL"), + ) containersToStop, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{ Quiet: true, Filters: filters.NewArgs(filters.KeyValuePair{ - Key: "label", - Value: fmt.Sprintf( - "docker-volume-backup.stop-during-backup=%s", - os.Getenv("BACKUP_STOP_CONTAINER_LABEL"), - ), + Key: "label", + Value: containerLabel, }), }) if err != nil { return fmt.Errorf("stopContainersAndRun: error querying for containers to stop: %w", err) } - s.logger.Infof("Stopping %d out of %d running containers\n", len(containersToStop), len(allContainers)) + s.logger.Infof( + "Stopping %d containers labeled `%s` out of %d running containers.", + len(containersToStop), + containerLabel, + len(allContainers), + ) var stoppedContainers []types.Container var errors []error @@ -237,10 +241,10 @@ func (s *script) takeBackup() error { return fmt.Errorf("takeBackup: error formatting filename template: %w", err) } s.file = strings.TrimSpace(string(outBytes)) - if err := targz.Compress(os.Getenv("BACKUP_SOURCES"), s.file); err != nil { + if err := targz.Compress(s.sources, s.file); err != nil { return fmt.Errorf("takeBackup: error compressing backup folder: %w", err) } - s.logger.Infof("Successfully created backup from %s at %s", os.Getenv("BACKUP_SOURCES"), s.file) + s.logger.Infof("Successfully created backup of `%s` at `%s`.", s.sources, s.file) return nil } @@ -248,14 +252,13 @@ func (s *script) takeBackup() error { // In case no passphrase is given it returns early, leaving the backup file // untouched. func (s *script) encryptBackup() error { - passphrase := os.Getenv("GPG_PASSPHRASE") - if passphrase == "" { + if s.passphrase == "" { return nil } buf := bytes.NewBuffer(nil) _, name := path.Split(s.file) - pt, err := openpgp.SymmetricallyEncrypt(buf, []byte(passphrase), &openpgp.FileHints{ + pt, err := openpgp.SymmetricallyEncrypt(buf, []byte(s.passphrase), &openpgp.FileHints{ IsBinary: true, FileName: name, }, nil) @@ -284,7 +287,7 @@ func (s *script) encryptBackup() error { return fmt.Errorf("encryptBackup: error removing unencrpyted backup: %w", err) } s.file = gpgFile - s.logger.Info("Successfully encrypted backup using given passphrase.") + s.logger.Infof("Successfully encrypted backup using given passphrase, saving as `%s`.", s.file) return nil } @@ -299,7 +302,7 @@ func (s *script) copyBackup() error { if err != nil { return fmt.Errorf("copyBackup: error uploading backup to remote storage: %w", err) } - s.logger.Infof("Successfully uploaded backup %s to bucket %s", s.file, s.bucket) + s.logger.Infof("Successfully uploaded a copy of backup `%s` to bucket `%s`", s.file, s.bucket) } if s.archive != "" { @@ -308,7 +311,7 @@ func (s *script) copyBackup() error { return fmt.Errorf("copyBackup: error copying file to local archive: %w", err) } } - s.logger.Infof("Successfully stored copy of backup %s in local archive %s", s.file, s.archive) + s.logger.Infof("Successfully stored copy of backup `%s` in local archive `%s`", s.file, s.archive) } return nil } @@ -334,11 +337,12 @@ func (s *script) pruneOldBackups() error { if err != nil { return fmt.Errorf("pruneOldBackups: error parsing BACKUP_RETENTION_DAYS as int: %w", err) } - sleepFor, err := time.ParseDuration(os.Getenv("BACKUP_PRUNING_LEEWAY")) + leeway := os.Getenv("BACKUP_PRUNING_LEEWAY") + sleepFor, err := time.ParseDuration(leeway) if err != nil { return fmt.Errorf("pruneBackups: error parsing given leeway value: %w", err) } - s.logger.Infof("Sleeping for %s before pruning backups.", os.Getenv("BACKUP_PRUNING_LEEWAY")) + s.logger.Infof("Sleeping for %s before pruning backups.", leeway) time.Sleep(sleepFor) s.logger.Infof("Trying to prune backups older than %d days now.", retentionDays) @@ -391,7 +395,10 @@ func (s *script) pruneOldBackups() error { lenCandidates, ) } else if len(matches) != 0 && len(matches) == lenCandidates { - s.logger.Warnf("The current configuration would delete all %d remote backups. Refusing to do so.", len(matches)) + s.logger.Warnf( + "The current configuration would delete all %d remote backup copies. Refusing to do so, please check your configuration.", + len(matches), + ) } else { s.logger.Infof("None of %d remote backups were pruned.", lenCandidates) } @@ -443,7 +450,10 @@ func (s *script) pruneOldBackups() error { len(candidates), ) } else if len(matches) != 0 && len(matches) == len(candidates) { - s.logger.Warnf("The current configuration would delete all %d local backups. Refusing to do so.", len(matches)) + s.logger.Warnf( + "The current configuration would delete all %d local backup copies. Refusing to do so, please check your configuration.", + len(matches), + ) } else { s.logger.Infof("None of %d local backups were pruned.", len(candidates)) } @@ -451,11 +461,13 @@ func (s *script) pruneOldBackups() error { return nil } -func must(f func() error) func() { - return func() { - if err := f(); err != nil { +func (s *script) must(err error) { + if err != nil { + if s.logger == nil { panic(err) } + s.logger.Errorf("Fatal error running backup: %s", err) + os.Exit(1) } } From 935de92f2ef4787f6e515ae8d25f5eca98f1a88f Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 22 Aug 2021 20:16:24 +0200 Subject: [PATCH 13/33] only tag proper releases as latest --- .circleci/config.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 512687a..29bbf63 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -43,10 +43,13 @@ jobs: docker context create docker-volume-backup docker buildx create docker-volume-backup --name docker-volume-backup --use docker buildx inspect --bootstrap + tag_args="-t offen/docker-volume-backup:$CIRCLE_TAG" + if [[ "$CIRCLE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + # prerelease tags like `v2.0.0-alpha.1` should not be released as `latest` + tag_args="$tag_args -t offen/docker-volume-backup:latest" + fi docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 \ - -t offen/docker-volume-backup:$CIRCLE_TAG \ - -t offen/docker-volume-backup:latest \ - . --push + $tag_args . --push workflows: version: 2 From 7244725c5b96101e2f70c0b77ce50ca0d8170525 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 22 Aug 2021 20:42:25 +0200 Subject: [PATCH 14/33] fix location of success message for having created local backup --- README.md | 4 ++-- cmd/backup/main.go | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index df032b5..218c1ed 100644 --- a/README.md +++ b/README.md @@ -176,9 +176,9 @@ docker exec backup This image is heavily inspired by the `futurice/docker-volume-backup`. We decided to publish this image as a simpler and more lightweight alternative because of the following requirements: -- The original image is based on `ubuntu`, making it very heavy. This version is roughly 1/3 in compressed size. +- The original image is based on `ubuntu` and additional tools, making it very heavy. This version is roughly 1/25 in compressed size (it's ~12MB). - The original image uses a shell script, when this is written in Go. -- The original image proposed to handle backup rotation through AWS S3 lifecycle policies. This image adds the option to rotate away old backups through the same command so this functionality can also be offered for non-AWS storage backends like MinIO. Local backups can also be pruned once they reach a certain age. +- The original image proposed to handle backup rotation through AWS S3 lifecycle policies. This image adds the option to rotate away old backups through the same command so this functionality can also be offered for non-AWS storage backends like MinIO. Local copies of backups can also be pruned once they reach a certain age. - InfluxDB specific functionality was removed. - `arm64` and `arm/v7` architectures are supported. - Docker in Swarm mode is supported. diff --git a/cmd/backup/main.go b/cmd/backup/main.go index f6bfdc4..16d7d72 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -41,6 +41,7 @@ func main() { s.must(s.copyBackup()) s.must(s.cleanBackup()) s.must(s.pruneOldBackups()) + s.logger.Info("Finished running backup tasks.") } type script struct { @@ -305,11 +306,9 @@ func (s *script) copyBackup() error { s.logger.Infof("Successfully uploaded a copy of backup `%s` to bucket `%s`", s.file, s.bucket) } - if s.archive != "" { - if _, err := os.Stat(s.archive); !os.IsNotExist(err) { - if err := copy(s.file, path.Join(s.archive, name)); err != nil { - return fmt.Errorf("copyBackup: error copying file to local archive: %w", err) - } + if _, err := os.Stat(s.archive); !os.IsNotExist(err) { + if err := copy(s.file, path.Join(s.archive, name)); err != nil { + return fmt.Errorf("copyBackup: error copying file to local archive: %w", err) } s.logger.Infof("Successfully stored copy of backup `%s` in local archive `%s`", s.file, s.archive) } @@ -404,7 +403,7 @@ func (s *script) pruneOldBackups() error { } } - if s.archive != "" { + if _, err := os.Stat(s.archive); !os.IsNotExist(err) { candidates, err := filepath.Glob( path.Join(s.archive, fmt.Sprintf("%s*", os.Getenv("BACKUP_PRUNING_PREFIX"))), ) From 4c804944333f70e9d6465ee99c51ca93fb222545 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 22 Aug 2021 21:06:51 +0200 Subject: [PATCH 15/33] use go native strftime version --- README.md | 2 +- cmd/backup/main.go | 14 ++++++-------- entrypoint.sh | 2 +- go.mod | 3 ++- go.sum | 2 ++ 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 218c1ed..65a3ace 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ AWS_S3_BUCKET_NAME="" # that is expected to be bigger than the maximum difference of backups. # Valid values have a suffix of (s)econds, (m)inutes or (h)ours. -# BACKUP_PRUNING_LEEWAY="10m" +# BACKUP_PRUNING_LEEWAY="1m" # In case your target bucket or directory contains other files than the ones # managed by this container, you can limit the scope of rotation by setting diff --git a/cmd/backup/main.go b/cmd/backup/main.go index 16d7d72..5453f72 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -11,11 +11,9 @@ import ( "io" "io/ioutil" "os" - "os/exec" "path" "path/filepath" "strconv" - "strings" "time" "github.com/docker/docker/api/types" @@ -23,6 +21,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" "github.com/joho/godotenv" + "github.com/leekchan/timeutil" minio "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/sirupsen/logrus" @@ -54,6 +53,7 @@ type script struct { archive string sources string passphrase string + now time.Time } // lock opens a lockfile at the given location, keeping it locked until the @@ -119,6 +119,8 @@ func (s *script) init() error { s.archive = os.Getenv("BACKUP_ARCHIVE") s.sources = os.Getenv("BACKUP_SOURCES") s.passphrase = os.Getenv("GPG_PASSPHRASE") + + s.now = time.Now() return nil } @@ -237,11 +239,7 @@ func (s *script) stopContainersAndRun(thunk func() error) error { // takeBackup creates a tar archive of the configured backup location and // saves it to disk. func (s *script) takeBackup() error { - outBytes, err := exec.Command("date", fmt.Sprintf("+%s", s.file)).Output() - if err != nil { - return fmt.Errorf("takeBackup: error formatting filename template: %w", err) - } - s.file = strings.TrimSpace(string(outBytes)) + s.file = timeutil.Strftime(&s.now, s.file) if err := targz.Compress(s.sources, s.file); err != nil { return fmt.Errorf("takeBackup: error compressing backup folder: %w", err) } @@ -345,7 +343,7 @@ func (s *script) pruneOldBackups() error { time.Sleep(sleepFor) s.logger.Infof("Trying to prune backups older than %d days now.", retentionDays) - deadline := time.Now().AddDate(0, 0, -retentionDays) + deadline := s.now.AddDate(0, 0, -retentionDays) if s.bucket != "" { candidates := s.mc.ListObjects(s.ctx, s.bucket, minio.ListObjectsOptions{ diff --git a/entrypoint.sh b/entrypoint.sh index 2469ee7..1258c08 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -17,7 +17,7 @@ BACKUP_FILENAME="${BACKUP_FILENAME:-backup-%Y-%m-%dT%H-%M-%S.tar.gz}" BACKUP_ARCHIVE="${BACKUP_ARCHIVE:-/archive}" BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-}" -BACKUP_PRUNING_LEEWAY="${BACKUP_PRUNING_LEEWAY:-10m}" +BACKUP_PRUNING_LEEWAY="${BACKUP_PRUNING_LEEWAY:-1m}" BACKUP_PRUNING_PREFIX="${BACKUP_PRUNING_PREFIX:-}" BACKUP_STOP_CONTAINER_LABEL="${BACKUP_STOP_CONTAINER_LABEL:-true}" diff --git a/go.mod b/go.mod index 771befb..8ccfd39 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,9 @@ go 1.17 require ( github.com/docker/docker v20.10.8+incompatible github.com/joho/godotenv v1.3.0 + github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d github.com/minio/minio-go/v7 v7.0.12 + github.com/sirupsen/logrus v1.8.1 github.com/walle/targz v0.0.0-20140417120357-57fe4206da5a golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 ) @@ -32,7 +34,6 @@ require ( github.com/opencontainers/image-spec v1.0.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/rs/xid v1.2.1 // indirect - github.com/sirupsen/logrus v1.8.1 // indirect golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect golang.org/x/text v0.3.4 // indirect diff --git a/go.sum b/go.sum index 6e3eade..1bb1be4 100644 --- a/go.sum +++ b/go.sum @@ -401,6 +401,8 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d h1:2puqoOQwi3Ai1oznMOsFIbifm6kIfJaLLyYzWD4IzTs= +github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d/go.mod h1:hO90vCP2x3exaSH58BIAowSKvV+0OsY21TtzuFGHON4= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= From 07b06cf0ba551ece9de8a01b2e074780e51f131c Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 22 Aug 2021 22:02:19 +0200 Subject: [PATCH 16/33] read all configuration in init --- cmd/backup/main.go | 98 +++++++++++++++++++++++++++------------------- 1 file changed, 58 insertions(+), 40 deletions(-) diff --git a/cmd/backup/main.go b/cmd/backup/main.go index 5453f72..64db708 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -44,16 +44,22 @@ func main() { } type script struct { - ctx context.Context - cli *client.Client - mc *minio.Client - logger *logrus.Logger - file string - bucket string - archive string - sources string - passphrase string - now time.Time + ctx context.Context + cli *client.Client + mc *minio.Client + logger *logrus.Logger + + start time.Time + + file string + bucket string + archive string + sources string + passphrase []byte + retentionDays *int + leeway *time.Duration + containerLabel string + pruningPrefix string } // lock opens a lockfile at the given location, keeping it locked until the @@ -118,9 +124,27 @@ func (s *script) init() error { s.file = path.Join("/tmp", file) s.archive = os.Getenv("BACKUP_ARCHIVE") s.sources = os.Getenv("BACKUP_SOURCES") - s.passphrase = os.Getenv("GPG_PASSPHRASE") + if v := os.Getenv("GPG_PASSPHRASE"); v != "" { + s.passphrase = []byte(v) + } + if v := os.Getenv("BACKUP_RETENTION_DAYS"); v != "" { + i, err := strconv.Atoi(v) + if err != nil { + return fmt.Errorf("init: error parsing BACKUP_RETENTION_DAYS as int: %w", err) + } + s.retentionDays = &i + } + if v := os.Getenv("BACKUP_PRUNING_LEEWAY"); v != "" { + d, err := time.ParseDuration(v) + if err != nil { + return fmt.Errorf("init: error parsing BACKUP_PRUNING_LEEWAY as duration: %w", err) + } + s.leeway = &d + } + s.containerLabel = os.Getenv("BACKUP_STOP_CONTAINER_LABEL") + s.pruningPrefix = os.Getenv("BACKUP_PRUNING_PREFIX") + s.start = time.Now() - s.now = time.Now() return nil } @@ -140,7 +164,7 @@ func (s *script) stopContainersAndRun(thunk func() error) error { containerLabel := fmt.Sprintf( "docker-volume-backup.stop-during-backup=%s", - os.Getenv("BACKUP_STOP_CONTAINER_LABEL"), + s.containerLabel, ) containersToStop, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{ Quiet: true, @@ -154,7 +178,7 @@ func (s *script) stopContainersAndRun(thunk func() error) error { return fmt.Errorf("stopContainersAndRun: error querying for containers to stop: %w", err) } s.logger.Infof( - "Stopping %d containers labeled `%s` out of %d running containers.", + "Stopping %d containers labeled `%s` out of %d running container(s).", len(containersToStop), containerLabel, len(allContainers), @@ -217,7 +241,7 @@ func (s *script) stopContainersAndRun(thunk func() error) error { err, ) } - s.logger.Infof("Successfully restarted %d containers.", len(stoppedContainers)) + s.logger.Infof("Successfully restarted %d container(s) and the matching service(s).", len(stoppedContainers)) return nil }() @@ -239,7 +263,7 @@ func (s *script) stopContainersAndRun(thunk func() error) error { // takeBackup creates a tar archive of the configured backup location and // saves it to disk. func (s *script) takeBackup() error { - s.file = timeutil.Strftime(&s.now, s.file) + s.file = timeutil.Strftime(&s.start, s.file) if err := targz.Compress(s.sources, s.file); err != nil { return fmt.Errorf("takeBackup: error compressing backup folder: %w", err) } @@ -251,7 +275,7 @@ func (s *script) takeBackup() error { // In case no passphrase is given it returns early, leaving the backup file // untouched. func (s *script) encryptBackup() error { - if s.passphrase == "" { + if s.passphrase == nil { return nil } @@ -285,6 +309,7 @@ func (s *script) encryptBackup() error { if err := os.Remove(s.file); err != nil { return fmt.Errorf("encryptBackup: error removing unencrpyted backup: %w", err) } + s.file = gpgFile s.logger.Infof("Successfully encrypted backup using given passphrase, saving as `%s`.", s.file) return nil @@ -326,29 +351,22 @@ func (s *script) cleanBackup() error { // the given configuration. In case the given configuration would delete all // backups, it does nothing instead. func (s *script) pruneOldBackups() error { - retention := os.Getenv("BACKUP_RETENTION_DAYS") - if retention == "" { + if s.retentionDays == nil { return nil } - retentionDays, err := strconv.Atoi(retention) - if err != nil { - return fmt.Errorf("pruneOldBackups: error parsing BACKUP_RETENTION_DAYS as int: %w", err) - } - leeway := os.Getenv("BACKUP_PRUNING_LEEWAY") - sleepFor, err := time.ParseDuration(leeway) - if err != nil { - return fmt.Errorf("pruneBackups: error parsing given leeway value: %w", err) - } - s.logger.Infof("Sleeping for %s before pruning backups.", leeway) - time.Sleep(sleepFor) - s.logger.Infof("Trying to prune backups older than %d days now.", retentionDays) - deadline := s.now.AddDate(0, 0, -retentionDays) + if s.leeway != nil { + s.logger.Infof("Sleeping for %s before pruning backups.", s.leeway) + time.Sleep(*s.leeway) + } + + s.logger.Infof("Trying to prune backups older than %d day(s) now.", *s.retentionDays) + deadline := s.start.AddDate(0, 0, -*s.retentionDays) if s.bucket != "" { candidates := s.mc.ListObjects(s.ctx, s.bucket, minio.ListObjectsOptions{ WithMetadata: true, - Prefix: os.Getenv("BACKUP_PRUNING_PREFIX"), + Prefix: s.pruningPrefix, }) var matches []minio.ObjectInfo @@ -381,13 +399,13 @@ func (s *script) pruneOldBackups() error { if len(errors) != 0 { return fmt.Errorf( - "pruneOldBackups: %d errors removing files from remote storage: %w", + "pruneOldBackups: %d error(s) removing files from remote storage: %w", len(errors), errors[0], ) } s.logger.Infof( - "Successfully pruned %d out of %d remote backups as their age exceeded the configured retention period.", + "Successfully pruned %d out of %d remote backup(s) as their age exceeded the configured retention period.", len(matches), lenCandidates, ) @@ -397,13 +415,13 @@ func (s *script) pruneOldBackups() error { len(matches), ) } else { - s.logger.Infof("None of %d remote backups were pruned.", lenCandidates) + s.logger.Infof("None of %d remote backup(s) were pruned.", lenCandidates) } } if _, err := os.Stat(s.archive); !os.IsNotExist(err) { candidates, err := filepath.Glob( - path.Join(s.archive, fmt.Sprintf("%s*", os.Getenv("BACKUP_PRUNING_PREFIX"))), + path.Join(s.archive, fmt.Sprintf("%s*", s.pruningPrefix)), ) if err != nil { return fmt.Errorf( @@ -436,13 +454,13 @@ func (s *script) pruneOldBackups() error { } if len(errors) != 0 { return fmt.Errorf( - "pruneOldBackups: %d errors deleting local files, starting with: %w", + "pruneOldBackups: %d error(s) deleting local files, starting with: %w", len(errors), errors[0], ) } s.logger.Infof( - "Successfully pruned %d out of %d local backups as their age exceeded the configured retention period.", + "Successfully pruned %d out of %d local backup(s) as their age exceeded the configured retention period.", len(matches), len(candidates), ) @@ -452,7 +470,7 @@ func (s *script) pruneOldBackups() error { len(matches), ) } else { - s.logger.Infof("None of %d local backups were pruned.", len(candidates)) + s.logger.Infof("None of %d local backup(s) were pruned.", len(candidates)) } } return nil From 5a2bf48ec62df6eb8e18827e61bcb9f63b1159b6 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 22 Aug 2021 22:44:36 +0200 Subject: [PATCH 17/33] make sure backup also runs when socket isn't present --- cmd/backup/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/backup/main.go b/cmd/backup/main.go index 64db708..5cdcb94 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -153,8 +153,9 @@ func (s *script) init() error { // sure containers are being restarted if required. func (s *script) stopContainersAndRun(thunk func() error) error { if s.cli == nil { - return nil + return thunk() } + allContainers, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{ Quiet: true, }) From 411a62a6c7ce42f9e2cc0e3a62c119ac44ed49ed Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Mon, 23 Aug 2021 07:07:44 +0200 Subject: [PATCH 18/33] shorten log messages --- cmd/backup/main.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/backup/main.go b/cmd/backup/main.go index 5cdcb94..81b0577 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -242,7 +242,7 @@ func (s *script) stopContainersAndRun(thunk func() error) error { err, ) } - s.logger.Infof("Successfully restarted %d container(s) and the matching service(s).", len(stoppedContainers)) + s.logger.Infof("Restarted %d container(s) and the matching service(s).", len(stoppedContainers)) return nil }() @@ -268,7 +268,7 @@ func (s *script) takeBackup() error { if err := targz.Compress(s.sources, s.file); err != nil { return fmt.Errorf("takeBackup: error compressing backup folder: %w", err) } - s.logger.Infof("Successfully created backup of `%s` at `%s`.", s.sources, s.file) + s.logger.Infof("Created backup of `%s` at `%s`.", s.sources, s.file) return nil } @@ -312,7 +312,7 @@ func (s *script) encryptBackup() error { } s.file = gpgFile - s.logger.Infof("Successfully encrypted backup using given passphrase, saving as `%s`.", s.file) + s.logger.Infof("Encrypted backup using given passphrase, saving as `%s`.", s.file) return nil } @@ -327,14 +327,14 @@ func (s *script) copyBackup() error { if err != nil { return fmt.Errorf("copyBackup: error uploading backup to remote storage: %w", err) } - s.logger.Infof("Successfully uploaded a copy of backup `%s` to bucket `%s`", s.file, s.bucket) + s.logger.Infof("Uploaded a copy of backup `%s` to bucket `%s`", s.file, s.bucket) } if _, err := os.Stat(s.archive); !os.IsNotExist(err) { if err := copy(s.file, path.Join(s.archive, name)); err != nil { return fmt.Errorf("copyBackup: error copying file to local archive: %w", err) } - s.logger.Infof("Successfully stored copy of backup `%s` in local archive `%s`", s.file, s.archive) + s.logger.Infof("Stored copy of backup `%s` in local archive `%s`", s.file, s.archive) } return nil } @@ -344,7 +344,7 @@ func (s *script) cleanBackup() error { if err := os.Remove(s.file); err != nil { return fmt.Errorf("cleanBackup: error removing file: %w", err) } - s.logger.Info("Successfully cleaned up local artifacts.") + s.logger.Info("Cleaned up local artifacts.") return nil } @@ -406,7 +406,7 @@ func (s *script) pruneOldBackups() error { ) } s.logger.Infof( - "Successfully pruned %d out of %d remote backup(s) as their age exceeded the configured retention period.", + "Pruned %d out of %d remote backup(s) as their age exceeded the configured retention period.", len(matches), lenCandidates, ) @@ -461,7 +461,7 @@ func (s *script) pruneOldBackups() error { ) } s.logger.Infof( - "Successfully pruned %d out of %d local backup(s) as their age exceeded the configured retention period.", + "Pruned %d out of %d local backup(s) as their age exceeded the configured retention period.", len(matches), len(candidates), ) From 7086c6e645922c5e48144d0d6a49017db1a803a2 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Mon, 23 Aug 2021 08:19:22 +0200 Subject: [PATCH 19/33] read backup in small chunks when encrypting --- README.md | 6 +-- cmd/backup/main.go | 110 ++++++++++++++++++++++++++------------------- entrypoint.sh | 1 - 3 files changed, 66 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 65a3ace..ef7598f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Backup Docker volumes locally or to any S3 compatible storage. -The [offen/docker-volume-backup](https://hub.docker.com/r/offen/docker-volume-backup) Docker image can be used as a sidecar container to an existing Docker setup. It handles recurring backups of Docker volumes to a local directory or any S3 compatible storage (or both) and rotates away old backups if configured. +The [offen/docker-volume-backup](https://hub.docker.com/r/offen/docker-volume-backup) Docker image can be used as a lightweight (below 15MB) sidecar container to an existing Docker setup. It handles recurring backups of Docker volumes to a local directory or any S3 compatible storage (or both), and rotates away old backups if configured. ## Configuration @@ -177,8 +177,8 @@ docker exec backup This image is heavily inspired by the `futurice/docker-volume-backup`. We decided to publish this image as a simpler and more lightweight alternative because of the following requirements: - The original image is based on `ubuntu` and additional tools, making it very heavy. This version is roughly 1/25 in compressed size (it's ~12MB). -- The original image uses a shell script, when this is written in Go. +- The original image uses a shell script, when this is written in Go, which makes it easier to extend and maintain (more verbose also). - The original image proposed to handle backup rotation through AWS S3 lifecycle policies. This image adds the option to rotate away old backups through the same command so this functionality can also be offered for non-AWS storage backends like MinIO. Local copies of backups can also be pruned once they reach a certain age. -- InfluxDB specific functionality was removed. +- InfluxDB specific functionality from the original image was removed. - `arm64` and `arm/v7` architectures are supported. - Docker in Swarm mode is supported. diff --git a/cmd/backup/main.go b/cmd/backup/main.go index 81b0577..1b3e74f 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -4,6 +4,7 @@ package main import ( + "bufio" "bytes" "context" "errors" @@ -43,6 +44,8 @@ func main() { s.logger.Info("Finished running backup tasks.") } +// script holds all the stateful information required to orchestrate a +// single backup run. type script struct { ctx context.Context cli *client.Client @@ -62,27 +65,10 @@ type script struct { pruningPrefix string } -// lock opens a lockfile at the given location, keeping it locked until the -// caller invokes the returned release func. When invoked while the file is -// still locked the function panics. -func lock(lockfile string) func() error { - lf, err := os.OpenFile(lockfile, os.O_CREATE, os.ModeAppend) - if err != nil { - panic(err) - } - return func() error { - if err := lf.Close(); err != nil { - return fmt.Errorf("lock: error releasing file lock: %w", err) - } - if err := os.Remove(lockfile); err != nil { - return fmt.Errorf("lock: error removing lock file: %w", err) - } - return nil - } -} - // init creates all resources needed for the script to perform actions against -// remote resources like the Docker engine or remote storage locations. +// remote resources like the Docker engine or remote storage locations. All +// reading from env vars or other configuration sources is expected to happen +// in this method. func (s *script) init() error { s.ctx = context.Background() s.logger = logrus.New() @@ -178,22 +164,25 @@ func (s *script) stopContainersAndRun(thunk func() error) error { if err != nil { return fmt.Errorf("stopContainersAndRun: error querying for containers to stop: %w", err) } + + if len(containersToStop) == 0 { + return thunk() + } + s.logger.Infof( - "Stopping %d containers labeled `%s` out of %d running container(s).", + "Stopping %d container(s) labeled `%s` out of %d running container(s).", len(containersToStop), containerLabel, len(allContainers), ) var stoppedContainers []types.Container - var errors []error - if len(containersToStop) != 0 { - for _, container := range containersToStop { - if err := s.cli.ContainerStop(s.ctx, container.ID, nil); err != nil { - errors = append(errors, err) - } else { - stoppedContainers = append(stoppedContainers, container) - } + var stopErrors []error + for _, container := range containersToStop { + if err := s.cli.ContainerStop(s.ctx, container.ID, nil); err != nil { + stopErrors = append(stopErrors, err) + } else { + stoppedContainers = append(stoppedContainers, container) } } @@ -246,17 +235,13 @@ func (s *script) stopContainersAndRun(thunk func() error) error { return nil }() - var stopErr error - if len(errors) != 0 { - stopErr = fmt.Errorf( - "stopContainersAndRun: %d errors stopping containers: %w", - len(errors), + if len(stopErrors) != 0 { + return fmt.Errorf( + "stopContainersAndRun: %d error(s) stopping containers: %w", + len(stopErrors), err, ) } - if stopErr != nil { - return stopErr - } return thunk() } @@ -280,9 +265,10 @@ func (s *script) encryptBackup() error { return nil } - buf := bytes.NewBuffer(nil) + output := bytes.NewBuffer(nil) _, name := path.Split(s.file) - pt, err := openpgp.SymmetricallyEncrypt(buf, []byte(s.passphrase), &openpgp.FileHints{ + + pt, err := openpgp.SymmetricallyEncrypt(output, []byte(s.passphrase), &openpgp.FileHints{ IsBinary: true, FileName: name, }, nil) @@ -290,20 +276,30 @@ func (s *script) encryptBackup() error { return fmt.Errorf("encryptBackup: error encrypting backup file: %w", err) } - unencrypted, err := ioutil.ReadFile(s.file) + file, err := os.Open(s.file) if err != nil { - pt.Close() - return fmt.Errorf("encryptBackup: error reading unencrypted backup file: %w", err) + return fmt.Errorf("encryptBackup: error opening unencrypted backup file: %w", err) } - _, err = pt.Write(unencrypted) - if err != nil { - pt.Close() - return fmt.Errorf("encryptBackup: error writing backup contents: %w", err) + + // backup files might be very large, so they are being read in chunks instead + // of loading them into memory once. + scanner := bufio.NewScanner(file) + chunk := make([]byte, 0, 1024*1024) + scanner.Buffer(chunk, 10*1024*1024) + for scanner.Scan() { + _, err = pt.Write(scanner.Bytes()) + if err != nil { + file.Close() + pt.Close() + return fmt.Errorf("encryptBackup: error encrypting backup contents: %w", err) + } } + + file.Close() pt.Close() gpgFile := fmt.Sprintf("%s.gpg", s.file) - if err := ioutil.WriteFile(gpgFile, buf.Bytes(), os.ModeAppend); err != nil { + if err := ioutil.WriteFile(gpgFile, output.Bytes(), os.ModeAppend); err != nil { return fmt.Errorf("encryptBackup: error writing encrypted version of backup: %w", err) } @@ -487,6 +483,26 @@ func (s *script) must(err error) { } } +// lock opens a lockfile at the given location, keeping it locked until the +// caller invokes the returned release func. When invoked while the file is +// still locked the function panics. +func lock(lockfile string) func() error { + lf, err := os.OpenFile(lockfile, os.O_CREATE|os.O_RDWR, os.ModeAppend) + if err != nil { + panic(err) + } + return func() error { + if err := lf.Close(); err != nil { + return fmt.Errorf("lock: error releasing file lock: %w", err) + } + if err := os.Remove(lockfile); err != nil { + return fmt.Errorf("lock: error removing lock file: %w", err) + } + return nil + } +} + +// copy creates a copy of the file located at `dst` at `src`. func copy(src, dst string) error { in, err := os.Open(src) if err != nil { diff --git a/entrypoint.sh b/entrypoint.sh index 1258c08..dbaa1bd 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -9,7 +9,6 @@ set -e # Write cronjob env to file, fill in sensible defaults, and read them back in -mkdir -p /etc/backup cat < /etc/backup.env BACKUP_SOURCES="${BACKUP_SOURCES:-/backup}" BACKUP_CRON_EXPRESSION="${BACKUP_CRON_EXPRESSION:-@daily}" From f4f4fa9e749ba0921428ba9763d56b70648f2ad0 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Mon, 23 Aug 2021 09:10:49 +0200 Subject: [PATCH 20/33] use full filepath when pruning local backups --- cmd/backup/main.go | 6 +++--- test/compose/docker-compose.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/backup/main.go b/cmd/backup/main.go index 1b3e74f..b6a7545 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -426,7 +426,7 @@ func (s *script) pruneOldBackups() error { ) } - var matches []os.FileInfo + var matches []string for _, candidate := range candidates { fi, err := os.Stat(candidate) if err != nil { @@ -438,14 +438,14 @@ func (s *script) pruneOldBackups() error { } if fi.ModTime().Before(deadline) { - matches = append(matches, fi) + matches = append(matches, candidate) } } if len(matches) != 0 && len(matches) != len(candidates) { var errors []error for _, candidate := range matches { - if err := os.Remove(candidate.Name()); err != nil { + if err := os.Remove(candidate); err != nil { errors = append(errors, err) } } diff --git a/test/compose/docker-compose.yml b/test/compose/docker-compose.yml index 26dbbb9..1646ce6 100644 --- a/test/compose/docker-compose.yml +++ b/test/compose/docker-compose.yml @@ -27,8 +27,8 @@ services: BACKUP_CRON_EXPRESSION: 0 0 5 31 2 ? BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-7} BACKUP_PRUNING_LEEWAY: 5s + BACKUP_PRUNING_PREFIX: test GPG_PASSPHRASE: 1234secret - # BACKUP_PRUNING_PREFIX: test volumes: - ./local:/archive - app_data:/backup/app_data:ro From ec87bd27e714f493fdea02f59ea43ad895d98081 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Mon, 23 Aug 2021 15:19:50 +0200 Subject: [PATCH 21/33] do not use scanner to write file in chunks --- cmd/backup/main.go | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/cmd/backup/main.go b/cmd/backup/main.go index b6a7545..b83f0bd 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -4,7 +4,6 @@ package main import ( - "bufio" "bytes" "context" "errors" @@ -276,26 +275,11 @@ func (s *script) encryptBackup() error { return fmt.Errorf("encryptBackup: error encrypting backup file: %w", err) } - file, err := os.Open(s.file) + b, err := ioutil.ReadFile(s.file) if err != nil { return fmt.Errorf("encryptBackup: error opening unencrypted backup file: %w", err) } - - // backup files might be very large, so they are being read in chunks instead - // of loading them into memory once. - scanner := bufio.NewScanner(file) - chunk := make([]byte, 0, 1024*1024) - scanner.Buffer(chunk, 10*1024*1024) - for scanner.Scan() { - _, err = pt.Write(scanner.Bytes()) - if err != nil { - file.Close() - pt.Close() - return fmt.Errorf("encryptBackup: error encrypting backup contents: %w", err) - } - } - - file.Close() + pt.Write(b) pt.Close() gpgFile := fmt.Sprintf("%s.gpg", s.file) From b1c4bee85d2cd9f820755c165f497b1e539accae Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Mon, 23 Aug 2021 15:33:49 +0200 Subject: [PATCH 22/33] use buffered reader to write to encryption mechanism --- cmd/backup/main.go | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/cmd/backup/main.go b/cmd/backup/main.go index b83f0bd..65eeee6 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -4,12 +4,10 @@ package main import ( - "bytes" "context" "errors" "fmt" "io" - "io/ioutil" "os" "path" "path/filepath" @@ -264,27 +262,30 @@ func (s *script) encryptBackup() error { return nil } - output := bytes.NewBuffer(nil) - _, name := path.Split(s.file) + gpgFile := fmt.Sprintf("%s.gpg", s.file) + outFile, err := os.Create(gpgFile) + defer outFile.Close() + if err != nil { + return fmt.Errorf("encryptBackup: error opening out file: %w", err) + } - pt, err := openpgp.SymmetricallyEncrypt(output, []byte(s.passphrase), &openpgp.FileHints{ + _, name := path.Split(s.file) + dst, err := openpgp.SymmetricallyEncrypt(outFile, []byte(s.passphrase), &openpgp.FileHints{ IsBinary: true, FileName: name, }, nil) + defer dst.Close() if err != nil { return fmt.Errorf("encryptBackup: error encrypting backup file: %w", err) } - b, err := ioutil.ReadFile(s.file) + src, err := os.Open(s.file) if err != nil { - return fmt.Errorf("encryptBackup: error opening unencrypted backup file: %w", err) + return fmt.Errorf("encryptBackup: error opening backup file %s: %w", s.file, err) } - pt.Write(b) - pt.Close() - gpgFile := fmt.Sprintf("%s.gpg", s.file) - if err := ioutil.WriteFile(gpgFile, output.Bytes(), os.ModeAppend); err != nil { - return fmt.Errorf("encryptBackup: error writing encrypted version of backup: %w", err) + if _, err := io.Copy(dst, src); err != nil { + return fmt.Errorf("encryptBackup: error writing ciphertext to file: %w", err) } if err := os.Remove(s.file); err != nil { From 2469597848e0154053e71ad0a4c989687048bde9 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Mon, 23 Aug 2021 18:46:49 +0200 Subject: [PATCH 23/33] fix lockfile mechanism --- cmd/backup/main.go | 31 ++++++++++++++++++------------- go.mod | 1 + go.sum | 5 +++++ 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/cmd/backup/main.go b/cmd/backup/main.go index 65eeee6..3b52056 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -18,6 +18,7 @@ import ( "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" + "github.com/gofrs/flock" "github.com/joho/godotenv" "github.com/leekchan/timeutil" minio "github.com/minio/minio-go/v7" @@ -228,7 +229,10 @@ func (s *script) stopContainersAndRun(thunk func() error) error { err, ) } - s.logger.Infof("Restarted %d container(s) and the matching service(s).", len(stoppedContainers)) + s.logger.Infof( + "Restarted %d container(s) and the matching service(s).", + len(stoppedContainers), + ) return nil }() @@ -356,7 +360,10 @@ func (s *script) pruneOldBackups() error { for candidate := range candidates { lenCandidates++ if candidate.Err != nil { - return fmt.Errorf("pruneOldBackups: error looking up candidates from remote storage: %w", candidate.Err) + return fmt.Errorf( + "pruneOldBackups: error looking up candidates from remote storage: %w", + candidate.Err, + ) } if candidate.LastModified.Before(deadline) { matches = append(matches, candidate) @@ -393,9 +400,10 @@ func (s *script) pruneOldBackups() error { ) } else if len(matches) != 0 && len(matches) == lenCandidates { s.logger.Warnf( - "The current configuration would delete all %d remote backup copies. Refusing to do so, please check your configuration.", + "The current configuration would delete all %d remote backup copies.", len(matches), ) + s.logger.Warn("Refusing to do so, please check your configuration.") } else { s.logger.Infof("None of %d remote backup(s) were pruned.", lenCandidates) } @@ -448,9 +456,10 @@ func (s *script) pruneOldBackups() error { ) } else if len(matches) != 0 && len(matches) == len(candidates) { s.logger.Warnf( - "The current configuration would delete all %d local backup copies. Refusing to do so, please check your configuration.", + "The current configuration would delete all %d local backup copies.", len(matches), ) + s.logger.Warn("Refusing to do so, please check your configuration.") } else { s.logger.Infof("None of %d local backup(s) were pruned.", len(candidates)) } @@ -472,19 +481,15 @@ func (s *script) must(err error) { // caller invokes the returned release func. When invoked while the file is // still locked the function panics. func lock(lockfile string) func() error { - lf, err := os.OpenFile(lockfile, os.O_CREATE|os.O_RDWR, os.ModeAppend) + fileLock := flock.New(lockfile) + acquired, err := fileLock.TryLock() if err != nil { panic(err) } - return func() error { - if err := lf.Close(); err != nil { - return fmt.Errorf("lock: error releasing file lock: %w", err) - } - if err := os.Remove(lockfile); err != nil { - return fmt.Errorf("lock: error removing lock file: %w", err) - } - return nil + if !acquired { + panic("unable to acquire file lock") } + return fileLock.Unlock } // copy creates a copy of the file located at `dst` at `src`. diff --git a/go.mod b/go.mod index 8ccfd39..41c432d 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.17 require ( github.com/docker/docker v20.10.8+incompatible + github.com/gofrs/flock v0.8.1 github.com/joho/godotenv v1.3.0 github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d github.com/minio/minio-go/v7 v7.0.12 diff --git a/go.sum b/go.sum index 1bb1be4..6433a6e 100644 --- a/go.sum +++ b/go.sum @@ -274,6 +274,8 @@ github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblf github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -397,9 +399,11 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d h1:2puqoOQwi3Ai1oznMOsFIbifm6kIfJaLLyYzWD4IzTs= github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d/go.mod h1:hO90vCP2x3exaSH58BIAowSKvV+0OsY21TtzuFGHON4= @@ -907,6 +911,7 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= From e0c4adc563690f4b161360c8d0b9e790e3a5feb4 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Tue, 24 Aug 2021 09:01:44 +0200 Subject: [PATCH 24/33] move handling of config to script layer --- NOTICE | 22 ------ cmd/backup/main.go | 126 ++++++++++++++------------------ entrypoint.sh | 25 ------- go.mod | 2 +- go.sum | 4 +- test/compose/docker-compose.yml | 1 - 6 files changed, 58 insertions(+), 122 deletions(-) delete mode 100644 NOTICE diff --git a/NOTICE b/NOTICE deleted file mode 100644 index 37b7c92..0000000 --- a/NOTICE +++ /dev/null @@ -1,22 +0,0 @@ -Copyright 2021 Offen Authors - -This project contains software that is Copyright (c) 2018 Futurice -Licensed under the Terms of the MIT License: - -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. diff --git a/cmd/backup/main.go b/cmd/backup/main.go index 3b52056..4f99f0c 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -5,13 +5,11 @@ package main import ( "context" - "errors" "fmt" "io" "os" "path" "path/filepath" - "strconv" "time" "github.com/docker/docker/api/types" @@ -19,7 +17,7 @@ import ( "github.com/docker/docker/api/types/swarm" "github.com/docker/docker/client" "github.com/gofrs/flock" - "github.com/joho/godotenv" + "github.com/kelseyhightower/envconfig" "github.com/leekchan/timeutil" minio "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" @@ -29,7 +27,7 @@ import ( ) func main() { - unlock := lock("/var/dockervolumebackup.lock") + unlock := lock("/var/lock/dockervolumebackup.lock") defer unlock() s := &script{} @@ -51,16 +49,26 @@ type script struct { logger *logrus.Logger start time.Time + file string - file string - bucket string - archive string - sources string - passphrase []byte - retentionDays *int - leeway *time.Duration - containerLabel string - pruningPrefix string + c *config +} + +type config struct { + BackupSources string `split_words:"true" default:"/backup"` + BackupFilename string `split_words:"true" default:"backup-%Y-%m-%dT%H-%M-%S.tar.gz"` + BackupArchive string `split_words:"true" default:"/archive"` + BackupRetentionDays int32 `split_words:"true" default:"-1"` + BackupPruningLeeway time.Duration `split_words:"true" default:"1m"` + BackupPruningPrefix string `split_words:"true"` + BackupStopContainerLabel string `split_words:"true" default:"true"` + AwsS3BucketName string `split_words:"true"` + AwsEndpoint string `split_words:"true" default:"s3.amazonaws.com"` + AwsEndpointProto string `split_words:"true" default:"https"` + AwsEndpointInsecure bool `split_words:"true"` + AwsAccessKeyID string `envconfig:"AWS_ACCESS_KEY_ID"` + AwsSecretAccessKey string `split_words:"true"` + GpgPassphrase string `split_words:"true"` } // init creates all resources needed for the script to perform actions against @@ -72,8 +80,9 @@ func (s *script) init() error { s.logger = logrus.New() s.logger.SetOutput(os.Stdout) - if err := godotenv.Load("/etc/backup.env"); err != nil { - return fmt.Errorf("init: failed to load env file: %w", err) + s.c = &config{} + if err := envconfig.Process("", s.c); err != nil { + return fmt.Errorf("init: failed to process configuration values: %w", err) } _, err := os.Stat("/var/run/docker.sock") @@ -85,15 +94,14 @@ func (s *script) init() error { s.cli = cli } - if bucket := os.Getenv("AWS_S3_BUCKET_NAME"); bucket != "" { - s.bucket = bucket - mc, err := minio.New(os.Getenv("AWS_ENDPOINT"), &minio.Options{ + if s.c.AwsS3BucketName != "" { + mc, err := minio.New(s.c.AwsEndpoint, &minio.Options{ Creds: credentials.NewStaticV4( - os.Getenv("AWS_ACCESS_KEY_ID"), - os.Getenv("AWS_SECRET_ACCESS_KEY"), + s.c.AwsAccessKeyID, + s.c.AwsSecretAccessKey, "", ), - Secure: os.Getenv("AWS_ENDPOINT_INSECURE") == "" && os.Getenv("AWS_ENDPOINT_PROTO") == "https", + Secure: !s.c.AwsEndpointInsecure && s.c.AwsEndpointProto == "https", }) if err != nil { return fmt.Errorf("init: error setting up minio client: %w", err) @@ -101,32 +109,7 @@ func (s *script) init() error { s.mc = mc } - file := os.Getenv("BACKUP_FILENAME") - if file == "" { - return errors.New("init: BACKUP_FILENAME not given") - } - s.file = path.Join("/tmp", file) - s.archive = os.Getenv("BACKUP_ARCHIVE") - s.sources = os.Getenv("BACKUP_SOURCES") - if v := os.Getenv("GPG_PASSPHRASE"); v != "" { - s.passphrase = []byte(v) - } - if v := os.Getenv("BACKUP_RETENTION_DAYS"); v != "" { - i, err := strconv.Atoi(v) - if err != nil { - return fmt.Errorf("init: error parsing BACKUP_RETENTION_DAYS as int: %w", err) - } - s.retentionDays = &i - } - if v := os.Getenv("BACKUP_PRUNING_LEEWAY"); v != "" { - d, err := time.ParseDuration(v) - if err != nil { - return fmt.Errorf("init: error parsing BACKUP_PRUNING_LEEWAY as duration: %w", err) - } - s.leeway = &d - } - s.containerLabel = os.Getenv("BACKUP_STOP_CONTAINER_LABEL") - s.pruningPrefix = os.Getenv("BACKUP_PRUNING_PREFIX") + s.file = path.Join("/tmp", s.c.BackupFilename) s.start = time.Now() return nil @@ -134,7 +117,8 @@ func (s *script) init() error { // stopContainersAndRun stops all Docker containers that are marked as to being // stopped during the backup and runs the given thunk. After returning, it makes -// sure containers are being restarted if required. +// sure containers are being restarted if required. In case the docker cli +// is not configured, it will call the given thunk immediately. func (s *script) stopContainersAndRun(thunk func() error) error { if s.cli == nil { return thunk() @@ -149,7 +133,7 @@ func (s *script) stopContainersAndRun(thunk func() error) error { containerLabel := fmt.Sprintf( "docker-volume-backup.stop-during-backup=%s", - s.containerLabel, + s.c.BackupStopContainerLabel, ) containersToStop, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{ Quiet: true, @@ -251,10 +235,10 @@ func (s *script) stopContainersAndRun(thunk func() error) error { // saves it to disk. func (s *script) takeBackup() error { s.file = timeutil.Strftime(&s.start, s.file) - if err := targz.Compress(s.sources, s.file); err != nil { + if err := targz.Compress(s.c.BackupSources, s.file); err != nil { return fmt.Errorf("takeBackup: error compressing backup folder: %w", err) } - s.logger.Infof("Created backup of `%s` at `%s`.", s.sources, s.file) + s.logger.Infof("Created backup of `%s` at `%s`.", s.c.BackupSources, s.file) return nil } @@ -262,7 +246,7 @@ func (s *script) takeBackup() error { // In case no passphrase is given it returns early, leaving the backup file // untouched. func (s *script) encryptBackup() error { - if s.passphrase == nil { + if s.c.GpgPassphrase == "" { return nil } @@ -274,7 +258,7 @@ func (s *script) encryptBackup() error { } _, name := path.Split(s.file) - dst, err := openpgp.SymmetricallyEncrypt(outFile, []byte(s.passphrase), &openpgp.FileHints{ + dst, err := openpgp.SymmetricallyEncrypt(outFile, []byte(s.c.GpgPassphrase), &openpgp.FileHints{ IsBinary: true, FileName: name, }, nil) @@ -305,21 +289,21 @@ func (s *script) encryptBackup() error { // as per the given configuration. func (s *script) copyBackup() error { _, name := path.Split(s.file) - if s.bucket != "" { - _, err := s.mc.FPutObject(s.ctx, s.bucket, name, s.file, minio.PutObjectOptions{ + if s.c.AwsS3BucketName != "" { + _, err := s.mc.FPutObject(s.ctx, s.c.AwsS3BucketName, name, s.file, minio.PutObjectOptions{ ContentType: "application/tar+gzip", }) if err != nil { return fmt.Errorf("copyBackup: error uploading backup to remote storage: %w", err) } - s.logger.Infof("Uploaded a copy of backup `%s` to bucket `%s`", s.file, s.bucket) + s.logger.Infof("Uploaded a copy of backup `%s` to bucket `%s`", s.file, s.c.AwsS3BucketName) } - if _, err := os.Stat(s.archive); !os.IsNotExist(err) { - if err := copy(s.file, path.Join(s.archive, name)); err != nil { + if _, err := os.Stat(s.c.BackupArchive); !os.IsNotExist(err) { + if err := copy(s.file, path.Join(s.c.BackupArchive, name)); err != nil { return fmt.Errorf("copyBackup: error copying file to local archive: %w", err) } - s.logger.Infof("Stored copy of backup `%s` in local archive `%s`", s.file, s.archive) + s.logger.Infof("Stored copy of backup `%s` in local archive `%s`", s.file, s.c.AwsS3BucketName) } return nil } @@ -337,22 +321,22 @@ func (s *script) cleanBackup() error { // the given configuration. In case the given configuration would delete all // backups, it does nothing instead. func (s *script) pruneOldBackups() error { - if s.retentionDays == nil { + if s.c.BackupRetentionDays < 0 { return nil } - if s.leeway != nil { - s.logger.Infof("Sleeping for %s before pruning backups.", s.leeway) - time.Sleep(*s.leeway) + if s.c.BackupPruningLeeway != 0 { + s.logger.Infof("Sleeping for %s before pruning backups.", s.c.BackupPruningLeeway) + time.Sleep(s.c.BackupPruningLeeway) } - s.logger.Infof("Trying to prune backups older than %d day(s) now.", *s.retentionDays) - deadline := s.start.AddDate(0, 0, -*s.retentionDays) + s.logger.Infof("Trying to prune backups older than %d day(s) now.", s.c.BackupRetentionDays) + deadline := s.start.AddDate(0, 0, -int(s.c.BackupRetentionDays)) - if s.bucket != "" { - candidates := s.mc.ListObjects(s.ctx, s.bucket, minio.ListObjectsOptions{ + if s.c.AwsS3BucketName != "" { + candidates := s.mc.ListObjects(s.ctx, s.c.AwsS3BucketName, minio.ListObjectsOptions{ WithMetadata: true, - Prefix: s.pruningPrefix, + Prefix: s.c.BackupPruningPrefix, }) var matches []minio.ObjectInfo @@ -378,7 +362,7 @@ func (s *script) pruneOldBackups() error { } close(objectsCh) }() - errChan := s.mc.RemoveObjects(s.ctx, s.bucket, objectsCh, minio.RemoveObjectsOptions{}) + errChan := s.mc.RemoveObjects(s.ctx, s.c.AwsS3BucketName, objectsCh, minio.RemoveObjectsOptions{}) var errors []error for result := range errChan { if result.Err != nil { @@ -409,9 +393,9 @@ func (s *script) pruneOldBackups() error { } } - if _, err := os.Stat(s.archive); !os.IsNotExist(err) { + if _, err := os.Stat(s.c.BackupArchive); !os.IsNotExist(err) { candidates, err := filepath.Glob( - path.Join(s.archive, fmt.Sprintf("%s*", s.pruningPrefix)), + path.Join(s.c.BackupArchive, fmt.Sprintf("%s*", s.c.BackupPruningPrefix)), ) if err != nil { return fmt.Errorf( diff --git a/entrypoint.sh b/entrypoint.sh index dbaa1bd..e579435 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -3,37 +3,12 @@ # Copyright 2021 - Offen Authors # SPDX-License-Identifier: MPL-2.0 -# Portions of this file are taken from github.com/futurice/docker-volume-backup -# See NOTICE for information about authors and licensing. - set -e -# Write cronjob env to file, fill in sensible defaults, and read them back in -cat < /etc/backup.env -BACKUP_SOURCES="${BACKUP_SOURCES:-/backup}" BACKUP_CRON_EXPRESSION="${BACKUP_CRON_EXPRESSION:-@daily}" -BACKUP_FILENAME="${BACKUP_FILENAME:-backup-%Y-%m-%dT%H-%M-%S.tar.gz}" -BACKUP_ARCHIVE="${BACKUP_ARCHIVE:-/archive}" -BACKUP_RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-}" -BACKUP_PRUNING_LEEWAY="${BACKUP_PRUNING_LEEWAY:-1m}" -BACKUP_PRUNING_PREFIX="${BACKUP_PRUNING_PREFIX:-}" -BACKUP_STOP_CONTAINER_LABEL="${BACKUP_STOP_CONTAINER_LABEL:-true}" - -AWS_S3_BUCKET_NAME="${AWS_S3_BUCKET_NAME:-}" -AWS_ENDPOINT="${AWS_ENDPOINT:-s3.amazonaws.com}" -AWS_ENDPOINT_PROTO="${AWS_ENDPOINT_PROTO:-https}" -AWS_ENDPOINT_INSECURE="${AWS_ENDPOINT_INSECURE:-}" - -GPG_PASSPHRASE="${GPG_PASSPHRASE:-}" -EOF -chmod a+x /etc/backup.env -source /etc/backup.env - -# Add our cron entry, and direct stdout & stderr to Docker commands stdout echo "Installing cron.d entry with expression $BACKUP_CRON_EXPRESSION." echo "$BACKUP_CRON_EXPRESSION backup 2>&1" | crontab - -# Let cron take the wheel echo "Starting cron in foreground." crond -f -l 8 diff --git a/go.mod b/go.mod index 41c432d..d348f6f 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.17 require ( github.com/docker/docker v20.10.8+incompatible github.com/gofrs/flock v0.8.1 - github.com/joho/godotenv v1.3.0 + github.com/kelseyhightower/envconfig v1.4.0 github.com/leekchan/timeutil v0.0.0-20150802142658-28917288c48d github.com/minio/minio-go/v7 v7.0.12 github.com/sirupsen/logrus v1.8.1 diff --git a/go.sum b/go.sum index 6433a6e..11d7ff4 100644 --- a/go.sum +++ b/go.sum @@ -372,8 +372,6 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -384,6 +382,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= diff --git a/test/compose/docker-compose.yml b/test/compose/docker-compose.yml index 1646ce6..a61d106 100644 --- a/test/compose/docker-compose.yml +++ b/test/compose/docker-compose.yml @@ -41,7 +41,6 @@ services: volumes: - app_data:/var/opt/offen - volumes: backup_data: app_data: From e73256ad70d027e4a4c85f681f50b8834fa4a85c Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Tue, 24 Aug 2021 09:15:43 +0200 Subject: [PATCH 25/33] do not use start time as deadline --- cmd/backup/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/backup/main.go b/cmd/backup/main.go index 4f99f0c..db7a1d8 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -331,7 +331,7 @@ func (s *script) pruneOldBackups() error { } s.logger.Infof("Trying to prune backups older than %d day(s) now.", s.c.BackupRetentionDays) - deadline := s.start.AddDate(0, 0, -int(s.c.BackupRetentionDays)) + deadline := time.Now().AddDate(0, 0, -int(s.c.BackupRetentionDays)) if s.c.AwsS3BucketName != "" { candidates := s.mc.ListObjects(s.ctx, s.c.AwsS3BucketName, minio.ListObjectsOptions{ From 5334ff1a5aeab4b3237db4069bd6a97218f84c51 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Tue, 24 Aug 2021 11:39:27 +0200 Subject: [PATCH 26/33] refactor script initialization --- README.md | 2 +- cmd/backup/main.go | 117 ++++++++++++++++++++++++--------------------- 2 files changed, 64 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index ef7598f..185184a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Backup Docker volumes locally or to any S3 compatible storage. -The [offen/docker-volume-backup](https://hub.docker.com/r/offen/docker-volume-backup) Docker image can be used as a lightweight (below 15MB) sidecar container to an existing Docker setup. It handles recurring backups of Docker volumes to a local directory or any S3 compatible storage (or both), and rotates away old backups if configured. +The [offen/docker-volume-backup](https://hub.docker.com/r/offen/docker-volume-backup) Docker image can be used as a lightweight (below 15MB) sidecar container to an existing Docker setup. It handles __recurring or one-off backups of Docker volumes__ to a __local directory__ or __any S3 compatible storage__ (or both), and __rotates away old backups__ if configured. It also supports __encrypting your backups using GPG__. ## Configuration diff --git a/cmd/backup/main.go b/cmd/backup/main.go index db7a1d8..bda0bde 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -19,7 +19,7 @@ import ( "github.com/gofrs/flock" "github.com/kelseyhightower/envconfig" "github.com/leekchan/timeutil" - minio "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" "github.com/sirupsen/logrus" "github.com/walle/targz" @@ -30,12 +30,23 @@ func main() { unlock := lock("/var/lock/dockervolumebackup.lock") defer unlock() - s := &script{} - s.must(s.init()) - s.must(s.stopContainersAndRun(s.takeBackup)) + s, err := newScript() + if err != nil { + panic(err) + } + + s.must(func() error { + restartContainers, err := s.stopContainers() + defer restartContainers() + if err != nil { + return err + } + return s.takeBackup() + }()) + s.must(s.encryptBackup()) s.must(s.copyBackup()) - s.must(s.cleanBackup()) + s.must(s.removeArtifacts()) s.must(s.pruneOldBackups()) s.logger.Info("Finished running backup tasks.") } @@ -71,25 +82,34 @@ type config struct { GpgPassphrase string `split_words:"true"` } -// init creates all resources needed for the script to perform actions against +// newScript creates all resources needed for the script to perform actions against // remote resources like the Docker engine or remote storage locations. All // reading from env vars or other configuration sources is expected to happen // in this method. -func (s *script) init() error { - s.ctx = context.Background() - s.logger = logrus.New() - s.logger.SetOutput(os.Stdout) - - s.c = &config{} - if err := envconfig.Process("", s.c); err != nil { - return fmt.Errorf("init: failed to process configuration values: %w", err) +func newScript() (*script, error) { + s := &script{ + c: &config{}, + ctx: context.Background(), + logger: &logrus.Logger{ + Out: os.Stdout, + Formatter: new(logrus.TextFormatter), + Hooks: make(logrus.LevelHooks), + Level: logrus.InfoLevel, + }, + start: time.Now(), } + if err := envconfig.Process("", s.c); err != nil { + return nil, fmt.Errorf("newScript: failed to process configuration values: %w", err) + } + + s.file = path.Join("/tmp", s.c.BackupFilename) + _, err := os.Stat("/var/run/docker.sock") if !os.IsNotExist(err) { cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { - return fmt.Errorf("init: failed to create docker client") + return nil, fmt.Errorf("newScript: failed to create docker client") } s.cli = cli } @@ -104,31 +124,29 @@ func (s *script) init() error { Secure: !s.c.AwsEndpointInsecure && s.c.AwsEndpointProto == "https", }) if err != nil { - return fmt.Errorf("init: error setting up minio client: %w", err) + return nil, fmt.Errorf("newScript: error setting up minio client: %w", err) } s.mc = mc } - s.file = path.Join("/tmp", s.c.BackupFilename) - s.start = time.Now() - - return nil + return s, nil } -// stopContainersAndRun stops all Docker containers that are marked as to being -// stopped during the backup and runs the given thunk. After returning, it makes -// sure containers are being restarted if required. In case the docker cli -// is not configured, it will call the given thunk immediately. -func (s *script) stopContainersAndRun(thunk func() error) error { +var noop = func() error { return nil } + +// stopContainers stops all Docker containers that are marked as to being +// stopped during the backup and returns a function that can be called to +// restart everything that has been stopped. +func (s *script) stopContainers() (func() error, error) { if s.cli == nil { - return thunk() + return noop, nil } allContainers, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{ Quiet: true, }) if err != nil { - return fmt.Errorf("stopContainersAndRun: error querying for containers: %w", err) + return noop, fmt.Errorf("stopContainersAndRun: error querying for containers: %w", err) } containerLabel := fmt.Sprintf( @@ -144,11 +162,11 @@ func (s *script) stopContainersAndRun(thunk func() error) error { }) if err != nil { - return fmt.Errorf("stopContainersAndRun: error querying for containers to stop: %w", err) + return noop, fmt.Errorf("stopContainersAndRun: error querying for containers to stop: %w", err) } if len(containersToStop) == 0 { - return thunk() + return noop, nil } s.logger.Infof( @@ -168,7 +186,15 @@ func (s *script) stopContainersAndRun(thunk func() error) error { } } - defer func() error { + if len(stopErrors) != 0 { + return noop, fmt.Errorf( + "stopContainersAndRun: %d error(s) stopping containers: %w", + len(stopErrors), + err, + ) + } + + return func() error { servicesRequiringUpdate := map[string]struct{}{} var restartErrors []error @@ -218,17 +244,7 @@ func (s *script) stopContainersAndRun(thunk func() error) error { len(stoppedContainers), ) return nil - }() - - if len(stopErrors) != 0 { - return fmt.Errorf( - "stopContainersAndRun: %d error(s) stopping containers: %w", - len(stopErrors), - err, - ) - } - - return thunk() + }, nil } // takeBackup creates a tar archive of the configured backup location and @@ -249,6 +265,7 @@ func (s *script) encryptBackup() error { if s.c.GpgPassphrase == "" { return nil } + defer os.Remove(s.file) gpgFile := fmt.Sprintf("%s.gpg", s.file) outFile, err := os.Create(gpgFile) @@ -276,10 +293,6 @@ func (s *script) encryptBackup() error { return fmt.Errorf("encryptBackup: error writing ciphertext to file: %w", err) } - if err := os.Remove(s.file); err != nil { - return fmt.Errorf("encryptBackup: error removing unencrpyted backup: %w", err) - } - s.file = gpgFile s.logger.Infof("Encrypted backup using given passphrase, saving as `%s`.", s.file) return nil @@ -308,12 +321,12 @@ func (s *script) copyBackup() error { return nil } -// cleanBackup removes the backup file from disk. -func (s *script) cleanBackup() error { +// removeArtifacts removes the backup file from disk. +func (s *script) removeArtifacts() error { if err := os.Remove(s.file); err != nil { - return fmt.Errorf("cleanBackup: error removing file: %w", err) + return fmt.Errorf("removeArtifacts: error removing file: %w", err) } - s.logger.Info("Cleaned up local artifacts.") + s.logger.Info("Removed local artifacts.") return nil } @@ -453,11 +466,7 @@ func (s *script) pruneOldBackups() error { func (s *script) must(err error) { if err != nil { - if s.logger == nil { - panic(err) - } - s.logger.Errorf("Fatal error running backup: %s", err) - os.Exit(1) + s.logger.Fatalf("Fatal error running backup: %s", err) } } From a0fe2cf42d0248830762422faa783c4f673c478e Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Thu, 26 Aug 2021 12:50:22 +0200 Subject: [PATCH 27/33] handle errors on container restart --- cmd/backup/main.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/backup/main.go b/cmd/backup/main.go index bda0bde..ade109d 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -37,7 +37,7 @@ func main() { s.must(func() error { restartContainers, err := s.stopContainers() - defer restartContainers() + defer s.must(restartContainers()) if err != nil { return err } @@ -391,9 +391,10 @@ func (s *script) pruneOldBackups() error { ) } s.logger.Infof( - "Pruned %d out of %d remote backup(s) as their age exceeded the configured retention period.", + "Pruned %d out of %d remote backup(s) as their age exceeded the configured retention period of %d days.", len(matches), lenCandidates, + s.c.BackupRetentionDays, ) } else if len(matches) != 0 && len(matches) == lenCandidates { s.logger.Warnf( @@ -447,9 +448,10 @@ func (s *script) pruneOldBackups() error { ) } s.logger.Infof( - "Pruned %d out of %d local backup(s) as their age exceeded the configured retention period.", + "Pruned %d out of %d local backup(s) as their age exceeded the configured retention period of %d days.", len(matches), len(candidates), + s.c.BackupRetentionDays, ) } else if len(matches) != 0 && len(matches) == len(candidates) { s.logger.Warnf( From d0eca0a179ddd70500a3bd7c5c03546223a3f7dc Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Thu, 26 Aug 2021 16:22:24 +0200 Subject: [PATCH 28/33] fix container stop execution order --- cmd/backup/main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/backup/main.go b/cmd/backup/main.go index ade109d..aa25abd 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -37,7 +37,9 @@ func main() { s.must(func() error { restartContainers, err := s.stopContainers() - defer s.must(restartContainers()) + defer func() { + s.must(restartContainers()) + }() if err != nil { return err } From 6034e6a90299fb0efa40e26757669ec88ed50075 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Fri, 27 Aug 2021 15:05:12 +0200 Subject: [PATCH 29/33] print proper local archive in log message --- cmd/backup/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/backup/main.go b/cmd/backup/main.go index aa25abd..9fceffa 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -221,7 +221,7 @@ func (s *script) stopContainers() (func() error, error) { } } if serviceMatch.ID == "" { - return fmt.Errorf("stopContainersAndRun: Couldn't find service with name %s", serviceName) + return fmt.Errorf("stopContainersAndRun: couldn't find service with name %s", serviceName) } serviceMatch.Spec.TaskTemplate.ForceUpdate = 1 _, err := s.cli.ServiceUpdate( @@ -318,7 +318,7 @@ func (s *script) copyBackup() error { if err := copy(s.file, path.Join(s.c.BackupArchive, name)); err != nil { return fmt.Errorf("copyBackup: error copying file to local archive: %w", err) } - s.logger.Infof("Stored copy of backup `%s` in local archive `%s`", s.file, s.c.AwsS3BucketName) + s.logger.Infof("Stored copy of backup `%s` in local archive `%s`", s.file, s.c.BackupArchive) } return nil } From bea203af3dbd3a8f4b6059e56b081ef2d4f115aa Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 29 Aug 2021 10:23:25 +0200 Subject: [PATCH 30/33] improve documentation --- README.md | 550 ++++++++++++++++++++++++++++++++++++--------- cmd/backup/main.go | 9 +- 2 files changed, 444 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index 185184a..7e387ae 100644 --- a/README.md +++ b/README.md @@ -2,114 +2,42 @@ Backup Docker volumes locally or to any S3 compatible storage. -The [offen/docker-volume-backup](https://hub.docker.com/r/offen/docker-volume-backup) Docker image can be used as a lightweight (below 15MB) sidecar container to an existing Docker setup. It handles __recurring or one-off backups of Docker volumes__ to a __local directory__ or __any S3 compatible storage__ (or both), and __rotates away old backups__ if configured. It also supports __encrypting your backups using GPG__. +The [offen/docker-volume-backup](https://hub.docker.com/r/offen/docker-volume-backup) Docker image can be used as a lightweight (below 15MB) sidecar container to an existing Docker setup. +It handles __recurring or one-off backups of Docker volumes__ to a __local directory__ or __any S3 compatible storage__ (or both), and __rotates away old backups__ if configured. It also supports __encrypting your backups using GPG__. -## Configuration + -Backup targets, schedule and retention are configured in environment variables: +- [Quickstart](#quickstart) +- [Configuration reference](#configuration-reference) +- [How to](#how-to) + - [Stopping containers during backup](#stopping-containers-during-backup) + - [Automatically pruning old backups](#automatically-pruning-old-backups) + - [Encrypting your backup using GPG](#encrypting-your-backup-using-gpg) + - [Restoring a volume from a backup](#restoring-a-volume-from-a-backup) + - [Using with Docker Swarm](#using-with-docker-swarm) + - [Manually triggering a backup](#manually-triggering-a-backup) +- [Recipes](#recipes) + - [Backing up to AWS S3](#backing-up-to-aws-s3) + - [Backing up to MinIO](#backing-up-to-minio) + - [Backing up locally](#backing-up-locally) + - [Backing up to AWS S3 as well as locally](#backing-up-to-aws-s3-as-well-as-locally) + - [Running on a custom cron schedule](#running-on-a-custom-cron-schedule) + - [Rotating away backups that are older than 7 days](#rotating-away-backups-that-are-older-than-7-days) + - [Encrypting your backups using GPG](#encrypting-your-backups-using-gpg) + - [Running multiple instances in the same setup](#running-multiple-instances-in-the-same-setup) +- [Differences to `futurice/docker-volume-backup`](#differences-to-futuricedocker-volume-backup) -```ini -########### BACKUP SCHEDULE + -# Backups run on the given cron schedule and use the filename defined in the -# template expression. +--- -BACKUP_CRON_EXPRESSION="0 2 * * *" -# Format verbs will be replaced as in the `date` command. Omitting them -# will result in the same filename for every backup run, which means previous -# versions will be overwritten. -BACKUP_FILENAME="backup-%Y-%m-%dT%H-%M-%S.tar.gz" +Code and documentation for `v1` versions are found on [this branch][v1-branch]. -########### BACKUP STORAGE +[v1-branch]: https://github.com/offen/docker-volume-backup/tree/v1 -# Define credentials for authenticating against the backup storage and a bucket -# name. Although all of these values are `AWS`-prefixed, the setup can be used -# with any S3 compatible storage. +## Quickstart -AWS_ACCESS_KEY_ID="" -AWS_SECRET_ACCESS_KEY="" -AWS_S3_BUCKET_NAME="" - -# This is the FQDN of your storage server, e.g. `storage.example.com`. -# Do not set this when working against AWS S3. If you need to set a -# specific protocol, you will need to use the option below. - -# AWS_ENDPOINT="" - -# The protocol to be used when communicating with your storage server. -# Defaults to "https". You can set this to "http" when communicating with -# a different Docker container on the same host for example. - -# AWS_ENDPOINT_PROTO="https" - -# Setting this variable to any value will disable verification of -# SSL certificates. You shouldn't use this unless you use self-signed -# certificates for your remote storage backend. - -# AWS_ENDPOINT_INSECURE="true" - -# In addition to backing up you can also store backups locally. Pass in -# a local path to store your backups here if needed. You likely want to -# mount a local folder or Docker volume into that location when running -# the container. Local paths can also be subject to pruning of old -# backups as defined below. - -# BACKUP_ARCHIVE="/archive" - -########### BACKUP PRUNING - -# **IMPORTANT, PLEASE READ THIS BEFORE USING THIS FEATURE**: -# The mechanism used for pruning backups is not very sophisticated -# and applies its rules to **all files in the target directory** by default, -# which means that if you are storing your backups next to other files, -# these might become subject to deletion too. When using this option -# make sure the backup files are stored in a directory used exclusively -# for storing them or to configure BACKUP_PRUNING_PREFIX to limit -# removal to certain files. - -# Define this value to enable automatic pruning of old backups. The value -# declares the number of days for which a backup is kept. - -# BACKUP_RETENTION_DAYS="7" - -# In case the duration a backup takes fluctuates noticeably in your setup -# you can adjust this setting to make sure there are no race conditions -# between the backup finishing and the rotation not deleting backups that -# sit on the very edge of the time window. Set this value to a duration -# that is expected to be bigger than the maximum difference of backups. -# Valid values have a suffix of (s)econds, (m)inutes or (h)ours. - -# BACKUP_PRUNING_LEEWAY="1m" - -# In case your target bucket or directory contains other files than the ones -# managed by this container, you can limit the scope of rotation by setting -# a prefix value. This would usually be the non-parametrized part of your -# BACKUP_FILENAME. E.g. if BACKUP_FILENAME is `db-backup-%Y-%m-%dT%H-%M-%S.tar.gz`, -# you can set BACKUP_PRUNING_PREFIX to `db-backup-` and make sure -# unrelated files are not affected. - -# BACKUP_PRUNING_PREFIX="backup-" - -########### BACKUP ENCRYPTION - -# Backups can be encrypted using gpg in case a passphrase is given - -# GPG_PASSPHRASE="" - -########### STOPPING CONTAINERS DURING BACKUP - -# Containers can be stopped by applying a -# `docker-volume-backup.stop-during-backup` label. By default, all containers -# that are labeled with `true` will be stopped. If you need more fine grained -# control (e.g. when running multiple containers based on this image), you can -# override this default by specifying a different value here. - -# BACKUP_STOP_CONTAINER_LABEL="service1" -``` - -## Example in a docker-compose setup - -Most likely, you will use this image as a sidecar container in an existing docker-compose setup like this: +Add a `backup` service to your compose setup and mount the volumes you would like to see backed up: ```yml version: '3' @@ -129,24 +57,232 @@ services: backup: image: offen/docker-volume-backup:latest restart: always - env_file: ./backup.env + env_file: ./backup.env # see below for configuration reference volumes: + - data:/backup/my-app-backup:ro # Mounting the Docker socket allows the script to stop and restart # the container during backup. You can omit this if you don't want # to stop the container - /var/run/docker.sock:/var/run/docker.sock:ro - - data:/backup/my-app-backup:ro # If you mount a local directory or volume to `/archive` a local # copy of the backup will be stored there. You can override the - # location inside of the container by setting `BACKUP_ARCHIVE` - # - /path/to/local_backups:/archive + # location inside of the container by setting `BACKUP_ARCHIVE`. + # You can omit this if you do not want to keep local backups. + - /path/to/local_backups:/archive volumes: data: ``` -## Using with Docker Swarm +## Configuration reference -By default, Docker Swarm will restart stopped containers automatically, even when manually stopped. If you plan to have your containers / services stopped during backup, this means you need to apply the `on-failure` restart policy to your service's definitions. A restart policy of `always` is not compatible with this tool. +Backup targets, schedule and retention are configured in environment variables. +You can populate below template according to your requirements and use it as your `env_file`: + +```ini +########### BACKUP SCHEDULE + +# Backups run on the given cron schedule in `busybox` flavor. If no +# value is set, `@daily` will be used. If you do not want the cron +# to ever run, use `0 0 5 31 2 ?`. + +# BACKUP_CRON_EXPRESSION="0 2 * * *" + +# The name of the backup file including the `.tar.gz` extension. +# Format verbs will be replaced as in `strftime`. Omitting them +# will result in the same filename for every backup run, which means previous +# versions will be overwritten on subsequent runs. The default results +# in filenames like `backup-2021-08-29T04-00-00.tar.gz`. + +# BACKUP_FILENAME="backup-%Y-%m-%dT%H-%M-%S.tar.gz" + +########### BACKUP STORAGE + +# The name of the remote bucket that should be used for storing backups. If +# this is not set, no remote backups will be stored. + +# AWS_S3_BUCKET_NAME="backup-bucket" + +# Define credentials for authenticating against the backup storage and a bucket +# name. Although all of these keys are `AWS`-prefixed, the setup can be used +# with any S3 compatible storage. + +# AWS_ACCESS_KEY_ID="" +# AWS_SECRET_ACCESS_KEY="" + +# This is the FQDN of your storage server, e.g. `storage.example.com`. +# Do not set this when working against AWS S3 (the default value is +# `s3.amazonaws.com`). If you need to set a specific (non-https) protocol, you +# will need to use the option below. + +# AWS_ENDPOINT="storage.example.com" + +# The protocol to be used when communicating with your storage server. +# Defaults to "https". You can set this to "http" when communicating with +# a different Docker container on the same host for example. + +# AWS_ENDPOINT_PROTO="https" + +# Setting this variable to `true` will disable verification of +# SSL certificates. You shouldn't use this unless you use self-signed +# certificates for your remote storage backend. + +# AWS_ENDPOINT_INSECURE="true" + +# In addition to storing backups remotely, you can also keep local copies. +# Pass a container-local path to store your backups if needed. You also need to +# mount a local folder or Docker volume into that location (`/archive` +# by default) when running the container. In case the specified directory does +# not exist (nothing is mounted) in the container when the backup is running, +# local backups will be skipped. Local paths are also be subject to pruning of +# old backups as defined below. + +# BACKUP_ARCHIVE="/archive" + +########### BACKUP PRUNING + +# **IMPORTANT, PLEASE READ THIS BEFORE USING THIS FEATURE**: +# The mechanism used for pruning old backups is not very sophisticated +# and applies its rules to **all files in the target directory** by default, +# which means that if you are storing your backups next to other files, +# these might become subject to deletion too. When using this option +# make sure the backup files are stored in a directory used exclusively +# for such files, or to configure BACKUP_PRUNING_PREFIX to limit +# removal to certain files. + +# Define this value to enable automatic rotation of old backups. The value +# declares the number of days for which a backup is kept. + +# BACKUP_RETENTION_DAYS="7" + +# In case the duration a backup takes fluctuates noticeably in your setup +# you can adjust this setting to make sure there are no race conditions +# between the backup finishing and the rotation not deleting backups that +# sit on the edge of the time window. Set this value to a duration +# that is expected to be bigger than the maximum difference of backups. +# Valid values have a suffix of (s)econds, (m)inutes or (h)ours. By default, +# one minute is used. + +# BACKUP_PRUNING_LEEWAY="1m" + +# In case your target bucket or directory contains other files than the ones +# managed by this container, you can limit the scope of rotation by setting +# a prefix value. This would usually be the non-parametrized part of your +# BACKUP_FILENAME. E.g. if BACKUP_FILENAME is `db-backup-%Y-%m-%dT%H-%M-%S.tar.gz`, +# you can set BACKUP_PRUNING_PREFIX to `db-backup-` and make sure +# unrelated files are not affected by the rotation mechanism. + +# BACKUP_PRUNING_PREFIX="backup-" + +########### BACKUP ENCRYPTION + +# Backups can be encrypted using gpg in case a passphrase is given. + +# GPG_PASSPHRASE="" + +########### STOPPING CONTAINERS DURING BACKUP + +# Containers can be stopped by applying a +# `docker-volume-backup.stop-during-backup` label. By default, all containers +# that are labeled with `true` will be stopped. If you need more fine grained +# control (e.g. when running multiple containers based on this image), you can +# override this default by specifying a different value here. + +# BACKUP_STOP_CONTAINER_LABEL="service1" +``` + +## How to + +### Stopping containers during backup + +In many cases, it will be desirable to stop the services that are consuming the volume you want to backup in order to ensure data integrity. +This image can automatically stop and restart containers and services (in case you are running Docker in Swarm mode). +By default, any container that is labeled `docker-volume-backup.stop-during-backup=true` will be stopped before the backup is being taken and restarted once it has finished. + +In case you need more fine grained control about which containers should be stopped (e.g. when backing up multiple volumes on different schedules), you can set the `BACKUP_STOP_CONTAINER_LABEL` environment variable and then use the same value for labeling: + +```yml +version: '3' + +services: + app: + # definition for app ... + labels: + - docker-volume-backup.stop-during-backup=service1 + + backup: + image: offen/docker-volume-backup:latest + environment: + BACKUP_STOP_CONTAINER_LABEL: service1 + volumes: + - data:/backup/my-app-backup:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + +volumes: + data: +``` + +### Automatically pruning old backups + +When `BACKUP_RETENTION_DAYS` is configured, the image will check if there are any backups in the remote bucket or local archive that are older than the given retention value and rotate these backups away. + +Be aware that this mechanism looks at __all files in the target bucket or archive__, which means that other files that are older than the given deadline are deleted as well. In case you need to use a target that cannot be used exclusively for your backups, you can configure `BACKUP_PRUNING_PREFIX` to limit which files are considered eligible for deletion: + +```yml +version: '3' + +services: + # ... define other services using the `data` volume here + backup: + image: offen/docker-volume-backup:latest + environment: + BACKUP_FILENAME: backup-%Y-%m-%dT%H-%M-%S.tar.gz + BACKUP_PRUNING_PREFIX: backup- + BACKUP_RETENTION_DAYS: 7 + volumes: + - ${HOME}/backups:/archive + - data:/backup/my-app-backup:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + +volumes: + data: +``` + +### Encrypting your backup using GPG + +The image supports encrypting backups using GPG out of the box. +In case a `GPG_PASSPHRASE` environment variable is set, the backup will be encrypted using the given key and saved as a `.gpg` file instead. + +Assuming you have `gpg` installed, you can decrypt such a backup using (your OS will prompt for the passphrase before decryption can happen): + +```console +gpg -o backup.tar.gz -d backup.tar.gz.gpg +``` + +### Restoring a volume from a backup + +In case you need to restore a volume from a backup, the most straight forward procedure to do so would be: + +- Stop the container(s) that are using the volume +- Untar the backup you want to restore + ```console + tar -C /tmp -xvf backup.tar.gz + ``` +- Using a temporary one-off container, mount the volume (the example assumes it's named `data`) and copy over the backup. Make sure you copy the correct path level (this depends on how you mount your volume into the backup container), you might need to strip some leading elements + ```console + docker run -d --name backup_restore -v data:/backup_restore alpine + docker cp /tmp/backup/data-backup backup_restore:/backup_restore + docker stop backup_restore + docker rm backup_restore + ``` +- Restart the container(s) that are using the volume + +Depending on your setup and the application(s) you are running, this might involve other steps to be taken still. + +### Using with Docker Swarm + +By default, Docker Swarm will restart stopped containers automatically, even when manually stopped. +If you plan to have your containers / services stopped during backup, this means you need to apply the `on-failure` restart policy to your service's definitions. +A restart policy of `always` is not compatible with this tool. --- @@ -162,7 +298,7 @@ services: memory: 25M ``` -## Manually triggering a backup +### Manually triggering a backup You can manually trigger a backup run outside of the defined cron schedule by executing the `backup` command inside the container: @@ -170,15 +306,207 @@ You can manually trigger a backup run outside of the defined cron schedule by ex docker exec backup ``` ---- +## Recipes + +This section lists configuration for some real-world use cases that you can mix and match according to your needs. + +### Backing up to AWS S3 + +```yml +version: '3' + +services: + # ... define other services using the `data` volume here + backup: + image: offen/docker-volume-backup:latest + environment: + AWS_BUCKET_NAME: backup-bucket + AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE + AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + volumes: + - data:/backup/my-app-backup:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + +volumes: + data: +``` + +### Backing up to MinIO + +```yml +version: '3' + +services: + # ... define other services using the `data` volume here + backup: + image: offen/docker-volume-backup:latest + environment: + AWS_ENDPOINT: minio.example.com + AWS_BUCKET_NAME: backup-bucket + AWS_ACCESS_KEY_ID: MINIOACCESSKEY + AWS_SECRET_ACCESS_KEY: MINIOSECRETKEY + volumes: + - data:/backup/my-app-backup:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + +volumes: + data: +``` + +### Backing up locally + +```yml +version: '3' + +services: + # ... define other services using the `data` volume here + backup: + image: offen/docker-volume-backup:latest + volumes: + - data:/backup/my-app-backup:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - ${HOME}/backups:/archive + +volumes: + data: +``` + +### Backing up to AWS S3 as well as locally + +```yml +version: '3' + +services: + # ... define other services using the `data` volume here + backup: + image: offen/docker-volume-backup:latest + environment: + AWS_BUCKET_NAME: backup-bucket + AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE + AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + volumes: + - data:/backup/my-app-backup:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - ${HOME}/backups:/archive + +volumes: + data: +``` + +### Running on a custom cron schedule + +```yml +version: '3' + +services: + # ... define other services using the `data` volume here + backup: + image: offen/docker-volume-backup:latest + environment: + # take a backup on every hour + BACKUP_CRON_EXPRESSION: "0 * * * *" + AWS_BUCKET_NAME: backup-bucket + AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE + AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + volumes: + - data:/backup/my-app-backup:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + +volumes: + data: +``` + +### Rotating away backups that are older than 7 days + +```yml +version: '3' + +services: + # ... define other services using the `data` volume here + backup: + image: offen/docker-volume-backup:latest + environment: + AWS_BUCKET_NAME: backup-bucket + AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE + AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + BACKUP_FILENAME: backup-%Y-%m-%dT%H-%M-%S.tar.gz + BACKUP_PRUNING_PREFIX: backup- + BACKUP_RETENTION_DAYS: 7 + volumes: + - data:/backup/my-app-backup:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + +volumes: + data: +``` + +### Encrypting your backups using GPG + +```yml +version: '3' + +services: + # ... define other services using the `data` volume here + backup: + image: offen/docker-volume-backup:latest + environment: + AWS_BUCKET_NAME: backup-bucket + AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE + AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + GPG_PASSPHRASE: somesecretstring + volumes: + - data:/backup/my-app-backup:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + +volumes: + data: +``` + +### Running multiple instances in the same setup + +```yml +version: '3' + +services: + # ... define other services using the `data_1` and `data_2` volumes here + backup_1: &backup_service + image: offen/docker-volume-backup:latest + environment: &backup_environment + BACKUP_CRON_EXPRESSION: "0 2 * * *" + AWS_BUCKET_NAME: backup-bucket + AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE + AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY + # Label the container using the `data_1` volume as `docker-volume-backup.stop-during-backup=service1` + BACKUP_STOP_CONTAINER_LABEL: service1 + volumes: + - data_1:/backup/data-1-backup:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + backup_2: + <<: *backup_service + environment: + <<: *backup_environment + # Label the container using the `data_2` volume as `docker-volume-backup.stop-during-backup=service2` + BACKUP_CRON_EXPRESSION: "0 3 * * *" + BACKUP_STOP_CONTAINER_LABEL: service2 + volumes: + - data_2:/backup/data-2-backup:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + +volumes: + data_1: + data_2: +``` ## Differences to `futurice/docker-volume-backup` This image is heavily inspired by the `futurice/docker-volume-backup`. We decided to publish this image as a simpler and more lightweight alternative because of the following requirements: -- The original image is based on `ubuntu` and additional tools, making it very heavy. This version is roughly 1/25 in compressed size (it's ~12MB). -- The original image uses a shell script, when this is written in Go, which makes it easier to extend and maintain (more verbose also). -- The original image proposed to handle backup rotation through AWS S3 lifecycle policies. This image adds the option to rotate away old backups through the same command so this functionality can also be offered for non-AWS storage backends like MinIO. Local copies of backups can also be pruned once they reach a certain age. +- The original image is based on `ubuntu` and requires additional tools, making it heavy. +This version is roughly 1/25 in compressed size (it's ~12MB). +- The original image uses a shell script, when this version is written in Go, which makes it easier to extend and maintain (more verbose too). +- The original image proposed to handle backup rotation through AWS S3 lifecycle policies. +This image adds the option to rotate away old backups through the same command so this functionality can also be offered for non-AWS storage backends like MinIO. +Local copies of backups can also be pruned once they reach a certain age. - InfluxDB specific functionality from the original image was removed. - `arm64` and `arm/v7` architectures are supported. - Docker in Swarm mode is supported. diff --git a/cmd/backup/main.go b/cmd/backup/main.go index 9fceffa..3291ed7 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -262,7 +262,7 @@ func (s *script) takeBackup() error { // encryptBackup encrypts the backup file using PGP and the configured passphrase. // In case no passphrase is given it returns early, leaving the backup file -// untouched. +// untouched. func (s *script) encryptBackup() error { if s.c.GpgPassphrase == "" { return nil @@ -311,14 +311,14 @@ func (s *script) copyBackup() error { if err != nil { return fmt.Errorf("copyBackup: error uploading backup to remote storage: %w", err) } - s.logger.Infof("Uploaded a copy of backup `%s` to bucket `%s`", s.file, s.c.AwsS3BucketName) + s.logger.Infof("Uploaded a copy of backup `%s` to bucket `%s`.", s.file, s.c.AwsS3BucketName) } if _, err := os.Stat(s.c.BackupArchive); !os.IsNotExist(err) { if err := copy(s.file, path.Join(s.c.BackupArchive, name)); err != nil { return fmt.Errorf("copyBackup: error copying file to local archive: %w", err) } - s.logger.Infof("Stored copy of backup `%s` in local archive `%s`", s.file, s.c.BackupArchive) + s.logger.Infof("Stored copy of backup `%s` in local archive `%s`.", s.file, s.c.BackupArchive) } return nil } @@ -345,7 +345,6 @@ func (s *script) pruneOldBackups() error { time.Sleep(s.c.BackupPruningLeeway) } - s.logger.Infof("Trying to prune backups older than %d day(s) now.", s.c.BackupRetentionDays) deadline := time.Now().AddDate(0, 0, -int(s.c.BackupRetentionDays)) if s.c.AwsS3BucketName != "" { @@ -468,6 +467,8 @@ func (s *script) pruneOldBackups() error { return nil } +// must exits the script run non-zero and prematurely in case the given error +// is non-nil. func (s *script) must(err error) { if err != nil { s.logger.Fatalf("Fatal error running backup: %s", err) From 825cbb50eff7eaacc56043e4b320d3c7a617b138 Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 29 Aug 2021 18:26:40 +0200 Subject: [PATCH 31/33] always use background context directly --- cmd/backup/main.go | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/cmd/backup/main.go b/cmd/backup/main.go index 3291ed7..0d515a9 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -56,7 +56,6 @@ func main() { // script holds all the stateful information required to orchestrate a // single backup run. type script struct { - ctx context.Context cli *client.Client mc *minio.Client logger *logrus.Logger @@ -90,8 +89,7 @@ type config struct { // in this method. func newScript() (*script, error) { s := &script{ - c: &config{}, - ctx: context.Background(), + c: &config{}, logger: &logrus.Logger{ Out: os.Stdout, Formatter: new(logrus.TextFormatter), @@ -144,7 +142,7 @@ func (s *script) stopContainers() (func() error, error) { return noop, nil } - allContainers, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{ + allContainers, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{ Quiet: true, }) if err != nil { @@ -155,7 +153,7 @@ func (s *script) stopContainers() (func() error, error) { "docker-volume-backup.stop-during-backup=%s", s.c.BackupStopContainerLabel, ) - containersToStop, err := s.cli.ContainerList(s.ctx, types.ContainerListOptions{ + containersToStop, err := s.cli.ContainerList(context.Background(), types.ContainerListOptions{ Quiet: true, Filters: filters.NewArgs(filters.KeyValuePair{ Key: "label", @@ -181,7 +179,7 @@ func (s *script) stopContainers() (func() error, error) { var stoppedContainers []types.Container var stopErrors []error for _, container := range containersToStop { - if err := s.cli.ContainerStop(s.ctx, container.ID, nil); err != nil { + if err := s.cli.ContainerStop(context.Background(), container.ID, nil); err != nil { stopErrors = append(stopErrors, err) } else { stoppedContainers = append(stoppedContainers, container) @@ -205,13 +203,13 @@ func (s *script) stopContainers() (func() error, error) { servicesRequiringUpdate[swarmServiceName] = struct{}{} continue } - if err := s.cli.ContainerStart(s.ctx, container.ID, types.ContainerStartOptions{}); err != nil { + if err := s.cli.ContainerStart(context.Background(), container.ID, types.ContainerStartOptions{}); err != nil { restartErrors = append(restartErrors, err) } } if len(servicesRequiringUpdate) != 0 { - services, _ := s.cli.ServiceList(s.ctx, types.ServiceListOptions{}) + services, _ := s.cli.ServiceList(context.Background(), types.ServiceListOptions{}) for serviceName := range servicesRequiringUpdate { var serviceMatch swarm.Service for _, service := range services { @@ -225,7 +223,7 @@ func (s *script) stopContainers() (func() error, error) { } serviceMatch.Spec.TaskTemplate.ForceUpdate = 1 _, err := s.cli.ServiceUpdate( - s.ctx, serviceMatch.ID, + context.Background(), serviceMatch.ID, serviceMatch.Version, serviceMatch.Spec, types.ServiceUpdateOptions{}, ) if err != nil { @@ -304,8 +302,8 @@ func (s *script) encryptBackup() error { // as per the given configuration. func (s *script) copyBackup() error { _, name := path.Split(s.file) - if s.c.AwsS3BucketName != "" { - _, err := s.mc.FPutObject(s.ctx, s.c.AwsS3BucketName, name, s.file, minio.PutObjectOptions{ + if s.mc != nil { + _, err := s.mc.FPutObject(context.Background(), s.c.AwsS3BucketName, name, s.file, minio.PutObjectOptions{ ContentType: "application/tar+gzip", }) if err != nil { @@ -347,8 +345,8 @@ func (s *script) pruneOldBackups() error { deadline := time.Now().AddDate(0, 0, -int(s.c.BackupRetentionDays)) - if s.c.AwsS3BucketName != "" { - candidates := s.mc.ListObjects(s.ctx, s.c.AwsS3BucketName, minio.ListObjectsOptions{ + if s.mc != nil { + candidates := s.mc.ListObjects(context.Background(), s.c.AwsS3BucketName, minio.ListObjectsOptions{ WithMetadata: true, Prefix: s.c.BackupPruningPrefix, }) @@ -376,7 +374,7 @@ func (s *script) pruneOldBackups() error { } close(objectsCh) }() - errChan := s.mc.RemoveObjects(s.ctx, s.c.AwsS3BucketName, objectsCh, minio.RemoveObjectsOptions{}) + errChan := s.mc.RemoveObjects(context.Background(), s.c.AwsS3BucketName, objectsCh, minio.RemoveObjectsOptions{}) var errors []error for result := range errChan { if result.Err != nil { From aae97a561757e1f2c74b6d53f86ed44ae596fc4e Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 29 Aug 2021 18:51:05 +0200 Subject: [PATCH 32/33] try restarting even when stopping some containers failed --- cmd/backup/main.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cmd/backup/main.go b/cmd/backup/main.go index 0d515a9..d475337 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -186,8 +186,9 @@ func (s *script) stopContainers() (func() error, error) { } } + var stopError error if len(stopErrors) != 0 { - return noop, fmt.Errorf( + stopError = fmt.Errorf( "stopContainersAndRun: %d error(s) stopping containers: %w", len(stopErrors), err, @@ -244,7 +245,7 @@ func (s *script) stopContainers() (func() error, error) { len(stoppedContainers), ) return nil - }, nil + }, stopError } // takeBackup creates a tar archive of the configured backup location and From ede94bcd88e41a8b31f6d07cd22a242ef823443e Mon Sep 17 00:00:00 2001 From: Frederik Ring Date: Sun, 29 Aug 2021 19:39:51 +0200 Subject: [PATCH 33/33] display all error messages instead of first one --- cmd/backup/main.go | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/cmd/backup/main.go b/cmd/backup/main.go index d475337..dd0c067 100644 --- a/cmd/backup/main.go +++ b/cmd/backup/main.go @@ -5,11 +5,13 @@ package main import ( "context" + "errors" "fmt" "io" "os" "path" "path/filepath" + "strings" "time" "github.com/docker/docker/api/types" @@ -191,7 +193,7 @@ func (s *script) stopContainers() (func() error, error) { stopError = fmt.Errorf( "stopContainersAndRun: %d error(s) stopping containers: %w", len(stopErrors), - err, + join(stopErrors...), ) } @@ -237,7 +239,7 @@ func (s *script) stopContainers() (func() error, error) { return fmt.Errorf( "stopContainersAndRun: %d error(s) restarting containers and services: %w", len(restartErrors), - err, + join(restartErrors...), ) } s.logger.Infof( @@ -376,18 +378,18 @@ func (s *script) pruneOldBackups() error { close(objectsCh) }() errChan := s.mc.RemoveObjects(context.Background(), s.c.AwsS3BucketName, objectsCh, minio.RemoveObjectsOptions{}) - var errors []error + var removeErrors []error for result := range errChan { if result.Err != nil { - errors = append(errors, result.Err) + removeErrors = append(removeErrors, result.Err) } } - if len(errors) != 0 { + if len(removeErrors) != 0 { return fmt.Errorf( "pruneOldBackups: %d error(s) removing files from remote storage: %w", - len(errors), - errors[0], + len(removeErrors), + join(removeErrors...), ) } s.logger.Infof( @@ -434,17 +436,17 @@ func (s *script) pruneOldBackups() error { } if len(matches) != 0 && len(matches) != len(candidates) { - var errors []error + var removeErrors []error for _, candidate := range matches { if err := os.Remove(candidate); err != nil { - errors = append(errors, err) + removeErrors = append(removeErrors, err) } } - if len(errors) != 0 { + if len(removeErrors) != 0 { return fmt.Errorf( "pruneOldBackups: %d error(s) deleting local files, starting with: %w", - len(errors), - errors[0], + len(removeErrors), + join(removeErrors...), ) } s.logger.Infof( @@ -509,3 +511,18 @@ func copy(src, dst string) error { } return out.Close() } + +// join takes a list of errors and joins them into a single error +func join(errs ...error) error { + if len(errs) == 1 { + return errs[0] + } + var msgs []string + for _, err := range errs { + if err == nil { + continue + } + msgs = append(msgs, err.Error()) + } + return errors.New("[" + strings.Join(msgs, ", ") + "]") +}