├── .dockerignore ├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── build.sh ├── chart ├── .helmignore ├── Chart.yaml ├── LICENSE ├── README.md ├── ci │ └── ci-values.yaml ├── notes.md ├── templates │ ├── _helpers.tpl │ ├── configmap.yaml │ ├── ingress-1.19.yaml │ ├── ingress.yaml │ ├── secret.yaml │ ├── service.yaml │ ├── serviceaccount.yaml │ └── statefulset.yaml └── values.yaml ├── cmd └── mgob │ └── mgob.go ├── go.mod ├── go.sum ├── k8s ├── README.md ├── mgob-cfg.yaml ├── mgob-dep.yaml ├── mgob-gstore-cfg.yaml ├── mgob-gstore-dep.yaml ├── mongo-ds.yaml ├── mongo-rs.yaml ├── namespace.yaml └── storage.yaml ├── pkg ├── api │ ├── backup.go │ ├── metrics.go │ ├── server.go │ ├── status.go │ └── version.go ├── backup │ ├── azure.go │ ├── backup.go │ ├── checks.go │ ├── encrypt.go │ ├── gcloud.go │ ├── local.go │ ├── rclone.go │ ├── result.go │ ├── s3.go │ └── sftp.go ├── config │ ├── app.go │ ├── modules.go │ └── plan.go ├── db │ ├── stats.go │ └── store.go ├── metrics │ └── metrics.go ├── notifier │ ├── notifier.go │ ├── slack.go │ └── smtp.go └── scheduler │ └── scheduler.go ├── screens ├── gke-diagram.png └── mgob-gcp.png └── test ├── config ├── mongo-dev.yml └── mongo-test.yml └── travis ├── mongo-test.yml └── sftp-authorization-test.yml /.dockerignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /test/ 3 | /k8s/ 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | /.idea 27 | /test/storage/ 28 | /test/tmp/ 29 | /test/config/mongo-debug.yml 30 | /test/data/ 31 | 32 | /k8s/service-account.json 33 | 34 | # working with relative dirs from project for testing 35 | /data/ 36 | /config/*.yml 37 | 38 | 39 | # intelij 40 | .idea 41 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: go 3 | 4 | go: 5 | - 1.13.x 6 | 7 | services: 8 | - docker 9 | - mongodb 10 | 11 | before_install: 12 | - docker run -dp 9000:9000 -e "MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE" -e "MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" minio/minio server /export 13 | - sudo curl -s -o /usr/bin/mc https://dl.minio.io/client/mc/release/linux-amd64/mc 14 | - sudo chmod u+x /usr/bin/mc 15 | - docker run -dp 20022:22 atmoz/sftp:alpine test:test:::backup 16 | - ssh-keygen -b 4096 -t rsa -N "" -f /tmp/ssh_host_rsa_key -q 17 | - >- 18 | docker run -dp 20023:22 19 | -v /tmp/ssh_host_rsa_key.pub:/home/test/.ssh/keys/ssh_host_rsa_key.pub:ro 20 | -v /tmp/ssh_host_rsa_key:/etc/ssh/ssh_host_rsa_key 21 | --name test-sftp atmoz/sftp:alpine test::1001::backup 22 | 23 | before_script: 24 | - sleep 10 25 | - >- 26 | mongo test --eval 'db.test.insert({item: "item", val: "test" });' 27 | - sudo mc config host add local http://127.0.0.1:9000 AKIAIOSFODNN7EXAMPLE wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY --api S3v4 28 | - sudo mc mb local/backup 29 | 30 | script: 31 | - make travis 32 | - sleep 90 33 | - docker logs mgob 34 | - curl http://localhost:8090/version 35 | - echo 'SFTP integration test' 36 | - docker logs mgob 2>&1 | grep 'SFTP upload finished' 37 | - echo 'S3 integration test' 38 | - docker logs mgob 2>&1 | grep 'S3 upload finished' 39 | - echo 'Local backup integration test' 40 | - docker logs mgob 2>&1 | grep 'Backup finished' 41 | - echo 'SFTP private key authorization integration test' 42 | - docker logs mgob 2>&1 | grep "Backup finished .* sftp-authorization-test" 43 | 44 | after_success: 45 | - if [ -z "$DOCKER_USER" ]; then 46 | echo "PR build, skipping Docker Hub push"; 47 | else 48 | make publish; 49 | fi 50 | 51 | deploy: 52 | provider: script 53 | script: make release 54 | on: 55 | tags: true 56 | branch: master 57 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG MONGODB_TOOLS_VERSION=100.5.1 2 | ARG EN_AWS_CLI=true 3 | ARG AWS_CLI_VERSION=1.22.46 4 | ARG EN_AZURE=true 5 | ARG AZURE_CLI_VERSION=2.32.0 6 | ARG EN_GCLOUD=true 7 | ARG GOOGLE_CLOUD_SDK_VERSION=370.0.0 8 | ARG EN_GPG=true 9 | ARG GNUPG_VERSION="2.2.31-r0" 10 | ARG EN_MINIO=true 11 | ARG EN_RCLONE=true 12 | 13 | FROM golang:1.17 as mgob-builder 14 | 15 | ARG VERSION 16 | 17 | COPY . /go/src/github.com/stefanprodan/mgob 18 | 19 | WORKDIR /go/src/github.com/stefanprodan/mgob 20 | 21 | RUN CGO_ENABLED=0 GOOS=linux \ 22 | go build \ 23 | -ldflags "-X main.version=$VERSION" \ 24 | -a -installsuffix cgo \ 25 | -o mgob github.com/stefanprodan/mgob/cmd/mgob 26 | 27 | FROM golang:1.17-alpine3.15 as tools-builder 28 | 29 | ARG MONGODB_TOOLS_VERSION 30 | 31 | RUN apk add --no-cache git build-base krb5-dev \ 32 | && git clone https://github.com/mongodb/mongo-tools.git --depth 1 --branch $MONGODB_TOOLS_VERSION 33 | 34 | WORKDIR mongo-tools 35 | RUN ./make build 36 | 37 | FROM alpine:3.15 38 | 39 | ARG BUILD_DATE 40 | ARG VCS_REF 41 | ARG VERSION 42 | ARG MONGODB_TOOLS_VERSION 43 | ARG AWS_CLI_VERSION 44 | ARG AZURE_CLI_VERSION 45 | ARG GOOGLE_CLOUD_SDK_VERSION 46 | ARG GNUPG_VERSION 47 | ARG EN_AWS_CLI 48 | ARG EN_AZURE 49 | ARG EN_GCLOUD 50 | ARG EN_GPG 51 | ARG EN_MINIO 52 | ARG EN_RCLONE 53 | 54 | ENV MONGODB_TOOLS_VERSION=$MONGODB_TOOLS_VERSION \ 55 | GNUPG_VERSION=$GNUPG_VERSION \ 56 | GOOGLE_CLOUD_SDK_VERSION=$GOOGLE_CLOUD_SDK_VERSION \ 57 | AZURE_CLI_VERSION=$AZURE_CLI_VERSION \ 58 | AWS_CLI_VERSION=$AWS_CLI_VERSION \ 59 | MGOB_EN_AWS_CLI=$EN_AWS_CLI \ 60 | MGOB_EN_AZURE=$EN_AZURE \ 61 | MGOB_EN_GCLOUD=$EN_GCLOUD \ 62 | MGOB_EN_GPG=$EN_GPG \ 63 | MGOB_EN_MINIO=$EN_MINIO \ 64 | MGOB_EN_RCLONE=$EN_RCLONE 65 | 66 | WORKDIR / 67 | 68 | COPY build.sh /tmp 69 | RUN /tmp/build.sh 70 | 71 | COPY --from=mgob-builder /go/src/github.com/stefanprodan/mgob/mgob . 72 | COPY --from=tools-builder /go/mongo-tools/bin/* /usr/bin/ 73 | 74 | VOLUME ["/config", "/storage", "/tmp", "/data"] 75 | 76 | LABEL org.label-schema.build-date=$BUILD_DATE \ 77 | org.label-schema.name="mgob" \ 78 | org.label-schema.description="MongoDB backup automation tool" \ 79 | org.label-schema.url="https://github.com/stefanprodan/mgob" \ 80 | org.label-schema.vcs-ref=$VCS_REF \ 81 | org.label-schema.vcs-url="https://github.com/stefanprodan/mgob" \ 82 | org.label-schema.vendor="stefanprodan.com" \ 83 | org.label-schema.version=$VERSION \ 84 | org.label-schema.schema-version="1.0" 85 | 86 | ENTRYPOINT [ "./mgob" ] 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Stefan Prodan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL:=/bin/bash 2 | 3 | APP_VERSION?=1.5 4 | 5 | # build vars 6 | BUILD_DATE:=$(shell date -u +"%Y-%m-%dT%H:%M:%SZ") 7 | REPOSITORY?=stefanprodan 8 | 9 | #run vars 10 | CONFIG:=$$(pwd)/test/config 11 | TRAVIS:=$$(pwd)/test/travis 12 | 13 | # go tools 14 | PACKAGES:=$(shell go list ./... | grep -v '/vendor/') 15 | VETARGS:=-asmdecl -atomic -bool -buildtags -copylocks -methods -nilfunc -rangeloops -shift -structtags -unsafeptr 16 | 17 | build: 18 | @echo ">>> Building $(REPOSITORY)/mgob:$(APP_VERSION)" 19 | @docker build \ 20 | --build-arg BUILD_DATE=$(BUILD_DATE) \ 21 | --build-arg VCS_REF=$(TRAVIS_COMMIT) \ 22 | --build-arg VERSION=$(APP_VERSION) \ 23 | -t $(REPOSITORY)/mgob:$(APP_VERSION) . 24 | 25 | aws: 26 | @echo ">>> Building $(REPOSITORY)/mgob:$(APP_VERSION)" 27 | @docker build \ 28 | --build-arg BUILD_DATE=$(BUILD_DATE) \ 29 | --build-arg VCS_REF=$(TRAVIS_COMMIT) \ 30 | --build-arg VERSION=$(APP_VERSION) \ 31 | --build-arg EN_AWS_CLI=true \ 32 | --build-arg EN_AZURE=false \ 33 | --build-arg EN_GCLOUD=false \ 34 | --build-arg EN_MINIO=false \ 35 | --build-arg EN_RCLONE=false \ 36 | --build-arg EN_GPG=true \ 37 | -t $(REPOSITORY)/mgob:$(APP_VERSION)-aws . 38 | 39 | azure: 40 | @echo ">>> Building $(REPOSITORY)/mgob:$(APP_VERSION)" 41 | @docker build \ 42 | --build-arg BUILD_DATE=$(BUILD_DATE) \ 43 | --build-arg VCS_REF=$(TRAVIS_COMMIT) \ 44 | --build-arg VERSION=$(APP_VERSION) \ 45 | --build-arg EN_AWS_CLI=false \ 46 | --build-arg EN_AZURE=true \ 47 | --build-arg EN_GCLOUD=false \ 48 | --build-arg EN_MINIO=false \ 49 | --build-arg EN_RCLONE=false \ 50 | --build-arg EN_GPG=true \ 51 | -t $(REPOSITORY)/mgob:$(APP_VERSION)-azure . 52 | 53 | gcloud: 54 | @echo ">>> Building $(REPOSITORY)/mgob:$(APP_VERSION)" 55 | @docker build \ 56 | --build-arg BUILD_DATE=$(BUILD_DATE) \ 57 | --build-arg VCS_REF=$(TRAVIS_COMMIT) \ 58 | --build-arg VERSION=$(APP_VERSION) \ 59 | --build-arg EN_AWS_CLI=false \ 60 | --build-arg EN_AZURE=false \ 61 | --build-arg EN_GCLOUD=true \ 62 | --build-arg EN_MINIO=false \ 63 | --build-arg EN_RCLONE=false \ 64 | --build-arg EN_GPG=true \ 65 | -t $(REPOSITORY)/mgob:$(APP_VERSION)-gcloud . 66 | 67 | travis: 68 | @echo ">>> Building mgob:$(APP_VERSION).$(TRAVIS_BUILD_NUMBER) image" 69 | @docker build \ 70 | --build-arg BUILD_DATE=$(BUILD_DATE) \ 71 | --build-arg VCS_REF=$(TRAVIS_COMMIT) \ 72 | --build-arg VERSION=$(APP_VERSION).$(TRAVIS_BUILD_NUMBER) \ 73 | -t $(REPOSITORY)/mgob:$(APP_VERSION).$(TRAVIS_BUILD_NUMBER) . 74 | 75 | @echo ">>> Starting mgob container" 76 | @docker run -d --net=host --name mgob \ 77 | --restart unless-stopped \ 78 | -v "$(TRAVIS):/config" \ 79 | -v "/tmp/ssh_host_rsa_key:/etc/ssh/ssh_host_rsa_key:ro" \ 80 | -v "/tmp/ssh_host_rsa_key.pub:/etc/ssh/ssh_host_rsa_key.pub:ro" \ 81 | $(REPOSITORY)/mgob:$(APP_VERSION).$(TRAVIS_BUILD_NUMBER) \ 82 | -ConfigPath=/config \ 83 | -StoragePath=/storage \ 84 | -TmpPath=/tmp \ 85 | -LogLevel=info 86 | 87 | publish: 88 | @echo $(DOCKER_PASS) | docker login -u "$(DOCKER_USER)" --password-stdin 89 | @docker tag $(REPOSITORY)/mgob:$(APP_VERSION).$(TRAVIS_BUILD_NUMBER) $(REPOSITORY)/mgob:edge 90 | @docker push $(REPOSITORY)/mgob:edge 91 | 92 | release: 93 | @echo $(DOCKER_PASS) | docker login -u "$(DOCKER_USER)" --password-stdin 94 | @docker tag $(REPOSITORY)/mgob:$(APP_VERSION).$(TRAVIS_BUILD_NUMBER) $(REPOSITORY)/mgob:$(APP_VERSION) 95 | @docker tag $(REPOSITORY)/mgob:$(APP_VERSION).$(TRAVIS_BUILD_NUMBER) $(REPOSITORY)/mgob:latest 96 | @docker push $(REPOSITORY)/mgob:$(APP_VERSION) 97 | @docker push $(REPOSITORY)/mgob:latest 98 | 99 | run: 100 | @echo ">>> Starting mgob container" 101 | @docker run -dp 8090:8090 --name mgob-$(APP_VERSION) \ 102 | --restart unless-stopped \ 103 | -v "$(CONFIG):/config" \ 104 | $(REPOSITORY)/mgob:$(APP_VERSION) \ 105 | -ConfigPath=/config \ 106 | -StoragePath=/storage \ 107 | -TmpPath=/tmp \ 108 | -LogLevel=info 109 | 110 | backend: 111 | @docker run -dp 20022:22 --name mgob-sftp \ 112 | atmoz/sftp:alpine test:test:::backup 113 | @docker run -dp 20099:9000 --name mgob-s3 \ 114 | -e "MINIO_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE" \ 115 | -e "MINIO_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" \ 116 | minio/minio server /export 117 | @mc config host add local http://localhost:20099 \ 118 | AKIAIOSFODNN7EXAMPLE wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY S3v4 119 | @sleep 5 120 | @mc mb local/backup 121 | 122 | fmt: 123 | @echo ">>> Running go fmt $(PACKAGES)" 124 | @go fmt $(PACKAGES) 125 | 126 | vet: 127 | @echo ">>> Running go vet $(VETARGS)" 128 | @go list ./... \ 129 | | grep -v /vendor/ \ 130 | | cut -d '/' -f 4- \ 131 | | xargs -n1 \ 132 | go tool vet $(VETARGS) ;\ 133 | if [ $$? -ne 0 ]; then \ 134 | echo ""; \ 135 | echo "go vet failed"; \ 136 | fi 137 | 138 | .PHONY: build 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mgob 2 | 3 | This project is no longer maintained. 4 | I lost interest in MongoDB some years ago and I can't drive an OSS project forward in my spare time without being passioned about it. 5 | 6 | But MGOB lives on thanks to [@maxisam](https://github.com/maxisam). The new home of MGOB is https://github.com/maxisam/mgob 7 | 8 | --- 9 | 10 | [![Build Status](https://travis-ci.org/stefanprodan/mgob.svg?branch=master)](https://travis-ci.org/stefanprodan/mgob) 11 | [![Docker Pulls](https://img.shields.io/docker/pulls/stefanprodan/mgob)](https://hub.docker.com/r/stefanprodan/mgob/) 12 | 13 | MGOB is a MongoDB backup automation tool built with Go. 14 | 15 | #### Features 16 | 17 | - schedule backups 18 | - local backups retention 19 | - upload to S3 Object Storage (Minio, AWS, Google Cloud, Azure) 20 | - upload to gcloud storage 21 | - upload to SFTP 22 | - upload to any [Rclone](https://rclone.org/) supported storage 23 | - notifications (Email, Slack) 24 | - instrumentation with Prometheus 25 | - http file server for local backups and logs 26 | - distributed as an Alpine Docker image 27 | 28 | #### Install 29 | 30 | MGOB is available on Docker Hub at [stefanprodan/mgob](https://hub.docker.com/r/stefanprodan/mgob/). 31 | 32 | Supported tags: 33 | 34 | - `stefanprodan/mgob:latest` latest stable [release](https://github.com/stefanprodan/mgob/releases) 35 | - `stefanprodan/mgob:edge` master branch latest successful [build](https://travis-ci.org/stefanprodan/mgob) 36 | 37 | Compatibility matrix: 38 | 39 | | MGOB | MongoDB | 40 | | ------------------------ | ------- | 41 | | `stefanprodan/mgob:0.9` | 3.4 | 42 | | `stefanprodan/mgob:0.10` | 3.6 | 43 | | `stefanprodan/mgob:1.0` | 4.0 | 44 | | `stefanprodan/mgob:1.1` | 4.2 | 45 | 46 | Docker: 47 | 48 | ```bash 49 | docker run -dp 8090:8090 --name mgob \ 50 | -v "/mgob/config:/config" \ 51 | -v "/mgob/storage:/storage" \ 52 | -v "/mgob/tmp:/tmp" \ 53 | -v "/mgob/data:/data" \ 54 | stefanprodan/mgob \ 55 | -LogLevel=info 56 | ``` 57 | 58 | Kubernetes: 59 | 60 | A step by step guide on running MGOB as a StatefulSet with PersistentVolumeClaims can be found [here](https://github.com/stefanprodan/mgob/tree/master/k8s). 61 | 62 | #### Configure 63 | 64 | Define a backup plan (yaml format) for each database you want to backup inside the `config` dir. 65 | The yaml file name is being used as the backup plan ID, no white spaces or special characters are allowed. 66 | 67 | _Backup plan_ 68 | 69 | ```yaml 70 | scheduler: 71 | # run every day at 6:00 and 18:00 UTC 72 | cron: "0 6,18 */1 * *" 73 | # number of backups to keep locally 74 | retention: 14 75 | # backup operation timeout in minutes 76 | timeout: 60 77 | target: 78 | # mongod IP or host name 79 | host: "172.18.7.21" 80 | # mongodb port 81 | port: 27017 82 | # mongodb database name, leave blank to backup all databases 83 | database: "test" 84 | # leave blank if auth is not enabled 85 | username: "admin" 86 | password: "secret" 87 | # add custom params to mongodump (eg. Auth or SSL support), leave blank if not needed 88 | params: "--ssl --authenticationDatabase admin" 89 | # Encryption (optional) 90 | encryption: 91 | # At the time being, only gpg asymmetric encryption is supported 92 | # Public key file or at least one recipient is mandatory 93 | gpg: 94 | # optional path to a public key file, only the first key is used. 95 | keyFile: /secret/mgob-key/key.pub 96 | # optional key server, defaults to hkps://keys.openpgp.org 97 | keyServer: hkps://keys.openpgp.org 98 | # optional list of recipients, they will be looked up on key server 99 | recipients: 100 | - example@example.com 101 | # S3 upload (optional) 102 | s3: 103 | url: "https://play.minio.io:9000" 104 | bucket: "backup" 105 | # accessKey and secretKey are optional for AWS, if your Docker image has awscli 106 | accessKey: "Q3AM3UQ867SPQQA43P2F" 107 | secretKey: "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" 108 | # Optional, only used for AWS (when awscli is present) 109 | # The customer-managed AWS Key Management Service (KMS) key ID that should be used to 110 | # server-side encrypt the backup in S3 111 | #kmsKeyId: 112 | # Optional, only used for AWS (when awscli is present) 113 | # Valid choices are: STANDARD | REDUCED_REDUNDANCY | STANDARD_IA | ONE- 114 | # ZONE_IA | INTELLIGENT_TIERING | GLACIER | DEEP_ARCHIVE. 115 | # Defaults to 'STANDARD' 116 | #storageClass: STANDARD 117 | # For Minio and AWS use S3v4 for GCP use S3v2 118 | api: "S3v4" 119 | # GCloud upload (optional) 120 | gcloud: 121 | bucket: "backup" 122 | keyFilePath: /path/to/service-account.json 123 | # Azure blob storage upload (optional) 124 | azure: 125 | containerName: "backup" 126 | connectionString: "DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net" 127 | # Rclone upload (optional) 128 | rclone: 129 | bucket: "my-backup-bucket" 130 | # See https://rclone.org/docs/ for details on how to configure rclone 131 | configFilePath: /etc/rclone.conf 132 | configSection: "myrclonesection" 133 | # SFTP upload (optional) 134 | sftp: 135 | host: sftp.company.com 136 | port: 2022 137 | username: user 138 | password: secret 139 | # you can also specify path to a private key and a passphrase 140 | private_key: /etc/ssh/ssh_host_rsa_key 141 | passphrase: secretpassphrase 142 | # dir must exist on the SFTP server 143 | dir: backup 144 | # Email notifications (optional) 145 | smtp: 146 | server: smtp.company.com 147 | port: 465 148 | username: user 149 | password: secret 150 | from: mgob@company.com 151 | to: 152 | - devops@company.com 153 | - alerts@company.com 154 | # Slack notifications (optional) 155 | slack: 156 | url: https://hooks.slack.com/services/xxxx/xxx/xx 157 | channel: devops-alerts 158 | username: mgob 159 | # 'true' to notify only on failures 160 | warnOnly: false 161 | ``` 162 | 163 | ReplicaSet example: 164 | 165 | ```yaml 166 | target: 167 | host: "mongo-0.mongo.db,mongo-1.mongo.db,mongo-2.mongo.db" 168 | port: 27017 169 | database: "test" 170 | ``` 171 | 172 | Sharded cluster with authentication and SSL example: 173 | 174 | ```yaml 175 | target: 176 | host: "mongos-0.db,mongos-1.db" 177 | port: 27017 178 | database: "test" 179 | username: "admin" 180 | password: "secret" 181 | params: "--ssl --authenticationDatabase admin" 182 | ``` 183 | 184 | #### Web API 185 | 186 | - `mgob-host:8090/storage` file server 187 | - `mgob-host:8090/status` backup jobs status 188 | - `mgob-host:8090/metrics` Prometheus endpoint 189 | - `mgob-host:8090/version` mgob version and runtime info 190 | - `mgob-host:8090/debug` pprof endpoint 191 | 192 | On demand backup: 193 | 194 | - HTTP POST `mgob-host:8090/backup/:planID` 195 | 196 | ```bash 197 | curl -X POST http://mgob-host:8090/backup/mongo-debug 198 | ``` 199 | 200 | ```json 201 | { 202 | "plan": "mongo-debug", 203 | "file": "mongo-debug-1494256295.gz", 204 | "duration": "3.635186255s", 205 | "size": "455 kB", 206 | "timestamp": "2017-05-08T15:11:35.940141701Z" 207 | } 208 | ``` 209 | 210 | Scheduler status: 211 | 212 | - HTTP GET `mgob-host:8090/status` 213 | - HTTP GET `mgob-host:8090/status/:planID` 214 | 215 | ```bash 216 | curl -X GET http://mgob-host:8090/status/mongo-debug 217 | ``` 218 | 219 | ```json 220 | { 221 | "plan": "mongo-debug", 222 | "next_run": "2017-05-13T14:32:00+03:00", 223 | "last_run": "2017-05-13T11:31:00.000622589Z", 224 | "last_run_status": "200", 225 | "last_run_log": "Backup finished in 2.339055539s archive mongo-debug-1494675060.gz size 527 kB" 226 | } 227 | ``` 228 | 229 | #### Logs 230 | 231 | View scheduler logs with `docker logs mgob`: 232 | 233 | ```bash 234 | time="2017-05-05T16:50:55+03:00" level=info msg="Next run at 2017-05-05 16:51:00 +0300 EEST" plan=mongo-dev 235 | time="2017-05-05T16:50:55+03:00" level=info msg="Next run at 2017-05-05 16:52:00 +0300 EEST" plan=mongo-test 236 | time="2017-05-05T16:51:00+03:00" level=info msg="Backup started" plan=mongo-dev 237 | time="2017-05-05T16:51:02+03:00" level=info msg="Backup finished in 2.359901432s archive size 448 kB" plan=mongo-dev 238 | time="2017-05-05T16:52:00+03:00" level=info msg="Backup started" plan=mongo-test 239 | time="2017-05-05T16:52:02+03:00" level=info msg="S3 upload finished `/storage/mongo-test/mongo-test-1493992320.gz` -> `bktest/mongo-test-1493992320.gz` Total: 1.17 KB, Transferred: 1.17 KB, Speed: 2.96 KB/s " plan=mongo-test 240 | time="2017-05-05T16:52:02+03:00" level=info msg="Backup finished in 2.855078717s archive size 1.2 kB" plan=mongo-test 241 | ``` 242 | 243 | The success/fail logs will be sent via SMTP and/or Slack if notifications are enabled. 244 | 245 | The mongodump log is stored along with the backup data (gzip archive) in the `storage` dir: 246 | 247 | ```bash 248 | aleph-mbp:test aleph$ ls -lh storage/mongo-dev 249 | total 4160 250 | -rw-r--r-- 1 aleph staff 410K May 3 17:46 mongo-dev-1493822760.gz 251 | -rw-r--r-- 1 aleph staff 1.9K May 3 17:46 mongo-dev-1493822760.log 252 | -rw-r--r-- 1 aleph staff 410K May 3 17:47 mongo-dev-1493822820.gz 253 | -rw-r--r-- 1 aleph staff 1.5K May 3 17:47 mongo-dev-1493822820.log 254 | ``` 255 | 256 | #### Metrics 257 | 258 | Successful backups counter 259 | 260 | ```bash 261 | mgob_scheduler_backup_total{plan="mongo-dev",status="200"} 8 262 | ``` 263 | 264 | Successful backups duration 265 | 266 | ```bash 267 | mgob_scheduler_backup_latency{plan="mongo-dev",status="200",quantile="0.5"} 2.149668417 268 | mgob_scheduler_backup_latency{plan="mongo-dev",status="200",quantile="0.9"} 2.39848413 269 | mgob_scheduler_backup_latency{plan="mongo-dev",status="200",quantile="0.99"} 2.39848413 270 | mgob_scheduler_backup_latency_sum{plan="mongo-dev",status="200"} 17.580484907 271 | mgob_scheduler_backup_latency_count{plan="mongo-dev",status="200"} 8 272 | ``` 273 | 274 | Failed jobs count and duration (status 500) 275 | 276 | ```bash 277 | mgob_scheduler_backup_latency{plan="mongo-test",status="500",quantile="0.5"} 2.4180213 278 | mgob_scheduler_backup_latency{plan="mongo-test",status="500",quantile="0.9"} 2.438254775 279 | mgob_scheduler_backup_latency{plan="mongo-test",status="500",quantile="0.99"} 2.438254775 280 | mgob_scheduler_backup_latency_sum{plan="mongo-test",status="500"} 9.679809477 281 | mgob_scheduler_backup_latency_count{plan="mongo-test",status="500"} 4 282 | ``` 283 | 284 | #### Restore 285 | 286 | In order to restore from a local backup you have two options: 287 | 288 | Browse `mgob-host:8090/storage` to identify the backup you want to restore. 289 | Login to your MongoDB server and download the archive using `curl` and restore the backup with `mongorestore` command line. 290 | 291 | ```bash 292 | curl -o /tmp/mongo-test-1494056760.gz http://mgob-host:8090/storage/mongo-test/mongo-test-1494056760.gz 293 | mongorestore --gzip --archive=/tmp/mongo-test-1494056760.gz --drop 294 | ``` 295 | 296 | You can also restore a backup from within mgob container. 297 | Exec into mgob, identify the backup you want to restore and use `mongorestore` to connect to your MongoDB server. 298 | 299 | ```bash 300 | docker exec -it mgob sh 301 | ls /storage/mongo-test 302 | mongorestore --gzip --archive=/storage/mongo-test/mongo-test-1494056760.gz --host mongohost:27017 --drop 303 | ``` 304 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | apk add --no-cache ca-certificates tzdata bash curl krb5-dev 4 | 5 | # Install GnuPG 6 | if [ "_${MGOB_EN_GPG}" = "_true" ] 7 | then 8 | apk add gnupg=${GNUPG_VERSION} 9 | fi 10 | 11 | cd /tmp 12 | 13 | # Install MinIO 14 | if [ "_${MGOB_EN_MINIO}" = "_true" ] 15 | then 16 | curl -O https://dl.minio.io/client/mc/release/linux-amd64/mc 17 | mv mc /usr/bin 18 | chmod u+x /usr/bin/mc 19 | fi 20 | 21 | # Install RClone 22 | if [ "_${MGOB_EN_RCLONE}" = "_true" ] 23 | then 24 | curl -O https://downloads.rclone.org/rclone-current-linux-amd64.zip 25 | unzip rclone-current-linux-amd64.zip 26 | cp rclone-*-linux-amd64/rclone /usr/bin/ 27 | chmod u+x /usr/bin/rclone 28 | rm rclone-current-linux-amd64.zip 29 | fi 30 | 31 | #install gcloud 32 | if [ "_${MGOB_EN_GCLOUD}" = "_true" ] 33 | then 34 | export PATH="/google-cloud-sdk/bin:$PATH" 35 | apk --no-cache add \ 36 | python3 \ 37 | py3-pip \ 38 | libc6-compat \ 39 | openssh-client \ 40 | git 41 | pip3 --no-cache-dir install --upgrade pip 42 | pip --no-cache-dir install wheel 43 | pip --no-cache-dir install crcmod 44 | curl -O https://dl.google.com/dl/cloudsdk/channels/rapid/downloads/google-cloud-sdk-${GOOGLE_CLOUD_SDK_VERSION}-linux-x86_64.tar.gz 45 | tar xzf google-cloud-sdk-${GOOGLE_CLOUD_SDK_VERSION}-linux-x86_64.tar.gz 46 | mv google-cloud-sdk / 47 | rm google-cloud-sdk-${GOOGLE_CLOUD_SDK_VERSION}-linux-x86_64.tar.gz 48 | ln -s /lib /lib64 49 | gcloud config set core/disable_usage_reporting true 50 | gcloud config set component_manager/disable_update_check true 51 | gcloud config set metrics/environment github_docker_image 52 | gcloud --version 53 | fi 54 | 55 | # install azure-cli and aws-cli 56 | if [ "_${MGOB_EN_AZURE}" = "_true" -o "_${MGOB_EN_AWS_CLI}" = "_true" ] 57 | then 58 | apk --no-cache add python3 py3-pip 59 | apk --no-cache add --virtual=build gcc libffi-dev musl-dev openssl-dev python3-dev make 60 | pip3 --no-cache-dir install --upgrade pip 61 | pip --no-cache-dir install wheel cffi 62 | echo "EN_AZURE: $MGOB_EN_AZURE; EN_AWS_CLI: $MGOB_EN_AWS_CLI" 63 | [ "_${MGOB_EN_AZURE}" = "_true" ] && pip --no-cache-dir install azure-cli==${AZURE_CLI_VERSION} 64 | [ "_${MGOB_EN_AWS_CLI}" = "_true" ] && pip --no-cache-dir install awscli==${AWS_CLI_VERSION} 65 | apk del --purge build 66 | fi 67 | -------------------------------------------------------------------------------- /chart/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: mgob 3 | description: | 4 | MongoDB dockerized backup agent. 5 | Runs scheduled backups with retention, S3 & SFTP upload, notifications, instrumentation with Prometheus and more. 6 | 7 | 8 | version: 1.2.2 9 | appVersion: "1.5" 10 | 11 | sources: 12 | - https://github.com/stefanprodan/mgob 13 | 14 | maintainers: 15 | - name: endrec 16 | email: endre.czirbesz@rungway.com 17 | -------------------------------------------------------------------------------- /chart/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Stefan Prodan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /chart/README.md: -------------------------------------------------------------------------------- 1 | # mgob 2 | 3 | ![Version: 1.2.1](https://img.shields.io/badge/Version-1.2.1-informational?style=flat-square) ![AppVersion: 1.3](https://img.shields.io/badge/AppVersion-1.3-informational?style=flat-square) 4 | 5 | MongoDB dockerized backup agent. 6 | Runs scheduled backups with retention, S3 & SFTP upload, notifications, instrumentation with Prometheus and more. 7 | 8 | ## Maintainers 9 | 10 | | Name | Email | Url | 11 | | ---- | ------ | --- | 12 | | endrec | endre.czirbesz@rungway.com | | 13 | 14 | ## Source Code 15 | 16 | * 17 | 18 | ## Values 19 | 20 | | Key | Type | Default | Description | 21 | |-----|------|---------|-------------| 22 | | config | object | `{}` | Backup plans. For details, see [values.yaml](values.yaml) | 23 | | env | object | `{}` | | 24 | | fullnameOverride | string | `""` | | 25 | | image.pullPolicy | string | `"IfNotPresent"` | Image pull policy | 26 | | image.repository | string | `"stefanprodan/mgob"` | Image repo | 27 | | image.tag | string | `""` | Image tag Overrides the image tag whose default is the chart appVersion. | 28 | | ingress.annotations | object | `{}` | | 29 | | ingress.enabled | bool | `false` | | 30 | | ingress.hosts | object | `{}` | | 31 | | ingress.tls | object | `{}` | | 32 | | logLevel | string | `"info"` | log level (debug|info|warn|error|fatal|panic) WARNING! debug logs might include passwords! | 33 | | nameOverride | string | `""` | | 34 | | podSecurityContext | object | `{"fsGroup":65534}` | Pod Security Context ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ | 35 | | replicaCount | int | `1` | Number of replicas | 36 | | resources | object | `{"limits":{"cpu":"100m","memory":"128Mi"},"requests":{"cpu":"100m","memory":"128Mi"}}` | Resource requests and limits ref: http://kubernetes.io/docs/user-guide/compute-resources/ | 37 | | secret | object | `{}` | Secret(s) to mount. For details, see [values.yaml](values.yaml) | 38 | | securityContext | object | `{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]},"privileged":false}` | Container Security Context ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ | 39 | | service.externalPort | int | `8090` | Port to access the service | 40 | | service.internalPort | int | `8090` | Port to connect to in pod | 41 | | service.name | string | `"mgob"` | Service name | 42 | | serviceAccount.annotations | object | `{}` | Annotations to add on service account | 43 | | serviceAccount.create | bool | `true` | If false, default service account will be used | 44 | | serviceAccount.name | string | `""` | | 45 | | storage.longTerm | object | `{"accessMode":"ReadWriteOnce","name":"mgob-storage","size":"10Gi","storageClass":"gp2"}` | Persistent volume for backups, see `config.retention` | 46 | | storage.tmp | object | `{"accessMode":"ReadWriteOnce","name":"mgob-tmp","size":"3Gi","storageClass":"gp2"}` | Persistent volume for temporary files | 47 | 48 | ---------------------------------------------- 49 | Autogenerated from chart metadata using [helm-docs v1.6.0](https://github.com/norwoodj/helm-docs/releases/v1.6.0) 50 | -------------------------------------------------------------------------------- /chart/ci/ci-values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for mgob. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | # Note that two backup plans are provided as templates - they contain dummy values and should be changed before 5 | # attempting to apply the chart to your cluster. 6 | replicaCount: 1 7 | image: 8 | repository: stefanprodan/mgob 9 | pullPolicy: IfNotPresent 10 | tag: 1.3 11 | service: 12 | name: mgob 13 | externalPort: 8090 14 | internalPort: 8090 15 | serviceAccount: 16 | create: true 17 | annotations: 18 | eks.amazonaws.com/role-arn: iamArn 19 | resources: 20 | limits: 21 | cpu: 100m 22 | memory: 128Mi 23 | requests: 24 | cpu: 100m 25 | memory: 128Mi 26 | storage: 27 | longTerm: 28 | accessMode: "ReadWriteOnce" 29 | # storageClass: "gp2" # Note: "gp2" is for AWS. Use the storage class for your cloud provider. 30 | name: "mgob-storage" 31 | size: 10Mi 32 | tmp: 33 | accessMode: "ReadWriteOnce" 34 | # storageClass: "gp2" # Note: "gp2" is for AWS. Use the storage class for your cloud provider. 35 | name: "mgob-tmp" 36 | size: 10Mi 37 | config: 38 | # Add each plan as per below. 39 | the-first-database.yml: 40 | # run every day at 6:00 and 18:00 UTC 41 | schedule: "0 6,18 */1 * *" 42 | # number of backups to keep locally 43 | retention: 14 44 | # backup operation timeout in minutes 45 | timeout: 60 46 | target: 47 | # mongod IP or host name 48 | host: "172.18.7.21" 49 | # mongodb port 50 | port: 27017 51 | # mongodb database name, leave blank to backup all databases 52 | database: "test" 53 | # leave blank if auth is not enabled 54 | username: "admin" 55 | password: "secret" 56 | # add custom params to mongodump (eg. Auth or SSL support), leave blank if not needed 57 | params: "--ssl --authenticationDatabase admin" 58 | # S3 upload (optional) 59 | s3: 60 | url: "https://play.minio.io:9000" 61 | bucket: "backup" 62 | accessKey: "Q3AM3UQ867SPQQA43P2F" 63 | secretKey: "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" 64 | # For Minio and AWS use S3v4 for GCP use S3v2 65 | api: "S3v4" 66 | # GCloud upload (optional) 67 | gcloud: 68 | bucket: "backup" 69 | keyFilePath: /path/to/service-account.json 70 | # Azure blob storage upload (optional) 71 | azure: 72 | containerName: "backup" 73 | connectionString: "DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net" 74 | # Rclone upload (optional) 75 | rclone: 76 | bucket: "my-backup-bucket" 77 | # See https://rclone.org/docs/ for details on how to configure rclone 78 | configFilePath: /etc/rclone.conf 79 | configSection: "myrclonesection" 80 | # SFTP upload (optional) 81 | sftp: 82 | host: sftp.company.com 83 | port: 2022 84 | username: user 85 | password: secret 86 | # you can also specify path to a private key and a passphrase 87 | private_key: /etc/ssh/ssh_host_rsa_key 88 | passphrase: secretpassphrase 89 | # dir must exist on the SFTP server 90 | dir: backup 91 | # Email notifications (optional) 92 | smtp: 93 | server: smtp.company.com 94 | port: 465 95 | username: user 96 | password: secret 97 | from: mgob@company.com 98 | to: 99 | - devops@company.com 100 | - alerts@company.com 101 | # Slack notifications (optional) 102 | slack: 103 | url: https://hooks.slack.com/services/xxxx/xxx/xx 104 | channel: devops-alerts 105 | username: mgob 106 | # 'true' to notify only on failures 107 | warnOnly: false 108 | secret: {} 109 | ## You can either insert your secret values as part of helm values, or refer externally created secrets. 110 | # - name: gcp-example-secret-name 111 | # - name: gcp-example-secret-name-with-values 112 | # data: 113 | # service-account.json: | 114 | # { 115 | # "type": "service_account", 116 | # "project_id": "your-gcp-project-id", 117 | # "private_key_id": "12345678901234567890", 118 | # "private_key": "-----BEGIN PRIVATE KEY-----\n...........\n-----END PRIVATE KEY-----\n", 119 | # ... 120 | # } 121 | env: {} 122 | # - name: HTTPS_PROXY 123 | # value: "http://localhost:3128" 124 | -------------------------------------------------------------------------------- /chart/notes.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | This script assumes a user has been created in the mongoDB instance with sufficient read privileges to create the 4 | backups. 5 | 6 | The password set when creating the user below is referred to in the `values.yaml`-file for each plan. Make sure that 7 | those match, or your backups will fail. 8 | 9 | db.createUser({ 10 | user: "mongodb-backup", 11 | pwd: "backup-user-pwd", 12 | roles: [ 13 | { role: "backup", db: "admin" } 14 | ] 15 | }); 16 | 17 | Secondly, it assumes that [Helm](https://github.com/kubernetes/helm) is installed on your cluster and that you have 18 | a properly configured [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl/) installed. 19 | 20 | To install the chart, first clone the Git repository. Secondly, edit the `values.yaml`-file to define your backup 21 | plans. Documentation for those can be found in the default repository `readme.md`, easily accessible as the 22 | [mgob repository start page](https://github.com/stefanprodan/mgob). When the `values.yaml`-file properly represents the 23 | plan(s) you want to create, simply run: 24 | 25 | $ helm install --namespace my-kubernetes-ns ./chart 26 | 27 | This will install the chart on your cluster. 28 | -------------------------------------------------------------------------------- /chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "mgob.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 7 | {{- end }} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "mgob.fullname" -}} 15 | {{- if .Values.fullnameOverride }} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 17 | {{- else }} 18 | {{- $name := default .Chart.Name .Values.nameOverride }} 19 | {{- if contains $name .Release.Name }} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 21 | {{- else }} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 23 | {{- end }} 24 | {{- end }} 25 | {{- end }} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "mgob.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 32 | {{- end }} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "mgob.labels" -}} 38 | helm.sh/chart: {{ include "mgob.chart" . }} 39 | {{ include "mgob.selectorLabels" . }} 40 | {{- if .Chart.AppVersion }} 41 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 42 | {{- end }} 43 | app.kubernetes.io/managed-by: {{ .Release.Service }} 44 | {{- end }} 45 | 46 | {{/* 47 | Selector labels 48 | */}} 49 | {{- define "mgob.selectorLabels" -}} 50 | app.kubernetes.io/name: {{ include "mgob.name" . }} 51 | app.kubernetes.io/instance: {{ .Release.Name }} 52 | {{- end }} 53 | 54 | {{/* 55 | Create the name of the service account to use 56 | */}} 57 | {{- define "mgob.serviceAccountName" -}} 58 | {{- if .Values.serviceAccount.create }} 59 | {{- default (include "mgob.fullname" .) .Values.serviceAccount.name }} 60 | {{- else }} 61 | {{- default "default" .Values.serviceAccount.name }} 62 | {{- end }} 63 | {{- end }} 64 | 65 | {{/* 66 | Add node selectors for mgob if present in the given scope 67 | */}} 68 | {{- define "mgob.nodeSelector" }} 69 | {{- if .nodeSelector }} 70 | nodeSelector: 71 | {{- range $key, $val := .nodeSelector }} 72 | {{ $key }}: {{ $val | quote }} 73 | {{- end }} 74 | {{- end }} 75 | {{- end }} 76 | -------------------------------------------------------------------------------- /chart/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | kind: ConfigMap 2 | apiVersion: v1 3 | metadata: 4 | name: {{ template "mgob.fullname" . }}-config 5 | labels: 6 | {{- include "mgob.labels" . | nindent 4 }} 7 | data: 8 | {{- range $name,$value := .Values.config }} 9 | {{ $name | quote }}: |- 10 | scheduler: 11 | cron: {{ $value.scheduler.cron | quote }} 12 | retention: {{ $value.scheduler.retention }} 13 | timeout: {{ $value.scheduler.timeout }} 14 | {{- with $value.target }} 15 | target: 16 | {{ toYaml . | nindent 6 | trim }} 17 | {{- end }} 18 | {{- with $value.encryption }} 19 | encryption: 20 | {{ toYaml . | nindent 6 | trim }} 21 | {{- end }} 22 | {{- with $value.s3 }} 23 | s3: 24 | {{ toYaml . | nindent 6 | trim }} 25 | {{- end }} 26 | {{- with $value.gcloud }} 27 | gcloud: 28 | {{ toYaml . | nindent 6 | trim }} 29 | {{- end }} 30 | {{- with $value.azure }} 31 | azure: 32 | {{ toYaml . | nindent 6 | trim }} 33 | {{- end }} 34 | {{- with $value.rclone }} 35 | rclone: 36 | {{ toYaml . | nindent 6 | trim }} 37 | {{- end }} 38 | {{- with $value.sftp }} 39 | sftp: 40 | {{ toYaml . | nindent 6 | trim }} 41 | {{- end }} 42 | {{- with $value.smtp }} 43 | smtp: 44 | {{ toYaml . | nindent 6 | trim }} 45 | {{- end }} 46 | {{- with $value.slack }} 47 | slack: 48 | {{ toYaml . | nindent 6 | trim }} 49 | {{- end }} 50 | {{- end }} 51 | -------------------------------------------------------------------------------- /chart/templates/ingress-1.19.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.ingress.enabled (semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion) -}} 2 | {{- $fullName := include "mgob.fullname" . -}} 3 | {{- $svcPort := .Values.service.externalPort -}} 4 | apiVersion: networking.k8s.io/v1 5 | kind: Ingress 6 | metadata: 7 | name: {{ $fullName }} 8 | labels: 9 | {{- include "mgob.labels" . | nindent 4 }} 10 | {{- with .Values.ingress.annotations }} 11 | annotations: 12 | {{- toYaml . | nindent 4 }} 13 | {{- end }} 14 | spec: 15 | {{- if .Values.ingress.tls }} 16 | tls: 17 | {{- range .Values.ingress.tls }} 18 | - hosts: 19 | {{- range .hosts }} 20 | - {{ . | quote }} 21 | {{- end }} 22 | secretName: {{ .secretName }} 23 | {{- end }} 24 | {{- end }} 25 | rules: 26 | {{- range .Values.ingress.hosts }} 27 | - host: {{ .host | quote }} 28 | http: 29 | paths: 30 | {{- range .paths }} 31 | - path: {{ .path }} 32 | pathType: ImplementationSpecific 33 | backend: 34 | service: 35 | name: {{ $fullName }} 36 | port: 37 | number: {{ $svcPort }} 38 | {{- end }} 39 | {{- end }} 40 | {{- end }} 41 | -------------------------------------------------------------------------------- /chart/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.ingress.enabled (semverCompare "<1.19-0" .Capabilities.KubeVersion.GitVersion) -}} 2 | {{- $fullName := include "mgob.fullname" . -}} 3 | {{- $svcPort := .Values.service.externalPort -}} 4 | {{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 5 | apiVersion: networking.k8s.io/v1beta1 6 | {{- else -}} 7 | apiVersion: extensions/v1beta1 8 | {{- end }} 9 | kind: Ingress 10 | metadata: 11 | name: {{ $fullName }} 12 | labels: 13 | {{- include "mgob.labels" . | nindent 4 }} 14 | {{- with .Values.ingress.annotations }} 15 | annotations: 16 | {{- toYaml . | nindent 4 }} 17 | {{- end }} 18 | spec: 19 | {{- if .Values.ingress.tls }} 20 | tls: 21 | {{- range .Values.ingress.tls }} 22 | - hosts: 23 | {{- range .hosts }} 24 | - {{ . | quote }} 25 | {{- end }} 26 | secretName: {{ .secretName }} 27 | {{- end }} 28 | {{- end }} 29 | rules: 30 | {{- range .Values.ingress.hosts }} 31 | - host: {{ .host | quote }} 32 | http: 33 | paths: 34 | {{- range .paths }} 35 | - path: {{ .path }} 36 | backend: 37 | serviceName: {{ $fullName }} 38 | servicePort: {{ $svcPort }} 39 | {{- end }} 40 | {{- end }} 41 | {{- end }} 42 | -------------------------------------------------------------------------------- /chart/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | {{- range $secret := .Values.secret }} 2 | {{- if $secret.data -}} 3 | apiVersion: v1 4 | kind: Secret 5 | metadata: 6 | name: {{ $secret.name }} 7 | labels: 8 | {{- include "mgob.labels" . | nindent 4 }} 9 | type: Opaque 10 | data: 11 | {{- range $name, $value := $secret.data }} 12 | {{ $name }}: {{ $value | b64enc | quote }} 13 | {{- end }} 14 | --- 15 | {{- end -}} 16 | {{ end }} 17 | -------------------------------------------------------------------------------- /chart/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ template "mgob.fullname" . }} 5 | labels: 6 | {{- include "mgob.labels" . | nindent 4 }} 7 | spec: 8 | clusterIP: None 9 | ports: 10 | - port: {{ .Values.service.externalPort }} 11 | targetPort: {{ .Values.service.internalPort }} 12 | protocol: TCP 13 | name: {{ .Values.service.name }} 14 | selector: 15 | app.kubernetes.io/name: {{ include "mgob.name" . }} 16 | app.kubernetes.io/instance: {{ .Release.Name }} 17 | -------------------------------------------------------------------------------- /chart/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "mgob.serviceAccountName" . }} 6 | labels: 7 | {{- include "mgob.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /chart/templates/statefulset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: {{ template "mgob.fullname" . }} 5 | labels: 6 | {{- include "mgob.labels" . | nindent 4 }} 7 | spec: 8 | serviceName: {{ template "mgob.fullname" . }} 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app.kubernetes.io/name: {{ include "mgob.name" . }} 13 | app.kubernetes.io/instance: {{ .Release.Name }} 14 | template: 15 | metadata: 16 | labels: 17 | {{- include "mgob.labels" . | nindent 8 }} 18 | annotations: 19 | checksum/configMap: {{ toYaml .Values.config | sha256sum }} 20 | spec: 21 | containers: 22 | - name: {{ template "mgob.fullname" . }} 23 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 24 | imagePullPolicy: {{ .Values.image.pullPolicy }} 25 | args: 26 | - "-LogLevel={{ .Values.logLevel }}" 27 | env: 28 | {{- range $envVar := .Values.env }} 29 | - name: {{ $envVar.name }} 30 | value: {{ $envVar.value }} 31 | {{- end }} 32 | ports: 33 | - containerPort: {{ .Values.service.internalPort }} 34 | protocol: TCP 35 | securityContext: 36 | {{ toYaml .Values.securityContext | nindent 10 | trim }} 37 | volumeMounts: 38 | - name: "mgob-storage" 39 | mountPath: "/storage" 40 | - name: "mgob-tmp" 41 | mountPath: "/tmp" 42 | - name: "mgob-tmp" 43 | mountPath: "/data" 44 | {{- range $name, $value := .Values.config }} 45 | - mountPath: "/config/{{ $name }}" 46 | name: config 47 | subPath: {{ $name | quote }} 48 | {{- end }} 49 | {{- range $secret := .Values.secret }} 50 | - mountPath: "/secret/{{ $secret.name }}" 51 | name: {{ $secret.name }} 52 | {{- end }} 53 | initContainers: 54 | - name: init-cleanup 55 | image: busybox:1.34 56 | command: ['sh', '-c', 'find /tmp -not -name "mgob.db" -type f -delete'] 57 | securityContext: 58 | {{ toYaml .Values.podSecurityContext | nindent 8 | trim }} 59 | serviceAccountName: {{ template "mgob.serviceAccountName" . }} 60 | {{- include "mgob.nodeSelector" .Values | indent 6 }} 61 | volumes: 62 | - name: config 63 | configMap: 64 | name: {{ template "mgob.fullname" . }}-config 65 | items: 66 | {{- range $name, $value := .Values.config }} 67 | - key: {{ $name }} 68 | path: {{ $name }} 69 | {{- end }} 70 | {{- range $secret := .Values.secret }} 71 | - name: {{ $secret.name }} 72 | secret: 73 | secretName: {{ $secret.name }} 74 | {{- end }} 75 | volumeClaimTemplates: 76 | - metadata: 77 | name: {{ .Values.storage.longTerm.name }} 78 | spec: 79 | storageClassName: {{ .Values.storage.longTerm.storageClass }} 80 | accessModes: 81 | - {{ .Values.storage.longTerm.accessMode }} 82 | resources: 83 | requests: 84 | storage: {{ .Values.storage.longTerm.size }} 85 | - metadata: 86 | name: {{ .Values.storage.tmp.name }} 87 | spec: 88 | storageClassName: {{ .Values.storage.tmp.storageClass }} 89 | accessModes: 90 | - {{ .Values.storage.tmp.accessMode }} 91 | resources: 92 | requests: 93 | storage: {{ .Values.storage.tmp.size }} 94 | -------------------------------------------------------------------------------- /chart/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for mgob. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | # Note that two backup plans are provided as templates - they contain dummy values and should be changed before 5 | # attempting to apply the chart to your cluster. 6 | 7 | # -- Number of replicas 8 | replicaCount: 1 9 | 10 | nameOverride: "" 11 | fullnameOverride: "" 12 | 13 | # -- log level (debug|info|warn|error|fatal|panic) 14 | # WARNING! debug logs might include passwords! 15 | logLevel: info 16 | 17 | image: 18 | # -- Image repo 19 | repository: stefanprodan/mgob 20 | # -- Image pull policy 21 | pullPolicy: IfNotPresent 22 | # -- Image tag 23 | # Overrides the image tag whose default is the chart appVersion. 24 | tag: "" 25 | 26 | service: 27 | # -- Service name 28 | name: mgob 29 | # -- Port to access the service 30 | externalPort: 8090 31 | # -- Port to connect to in pod 32 | internalPort: 8090 33 | 34 | # TODO: add example values 35 | ingress: 36 | enabled: false 37 | annotations: {} 38 | tls: {} 39 | hosts: {} 40 | 41 | serviceAccount: 42 | # -- If false, default service account will be used 43 | create: true 44 | # The name of the service account to use. 45 | # If not set and create is true, a name is generated using the fullname template 46 | name: "" 47 | # -- Annotations to add on service account 48 | annotations: {} 49 | # For example, to attach an AWS IAM role: 50 | # eks.amazonaws.com/role-arn: iamArn 51 | 52 | storage: 53 | # -- Persistent volume for backups, see `config.retention` 54 | longTerm: 55 | accessMode: "ReadWriteOnce" 56 | storageClass: "gp2" # Note: "gp2" is for AWS. Use the storage class for your cloud provider. 57 | name: "mgob-storage" 58 | size: 10Gi 59 | # -- Persistent volume for temporary files 60 | tmp: 61 | accessMode: "ReadWriteOnce" 62 | storageClass: "gp2" # Note: "gp2" is for AWS. Use the storage class for your cloud provider. 63 | name: "mgob-tmp" 64 | size: 3Gi 65 | 66 | # -- Backup plans. 67 | # For details, see [values.yaml](values.yaml) 68 | config: {} 69 | # # Add each plan as per below. 70 | # the-first-database.yml: 71 | # # run every day at 6:00 and 18:00 UTC 72 | # scheduler: 73 | # cron: "0 6,18 */1 * *" 74 | # # number of backups to keep locally 75 | # retention: 14 76 | # # backup operation timeout in minutes 77 | # timeout: 60 78 | # target: 79 | # # mongod IP or host name 80 | # host: "172.18.7.21" 81 | # # mongodb port 82 | # port: 27017 83 | # # mongodb database name, leave blank to backup all databases 84 | # database: "test" 85 | # # leave blank if auth is not enabled 86 | # username: "admin" 87 | # password: "secret" 88 | # # add custom params to mongodump (eg. Auth or SSL support), leave blank if not needed 89 | # params: "--ssl --authenticationDatabase admin" 90 | # # Encryption (optional) 91 | # encryption: 92 | # # At the time being, only gpg asymmetric encryption is supported 93 | # # Public key file or at least one recipient is mandatory 94 | # gpg: 95 | # # optional path to a public key file, only the first key is used. 96 | # keyFile: /secret/mgob-key/key.pub 97 | # # optional key server, defaults to hkps://keys.openpgp.org 98 | # keyServer: hkps://keys.openpgp.org 99 | # # optional list of recipients, they will be looked up on key server 100 | # recipients: 101 | # - example@example.com 102 | # # S3 upload (optional) 103 | # s3: 104 | # url: "https://play.minio.io:9000" 105 | # bucket: "backup" 106 | # accessKey: "Q3AM3UQ867SPQQA43P2F" 107 | # secretKey: "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" 108 | # # For Minio and AWS use S3v4 for GCP use S3v2 109 | # api: "S3v4" 110 | # # GCloud upload (optional) 111 | # gcloud: 112 | # bucket: "backup" 113 | # keyFilePath: /path/to/service-account.json 114 | # # Azure blob storage upload (optional) 115 | # azure: 116 | # containerName: "backup" 117 | # connectionString: "DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;EndpointSuffix=core.windows.net" 118 | # # Rclone upload (optional) 119 | # rclone: 120 | # bucket: "my-backup-bucket" 121 | # # See https://rclone.org/docs/ for details on how to configure rclone 122 | # configFilePath: /etc/rclone.conf 123 | # configSection: "myrclonesection" 124 | # # SFTP upload (optional) 125 | # sftp: 126 | # host: sftp.company.com 127 | # port: 2022 128 | # username: user 129 | # password: secret 130 | # # you can also specify path to a private key and a passphrase 131 | # private_key: /etc/ssh/ssh_host_rsa_key 132 | # passphrase: secretpassphrase 133 | # # dir must exist on the SFTP server 134 | # dir: backup 135 | # # Email notifications (optional) 136 | # smtp: 137 | # server: smtp.company.com 138 | # port: 465 139 | # username: user 140 | # password: secret 141 | # from: mgob@company.com 142 | # to: 143 | # - devops@company.com 144 | # - alerts@company.com 145 | # # Slack notifications (optional) 146 | # slack: 147 | # url: https://hooks.slack.com/services/xxxx/xxx/xx 148 | # channel: devops-alerts 149 | # username: mgob 150 | # # 'true' to notify only on failures 151 | # warnOnly: false 152 | 153 | # -- Secret(s) to mount. 154 | # For details, see [values.yaml](values.yaml) 155 | secret: {} 156 | ## You can either insert your secret values as part of helm values, or refer externally created secrets. 157 | # - name: gcp-example-secret-name 158 | # - name: gcp-example-secret-name-with-values 159 | # data: 160 | # service-account.json: | 161 | # { 162 | # "type": "service_account", 163 | # "project_id": "your-gcp-project-id", 164 | # "private_key_id": "12345678901234567890", 165 | # "private_key": "-----BEGIN PRIVATE KEY-----\n...........\n-----END PRIVATE KEY-----\n", 166 | # ... 167 | # } 168 | 169 | # Environment variables 170 | env: {} 171 | # - name: HTTPS_PROXY 172 | # value: "http://localhost:3128" 173 | 174 | # -- Pod Security Context 175 | # ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ 176 | podSecurityContext: 177 | fsGroup: 65534 178 | 179 | # -- Container Security Context 180 | # ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/ 181 | securityContext: 182 | allowPrivilegeEscalation: false 183 | capabilities: 184 | drop: 185 | - ALL 186 | privileged: false 187 | ### The current image writes the root filesystem, and needs root. :( 188 | #readOnlyRootFilesystem: false 189 | #runAsNonRoot: false 190 | #runAsUser: 0 191 | 192 | # -- Resource requests and limits 193 | # ref: http://kubernetes.io/docs/user-guide/compute-resources/ 194 | resources: 195 | limits: 196 | cpu: 100m 197 | memory: 128Mi 198 | requests: 199 | cpu: 100m 200 | memory: 128Mi 201 | 202 | # -- Node labels for pod assignment 203 | # ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/ 204 | nodeSelector: {} 205 | -------------------------------------------------------------------------------- /cmd/mgob/mgob.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/kelseyhightower/envconfig" 5 | "os" 6 | "os/signal" 7 | "path" 8 | "syscall" 9 | 10 | log "github.com/sirupsen/logrus" 11 | "github.com/urfave/cli" 12 | 13 | "github.com/stefanprodan/mgob/pkg/api" 14 | "github.com/stefanprodan/mgob/pkg/backup" 15 | "github.com/stefanprodan/mgob/pkg/config" 16 | "github.com/stefanprodan/mgob/pkg/db" 17 | "github.com/stefanprodan/mgob/pkg/scheduler" 18 | ) 19 | 20 | var ( 21 | appConfig = &config.AppConfig{} 22 | modules = &config.ModuleConfig{} 23 | name = "mgob" 24 | version = "v1.5.0-dev" 25 | ) 26 | 27 | func beforeApp(c *cli.Context) error { 28 | level, err := log.ParseLevel(c.GlobalString("LogLevel")) 29 | if err != nil { 30 | log.Fatalf("unable to determine and set log level: %+v", err) 31 | } 32 | log.SetLevel(level) 33 | 34 | if c.GlobalBool("JSONLog") { 35 | // platforms such as Google StackDriver want logs to stdout 36 | log.SetOutput(os.Stdout) 37 | log.SetFormatter(&log.JSONFormatter{}) 38 | } 39 | 40 | log.Debug("log level set to ", c.GlobalString("LogLevel")) 41 | return nil 42 | } 43 | 44 | func main() { 45 | app := cli.NewApp() 46 | app.Name = name 47 | app.Version = version 48 | app.Usage = "mongodb dockerized backup agent" 49 | app.Action = start 50 | app.Before = beforeApp 51 | app.Flags = []cli.Flag{ 52 | cli.StringFlag{ 53 | Name: "ConfigPath,c", 54 | Usage: "plan yml files dir", 55 | Value: "/config", 56 | }, 57 | cli.StringFlag{ 58 | Name: "StoragePath,s", 59 | Usage: "backup storage", 60 | Value: "/storage", 61 | }, 62 | cli.StringFlag{ 63 | Name: "TmpPath,t", 64 | Usage: "temporary backup storage", 65 | Value: "/tmp", 66 | }, 67 | cli.StringFlag{ 68 | Name: "DataPath,d", 69 | Usage: "db dir", 70 | Value: "/data", 71 | }, 72 | cli.IntFlag{ 73 | Name: "Port,p", 74 | Usage: "Port to bind the HTTP server on", 75 | Value: 8090, 76 | }, 77 | cli.StringFlag{ 78 | Name: "Bind,b", 79 | Usage: "Host to bind the HTTP server on", 80 | Value: "", 81 | }, 82 | cli.BoolFlag{ 83 | Name: "JSONLog,j", 84 | Usage: "logs in JSON format", 85 | }, 86 | cli.StringFlag{ 87 | Name: "LogLevel,l", 88 | Usage: "logging threshold level: debug|info|warn|error|fatal|panic", 89 | Value: "info", 90 | }, 91 | } 92 | app.Run(os.Args) 93 | } 94 | 95 | func start(c *cli.Context) error { 96 | log.Infof("mgob %v", version) 97 | 98 | appConfig.LogLevel = c.String("LogLevel") 99 | appConfig.JSONLog = c.Bool("JSONLog") 100 | appConfig.Port = c.Int("Port") 101 | appConfig.Host = c.String("Bind") 102 | appConfig.ConfigPath = c.String("ConfigPath") 103 | appConfig.StoragePath = c.String("StoragePath") 104 | appConfig.TmpPath = c.String("TmpPath") 105 | appConfig.DataPath = c.String("DataPath") 106 | appConfig.Version = version 107 | 108 | log.Infof("starting with config: %+v", appConfig) 109 | 110 | err := envconfig.Process(name, modules) 111 | if err != nil { 112 | log.Fatal(err.Error()) 113 | } 114 | 115 | appConfig.UseAwsCli = true 116 | appConfig.HasGpg = true 117 | 118 | info, err := backup.CheckMongodump() 119 | if err != nil { 120 | log.Fatal(err) 121 | } 122 | log.Info(info) 123 | 124 | checkClients() 125 | 126 | plans, err := config.LoadPlans(appConfig.ConfigPath) 127 | if err != nil { 128 | log.Fatal(err) 129 | } 130 | 131 | store, err := db.Open(path.Join(appConfig.DataPath, "mgob.db")) 132 | if err != nil { 133 | log.Fatal(err) 134 | } 135 | statusStore, err := db.NewStatusStore(store) 136 | if err != nil { 137 | log.Fatal(err) 138 | } 139 | sch := scheduler.New(plans, appConfig, modules, statusStore) 140 | sch.Start() 141 | 142 | server := &api.HttpServer{ 143 | Config: appConfig, 144 | Modules: modules, 145 | Stats: statusStore, 146 | } 147 | log.Infof("starting http server on port %v", appConfig.Port) 148 | go server.Start(appConfig.Version) 149 | 150 | // wait for SIGINT (Ctrl+C) or SIGTERM (docker stop) 151 | sigChan := make(chan os.Signal, 1) 152 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 153 | sig := <-sigChan 154 | 155 | log.Infof("shutting down %v signal received", sig) 156 | 157 | return nil 158 | } 159 | 160 | func checkClients() { 161 | if modules.MinioClient { 162 | info, err := backup.CheckMinioClient() 163 | if err != nil { 164 | log.Fatal(err) 165 | } 166 | log.Info(info) 167 | } else { 168 | log.Info("Minio Client is disabled.") 169 | } 170 | 171 | if modules.AWSClient { 172 | info, err := backup.CheckAWSClient() 173 | if err != nil { 174 | log.Warn(err) 175 | appConfig.UseAwsCli = false 176 | } 177 | log.Info(info) 178 | } else { 179 | log.Info("AWS CLI is disabled.") 180 | } 181 | 182 | if modules.GnuPG { 183 | info, err := backup.CheckGpg() 184 | if err != nil { 185 | log.Warn(err) 186 | appConfig.HasGpg = false 187 | } 188 | log.Info(info) 189 | } else { 190 | log.Info("GPG is disabled.") 191 | } 192 | 193 | if modules.GCloudClient { 194 | info, err := backup.CheckGCloudClient() 195 | if err != nil { 196 | log.Fatal(err) 197 | } 198 | log.Info(info) 199 | } else { 200 | log.Info("Google Storage is disabled.") 201 | } 202 | 203 | if modules.AzureClient { 204 | info, err := backup.CheckAzureClient() 205 | if err != nil { 206 | log.Fatal(err) 207 | } 208 | log.Info(info) 209 | } else { 210 | log.Info("Azure Storage is disabled.") 211 | } 212 | 213 | if modules.RCloneClient { 214 | info, err := backup.CheckRCloneClient() 215 | if err != nil { 216 | log.Fatal(err) 217 | } 218 | log.Info(info) 219 | } else { 220 | log.Info("RClone is disabled.") 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stefanprodan/mgob 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/boltdb/bolt v1.3.1 7 | github.com/codeskyblue/go-sh v0.0.0-20200712050446-30169cf553fe 8 | github.com/dustin/go-humanize v1.0.0 9 | github.com/go-chi/chi v1.5.4 10 | github.com/go-chi/render v1.0.1 11 | github.com/kelseyhightower/envconfig v1.4.0 12 | github.com/pkg/errors v0.9.1 13 | github.com/pkg/sftp v1.13.4 14 | github.com/prometheus/client_golang v1.12.1 15 | github.com/robfig/cron v1.2.0 16 | github.com/sirupsen/logrus v1.8.1 17 | github.com/urfave/cli v1.22.5 18 | golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed 19 | gopkg.in/yaml.v2 v2.4.0 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 9 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 10 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 11 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 12 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 13 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 14 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 15 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 16 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 17 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 18 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 19 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 20 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 21 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 22 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 23 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 24 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 25 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 26 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 27 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 28 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 29 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 30 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 31 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 32 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 33 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 34 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 35 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 36 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 37 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 38 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 39 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 40 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 41 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 42 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 43 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 44 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 45 | github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= 46 | github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= 47 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 48 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 49 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 50 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 51 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 52 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 53 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 54 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 55 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 56 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q= 57 | github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM= 58 | github.com/codeskyblue/go-sh v0.0.0-20200712050446-30169cf553fe h1:69JI97HlzP+PH5Mi1thcGlDoBr6PS2Oe+l3mNmAkbs4= 59 | github.com/codeskyblue/go-sh v0.0.0-20200712050446-30169cf553fe/go.mod h1:VQx0hjo2oUeQkQUET7wRwradO6f+fN5jzXgB/zROxxE= 60 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 61 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 62 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 63 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 64 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 65 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 66 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 67 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 68 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 69 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 70 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 71 | github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= 72 | github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= 73 | github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= 74 | github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= 75 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 76 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 77 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 78 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 79 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 80 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 81 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 82 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 83 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 84 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 85 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 86 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 87 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 88 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 89 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 90 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 91 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 92 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 93 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 94 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 95 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 96 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 97 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 98 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 99 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 100 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 101 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 102 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 103 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 104 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 105 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 106 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 107 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 108 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 109 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 110 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 111 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 112 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 113 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 114 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 115 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 116 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 117 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 118 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 119 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 120 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 121 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 122 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 123 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 124 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 125 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 126 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 127 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 128 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 129 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 130 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 131 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 132 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 133 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 134 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 135 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 136 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 137 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 138 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 139 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 140 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 141 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 142 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 143 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 144 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 145 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 146 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 147 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 148 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 149 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 150 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 151 | github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= 152 | github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= 153 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 154 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 155 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 156 | github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= 157 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 158 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 159 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 160 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 161 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 162 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 163 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 164 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 165 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 166 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 167 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 168 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 169 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 170 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 171 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 172 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 173 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 174 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 175 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 176 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 177 | github.com/pkg/sftp v1.13.4 h1:Lb0RYJCmgUcBgZosfoi9Y9sbl6+LJgOIgk/2Y4YjMFg= 178 | github.com/pkg/sftp v1.13.4/go.mod h1:LzqnAvaD5TWeNBsZpfKxSYn1MbjWwOsCIAFFJbpIsK8= 179 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 180 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 181 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 182 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 183 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 184 | github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 185 | github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= 186 | github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= 187 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 188 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 189 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 190 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 191 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 192 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 193 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 194 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 195 | github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= 196 | github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= 197 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 198 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 199 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 200 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 201 | github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= 202 | github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 203 | github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= 204 | github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= 205 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 206 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 207 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 208 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 209 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 210 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 211 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 212 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 213 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 214 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 215 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 216 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 217 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 218 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 219 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 220 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 221 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 222 | github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= 223 | github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 224 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 225 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 226 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 227 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 228 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 229 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 230 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 231 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 232 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 233 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 234 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 235 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 236 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 237 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 238 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 239 | golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed h1:YoWVYYAfvQ4ddHv3OKmIvX7NCAhFGTj62VP2l2kfBbA= 240 | golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 241 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 242 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 243 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 244 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 245 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 246 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 247 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 248 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 249 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 250 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 251 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 252 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 253 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 254 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 255 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 256 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 257 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 258 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 259 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 260 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 261 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 262 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 263 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 264 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 265 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 266 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 267 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 268 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 269 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 270 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 271 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 272 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 273 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 274 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 275 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 276 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 277 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 278 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 279 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 280 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 281 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 282 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 283 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 284 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 285 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 286 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 287 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 288 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 289 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 290 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 291 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 292 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 293 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 294 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 295 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 296 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 297 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 298 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 299 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 300 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 301 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 302 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 303 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 304 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 305 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 306 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 307 | golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 308 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 309 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 310 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 311 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 312 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 313 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 314 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 315 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 316 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 317 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 318 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 319 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 320 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 321 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 322 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 323 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 324 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 325 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 326 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 327 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 328 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 329 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 330 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 331 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 332 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 333 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 334 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 335 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 336 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 337 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 338 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 339 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 340 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 341 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 342 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 343 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 344 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 345 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 346 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 347 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 348 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 349 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 350 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 351 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 352 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 353 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 354 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 355 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= 356 | golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 357 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= 358 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 359 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 360 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 361 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 362 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 363 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 364 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 365 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 366 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 367 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 368 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 369 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 370 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 371 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 372 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 373 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 374 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 375 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 376 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 377 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 378 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 379 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 380 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 381 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 382 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 383 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 384 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 385 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 386 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 387 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 388 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 389 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 390 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 391 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 392 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 393 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 394 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 395 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 396 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 397 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 398 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 399 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 400 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 401 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 402 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 403 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 404 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 405 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 406 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 407 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 408 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 409 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 410 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 411 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 412 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 413 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 414 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 415 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 416 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 417 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 418 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 419 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 420 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 421 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 422 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 423 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 424 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 425 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 426 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 427 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 428 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 429 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 430 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 431 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 432 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 433 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 434 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 435 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 436 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 437 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 438 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 439 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 440 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 441 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 442 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 443 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 444 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 445 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 446 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 447 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 448 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 449 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 450 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 451 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 452 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 453 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 454 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 455 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 456 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 457 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 458 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 459 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 460 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 461 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 462 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 463 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 464 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 465 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 466 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 467 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 468 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 469 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 470 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 471 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 472 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 473 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 474 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 475 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 476 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 477 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 478 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 479 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 480 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 481 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 482 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 483 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 484 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 485 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 486 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 487 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 488 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 489 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 490 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 491 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 492 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 493 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 494 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 495 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 496 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 497 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 498 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 499 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 500 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 501 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 502 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 503 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 504 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 505 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 506 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 507 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 508 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 509 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 510 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 511 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 512 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 513 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 514 | -------------------------------------------------------------------------------- /k8s/README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes MongoDB Backup Operator 2 | 3 | This is a step by step guide on setting up 4 | MGOB to automate MongoDB backups on Google Kubernetes Engine. 5 | 6 | Requirements: 7 | 8 | - GKE cluster minimum version v1.8 9 | - kubctl admin config 10 | 11 | Clone the mgob repository: 12 | 13 | ```bash 14 | $ git clone https://github.com/stefanprodan/mgob.git 15 | $ cd mgob/k8s 16 | ``` 17 | 18 | Create a cluster admin user: 19 | 20 | ```bash 21 | kubectl create clusterrolebinding "cluster-admin-$(whoami)" \ 22 | --clusterrole=cluster-admin \ 23 | --user="$(gcloud config get-value core/account)" 24 | ``` 25 | 26 | ### Create a MongoDB RS with Stateful Sets 27 | 28 | Create the `db` namespace: 29 | 30 | ```bash 31 | $ kubectl apply -f ./namespace.yaml 32 | namespace "db" created 33 | ``` 34 | 35 | Create the `ssd` and `hdd` storage classes: 36 | 37 | ```bash 38 | $ kubectl apply -f ./storage.yaml 39 | storageclass "ssd" created 40 | storageclass "hdd" created 41 | ``` 42 | 43 | Create the `startup-script` _Daemon Set_ to disable hugepage on all hosts: 44 | 45 | ```bash 46 | $ kubectl apply -f ./mongo-ds.yaml 47 | daemonset "startup-script" created 48 | ``` 49 | 50 | Create a 3 nodes _Replica Set_, each replica provisioned with a 1Gi SSD disk: 51 | 52 | ```bash 53 | $ kubectl apply -f ./mongo-rs.yaml 54 | service "mongo" created 55 | statefulset "mongo" created 56 | clusterrole "default" configured 57 | serviceaccount "default" configured 58 | clusterrolebinding "system:serviceaccount:db:default" configured 59 | ``` 60 | 61 | The above command creates a _Headless Service_ and a _Stateful Set_ for the Mongo _Replica Set_ and a _Service Account_ for the Mongo sidecar. 62 | Each pod contains a Mongo instance and a sidecar. 63 | The sidecar will initialize the _Replica Set_ and will add the rs members as soon as the pods are up. 64 | You can safely scale up or down the _Stateful Set_ replicas, the sidecar will add or remove rs members. 65 | 66 | You can monitor the rs initialization by looking at the sidecar logs: 67 | 68 | ```bash 69 | $ kubectl -n db logs mongo-0 mongo-sidecar 70 | Using mongo port: 27017 71 | Starting up mongo-k8s-sidecar 72 | The cluster domain 'cluster.local' was successfully verified. 73 | Pod has been elected for replica set initialization 74 | initReplSet 10.52.2.127:27017 75 | ``` 76 | 77 | Inspect the newly created cluster with `kubectl`: 78 | 79 | ```bash 80 | $ kubectl -n db get pods --selector=role=mongo 81 | NAME READY STATUS RESTARTS AGE 82 | po/mongo-0 2/2 Running 0 8m 83 | po/mongo-1 2/2 Running 0 7m 84 | po/mongo-2 2/2 Running 0 6m 85 | ``` 86 | 87 | Connect to the container running in `mongo-0` pod, create a `test` database and insert some data: 88 | 89 | ```bash 90 | $ kubectl -n db exec -it mongo-0 -c mongod mongo 91 | rs0:PRIMARY> use test 92 | rs0:PRIMARY> db.inventory.insert({item: "one", val: "two" }) 93 | WriteResult({ "nInserted" : 1 }) 94 | ``` 95 | 96 | Each MongoDB replica has its own DNS address as in `..`. 97 | If you need to access the _Replica Set_ from another namespace use the following connection url: 98 | 99 | ``` 100 | mongodb://mongo-0.mongo.db,mongo-1.mongo.db,mongo-2.mongo.db:27017/dbname_? 101 | ``` 102 | 103 | Test the connectivity by creating a temporary pod in the default namespace: 104 | 105 | ``` 106 | $ kubectl run -it --rm --restart=Never mongo-cli --image=mongo --command -- /bin/bash 107 | root@mongo-cli:/# mongo "mongodb://mongo-0.mongo.db,mongo-1.mongo.db,mongo-2.mongo.db:27017/test" 108 | rs0:PRIMARY> db.getCollectionNames() 109 | [ "inventory" ] 110 | ``` 111 | 112 | The [mongo-k8s-sidecar](https://github.com/cvallance/mongo-k8s-sidecar) deals with ReplicaSet provisioning only. 113 | if you want to run a sharded cluster on GKE, take a look at [pkdone/gke-mongodb-shards-demo](https://github.com/pkdone/gke-mongodb-shards-demo). 114 | 115 | ### Create a MongoDB Backup agent with Stateful Sets 116 | 117 | First let's create two databases `test1` and `test2`: 118 | 119 | ```bash 120 | $ kubectl -n db exec -it mongo-0 -c mongod mongo 121 | rs0:PRIMARY> use test1 122 | rs0:PRIMARY> db.inventory.insert({item: "one", val: "two" }) 123 | WriteResult({ "nInserted" : 1 }) 124 | rs0:PRIMARY> use test2 125 | rs0:PRIMARY> db.inventory.insert({item: "one", val: "two" }) 126 | WriteResult({ "nInserted" : 1 }) 127 | ``` 128 | 129 | Create a ConfigMap to schedule backups every minute for `test1` and every two minutes for `test2`: 130 | 131 | ```yaml 132 | kind: ConfigMap 133 | apiVersion: v1 134 | metadata: 135 | labels: 136 | role: backup 137 | name: mgob-config 138 | namespace: db 139 | data: 140 | test1.yml: | 141 | target: 142 | host: "mongo-0.mongo.db,mongo-1.mongo.db,mongo-2.mongo.db" 143 | port: 27017 144 | database: "test1" 145 | scheduler: 146 | cron: "*/1 * * * *" 147 | retention: 5 148 | timeout: 60 149 | test2.yml: | 150 | target: 151 | host: "mongo-0.mongo.db,mongo-1.mongo.db,mongo-2.mongo.db" 152 | port: 27017 153 | database: "test2" 154 | scheduler: 155 | cron: "*/2 * * * *" 156 | retention: 10 157 | timeout: 60 158 | ``` 159 | 160 | Apply the config: 161 | 162 | ```bash 163 | kubectl apply -f ./mgob-cfg.yaml 164 | ``` 165 | 166 | Deploy mgob _Headless Service_ and _Stateful Set_ with two disks, 3Gi for the long term backup storage 167 | and 1Gi for the temporary storage of the running backups: 168 | 169 | ```bash 170 | kubectl apply -f ./mgob-dep.yaml 171 | ``` 172 | 173 | To monitor the backups you can stream the mgob logs: 174 | 175 | ```bash 176 | $ kubectl -n db logs -f mgob-0 177 | msg="Backup started" plan=test1 178 | msg="Backup finished in 261.76829ms archive test1-1514491560.gz size 307 B" plan=test1 179 | msg="Next run at 2017-12-28 20:07:00 +0000 UTC" plan=test1 180 | msg="Backup started" plan=test2 181 | msg="Backup finished in 266.635088ms archive test2-1514491560.gz size 313 B" plan=test2 182 | msg="Next run at 2017-12-28 20:08:00 +0000 UTC" plan=test2 183 | ``` 184 | 185 | Or you can `curl` the mgob API: 186 | 187 | ```bash 188 | kubectl -n db exec -it mgob-0 -- curl mgob-0.mgob.db:8090/status 189 | ``` 190 | 191 | Let's run an on demand backup for `test2` database: 192 | 193 | ```bash 194 | kubectl -n db exec -it mgob-0 -- curl -XPOST mgob-0.mgob.db:8090/backup/test2 195 | {"plan":"test2","file":"test2-1514492080.gz","duration":"61.109042ms","size":"313 B","timestamp":"2017-12-28T20:14:40.604057546Z"} 196 | ``` 197 | 198 | You can restore a backup from within mgob container. 199 | Exec into mgob and identify the backup you want to restore, the backups are in `/storage/`. 200 | 201 | ```bash 202 | $ kubectl -n db exec -it mgob-0 /bin/bash 203 | ls -lh /storage/test1 204 | -rw-r--r-- 1 root root 307 Dec 28 20:23 test1-1514492580.gz 205 | -rw-r--r-- 1 root root 162 Dec 28 20:23 test1-1514492580.log 206 | -rw-r--r-- 1 root root 307 Dec 28 20:24 test1-1514492640.gz 207 | -rw-r--r-- 1 root root 162 Dec 28 20:24 test1-1514492640.log 208 | ``` 209 | 210 | Use `mongorestore` to connect to your MongoDB server and restore a backup: 211 | 212 | ```bash 213 | $ kubectl -n db exec -it mgob-0 /bin/bash 214 | mongorestore --gzip --archive=/storage/test1/test1-1514492640.gz --host mongo-0.mongo.db:27017 --drop 215 | ``` 216 | 217 | ### Monitoring and alerting 218 | 219 | For each backup plan you can configure alerting via email or Slack: 220 | 221 | ```yaml 222 | # Email notifications (optional) 223 | smtp: 224 | server: smtp.company.com 225 | port: 465 226 | username: user 227 | password: secret 228 | from: mgob@company.com 229 | to: 230 | - devops@company.com 231 | - alerts@company.com 232 | # Slack notifications (optional) 233 | slack: 234 | url: https://hooks.slack.com/services/xxxx/xxx/xx 235 | channel: devops-alerts 236 | username: mgob 237 | # 'true' to notify only on failures 238 | warnOnly: false 239 | ``` 240 | 241 | Mgob exposes Prometheus metrics on the `/metrics` endpoint. 242 | 243 | Successful/failed backups counter: 244 | 245 | ``` 246 | mgob_scheduler_backup_total{plan="test1",status="200"} 8 247 | mgob_scheduler_backup_total{plan="test2",status="500"} 2 248 | ``` 249 | 250 | Backup duration: 251 | 252 | ``` 253 | mgob_scheduler_backup_latency{plan="test1",status="200",quantile="0.5"} 2.149668417 254 | mgob_scheduler_backup_latency{plan="test1",status="200",quantile="0.9"} 2.39848413 255 | mgob_scheduler_backup_latency{plan="test1",status="200",quantile="0.99"} 2.39848413 256 | ``` 257 | 258 | ### Backup to GCP Storage Bucket 259 | 260 | For long term backup storage you could use a GCP Bucket since is a cheaper option than keeping all 261 | backups on disk. 262 | 263 | First you need to create an GCP service account key from the `API & Services` page. Download the JSON file 264 | and rename it to `service-account.json`. 265 | 266 | Store the JSON file as a secret in the `db` namespace: 267 | 268 | ```bash 269 | kubectl -n db create secret generic gcp-key --from-file=service-account.json=service-account.json 270 | ``` 271 | 272 | From the GCP web UI, navigate to _Storage_ and create a regional bucket named `mgob`. 273 | If the bucket name is taken you'll need to change it in the `mgob-gstore-cfg.yaml` file: 274 | 275 | ```yaml 276 | kind: ConfigMap 277 | apiVersion: v1 278 | metadata: 279 | labels: 280 | role: mongo-backup 281 | name: mgob-gstore-config 282 | namespace: db 283 | data: 284 | test.yml: | 285 | target: 286 | host: "mongo-0.mongo.db,mongo-1.mongo.db,mongo-2.mongo.db" 287 | port: 27017 288 | database: "test" 289 | scheduler: 290 | cron: "*/1 * * * *" 291 | retention: 1 292 | timeout: 60 293 | gcloud: 294 | bucket: "mgob" 295 | keyFilePath: /etc/mgob/service-account.json 296 | ``` 297 | 298 | Apply the config: 299 | 300 | ```bash 301 | kubectl apply -f ./mgob-gstore-cfg.yaml 302 | ``` 303 | 304 | Deploy mgob with the `gcp-key` secret map to a volume: 305 | 306 | ```bash 307 | kubectl apply -f ./mgob-gstore-dep.yaml 308 | ``` 309 | 310 | After one minute the backup will be uploaded to the GCP bucket: 311 | 312 | ```bash 313 | $ kubectl -n db logs -f mgob-0 314 | msg="Google Cloud SDK 181.0.0 bq 2.0.27 core 2017.11.28 gsutil 4.28" 315 | msg="Backup started" plan=test 316 | msg="GCloud upload finished Copying file:///storage/test/test-1514544660.gz" 317 | ``` 318 | -------------------------------------------------------------------------------- /k8s/mgob-cfg.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: ConfigMap 3 | apiVersion: v1 4 | metadata: 5 | labels: 6 | role: mongo-backup 7 | name: mgob-config 8 | namespace: db 9 | data: 10 | test1.yml: | 11 | target: 12 | host: "mongo-0.mongo.db,mongo-1.mongo.db,mongo-2.mongo.db" 13 | port: 27017 14 | database: "test1" 15 | scheduler: 16 | cron: "*/1 * * * *" 17 | retention: 5 18 | timeout: 60 19 | test2.yml: | 20 | target: 21 | host: "mongo-0.mongo.db,mongo-1.mongo.db,mongo-2.mongo.db" 22 | port: 27017 23 | database: "test2" 24 | scheduler: 25 | cron: "*/2 * * * *" 26 | retention: 10 27 | timeout: 60 28 | -------------------------------------------------------------------------------- /k8s/mgob-dep.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: mgob 6 | namespace: db 7 | labels: 8 | name: mgob 9 | spec: 10 | ports: 11 | - port: 8090 12 | targetPort: 8090 13 | clusterIP: None 14 | selector: 15 | role: mongo-backup 16 | --- 17 | apiVersion: apps/v1beta2 18 | kind: StatefulSet 19 | metadata: 20 | name: mgob 21 | namespace: db 22 | spec: 23 | serviceName: "mgob" 24 | replicas: 1 25 | selector: 26 | matchLabels: 27 | role: mongo-backup 28 | template: 29 | metadata: 30 | labels: 31 | role: mongo-backup 32 | spec: 33 | containers: 34 | - name: mgobd 35 | image: stefanprodan/mgob:edge 36 | imagePullPolicy: Always 37 | ports: 38 | - containerPort: 8090 39 | protocol: TCP 40 | volumeMounts: 41 | - name: mgob-storage 42 | mountPath: /storage 43 | - name: mgob-tmp 44 | mountPath: /tmp 45 | - name: mgob-tmp 46 | mountPath: /data 47 | - mountPath: /config/test1.yml 48 | name: mgob-config 49 | subPath: test1.yml 50 | - mountPath: /config/test2.yml 51 | name: mgob-config 52 | subPath: test2.yml 53 | volumes: 54 | - name: mgob-config 55 | configMap: 56 | name: mgob-config 57 | items: 58 | - key: test1.yml 59 | path: test1.yml 60 | - key: test2.yml 61 | path: test2.yml 62 | volumeClaimTemplates: 63 | - metadata: 64 | name: mgob-storage 65 | annotations: 66 | volume.beta.kubernetes.io/storage-class: "hdd" 67 | spec: 68 | accessModes: ["ReadWriteOnce"] 69 | resources: 70 | requests: 71 | storage: 3Gi 72 | - metadata: 73 | name: mgob-tmp 74 | annotations: 75 | volume.beta.kubernetes.io/storage-class: "hdd" 76 | spec: 77 | accessModes: ["ReadWriteOnce"] 78 | resources: 79 | requests: 80 | storage: 1Gi 81 | -------------------------------------------------------------------------------- /k8s/mgob-gstore-cfg.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: ConfigMap 3 | apiVersion: v1 4 | metadata: 5 | labels: 6 | role: mongo-backup 7 | name: mgob-gstore-config 8 | namespace: db 9 | data: 10 | test.yml: | 11 | target: 12 | host: "mongo-0.mongo.db,mongo-1.mongo.db,mongo-2.mongo.db" 13 | port: 27017 14 | database: "test" 15 | scheduler: 16 | cron: "*/1 * * * *" 17 | retention: 1 18 | timeout: 60 19 | gcloud: 20 | bucket: "mgob" 21 | keyFilePath: /etc/mgob/service-account.json 22 | -------------------------------------------------------------------------------- /k8s/mgob-gstore-dep.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: mgob 6 | namespace: db 7 | labels: 8 | name: mgob 9 | spec: 10 | ports: 11 | - port: 8090 12 | targetPort: 8090 13 | clusterIP: None 14 | selector: 15 | role: mongo-backup 16 | --- 17 | apiVersion: apps/v1beta2 18 | kind: StatefulSet 19 | metadata: 20 | name: mgob 21 | namespace: db 22 | spec: 23 | serviceName: "mgob" 24 | replicas: 1 25 | selector: 26 | matchLabels: 27 | role: mongo-backup 28 | template: 29 | metadata: 30 | labels: 31 | role: mongo-backup 32 | spec: 33 | containers: 34 | - name: mgobd 35 | image: stefanprodan/mgob:edge 36 | imagePullPolicy: Always 37 | ports: 38 | - containerPort: 8090 39 | protocol: TCP 40 | volumeMounts: 41 | - name: mgob-storage 42 | mountPath: /storage 43 | - name: mgob-data 44 | mountPath: /data 45 | - name: mgob-data 46 | mountPath: /tmp 47 | - name: mgob-gstore-config 48 | mountPath: /config/test.yml 49 | subPath: test.yml 50 | - name: gcp-key 51 | mountPath: "/etc/mgob" 52 | readOnly: true 53 | volumes: 54 | - name: mgob-gstore-config 55 | configMap: 56 | name: mgob-gstore-config 57 | items: 58 | - key: test.yml 59 | path: test.yml 60 | - name: gcp-key 61 | secret: 62 | secretName: gcp-key 63 | volumeClaimTemplates: 64 | - metadata: 65 | name: mgob-storage 66 | annotations: 67 | volume.beta.kubernetes.io/storage-class: "hdd" 68 | spec: 69 | accessModes: ["ReadWriteOnce"] 70 | resources: 71 | requests: 72 | storage: 3Gi 73 | - metadata: 74 | name: mgob-data 75 | annotations: 76 | volume.beta.kubernetes.io/storage-class: "hdd" 77 | spec: 78 | accessModes: ["ReadWriteOnce"] 79 | resources: 80 | requests: 81 | storage: 1Gi 82 | -------------------------------------------------------------------------------- /k8s/mongo-ds.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: DaemonSet 3 | apiVersion: apps/v1 4 | metadata: 5 | name: startup-script 6 | namespace: db 7 | spec: 8 | selector: 9 | matchLabels: 10 | role: startup-script 11 | template: 12 | metadata: 13 | labels: 14 | role: startup-script 15 | spec: 16 | hostPID: true 17 | containers: 18 | - name: startup-script 19 | image: gcr.io/google-containers/startup-script:v1 20 | securityContext: 21 | privileged: true 22 | env: 23 | - name: STARTUP_SCRIPT 24 | value: | 25 | #! /bin/bash 26 | set -o errexit 27 | set -o pipefail 28 | set -o nounset 29 | 30 | echo 'never' > /sys/kernel/mm/transparent_hugepage/enabled 31 | echo 'never' > /sys/kernel/mm/transparent_hugepage/defrag 32 | -------------------------------------------------------------------------------- /k8s/mongo-rs.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: mongo 6 | namespace: db 7 | labels: 8 | name: mongo 9 | environment: test 10 | spec: 11 | ports: 12 | - port: 27017 13 | targetPort: 27017 14 | clusterIP: None 15 | selector: 16 | role: mongo 17 | environment: test 18 | --- 19 | apiVersion: apps/v1beta2 20 | kind: StatefulSet 21 | metadata: 22 | name: mongo 23 | namespace: db 24 | spec: 25 | serviceName: "mongo" 26 | replicas: 3 27 | selector: 28 | matchLabels: 29 | role: mongo 30 | environment: test 31 | template: 32 | metadata: 33 | labels: 34 | role: mongo 35 | environment: test 36 | spec: 37 | terminationGracePeriodSeconds: 10 38 | containers: 39 | - name: mongod 40 | image: mongo:3.6 41 | command: 42 | - mongod 43 | - "--replSet" 44 | - rs0 45 | - "--bind_ip" 46 | - 0.0.0.0 47 | - "--smallfiles" 48 | - "--noprealloc" 49 | ports: 50 | - containerPort: 27017 51 | volumeMounts: 52 | - name: mongo-storage 53 | mountPath: /data/db 54 | - name: mongo-sidecar 55 | image: cvallance/mongo-k8s-sidecar 56 | env: 57 | - name: MONGO_SIDECAR_POD_LABELS 58 | value: "role=mongo,environment=test" 59 | - name: KUBE_NAMESPACE 60 | value: "db" 61 | volumeClaimTemplates: 62 | - metadata: 63 | name: mongo-storage 64 | annotations: 65 | volume.beta.kubernetes.io/storage-class: "ssd" 66 | spec: 67 | accessModes: [ "ReadWriteOnce" ] 68 | resources: 69 | requests: 70 | storage: 1Gi 71 | --- 72 | kind: ClusterRole 73 | apiVersion: rbac.authorization.k8s.io/v1 74 | metadata: 75 | name: default 76 | rules: 77 | - apiGroups: 78 | - "" 79 | resources: 80 | - pods 81 | - services 82 | - endpoints 83 | verbs: 84 | - get 85 | - list 86 | - watch 87 | --- 88 | apiVersion: v1 89 | kind: ServiceAccount 90 | metadata: 91 | name: default 92 | namespace: db 93 | --- 94 | kind: ClusterRoleBinding 95 | apiVersion: rbac.authorization.k8s.io/v1 96 | metadata: 97 | name: system:serviceaccount:db:default 98 | roleRef: 99 | apiGroup: rbac.authorization.k8s.io 100 | kind: ClusterRole 101 | name: default 102 | subjects: 103 | - kind: ServiceAccount 104 | name: default 105 | namespace: db 106 | -------------------------------------------------------------------------------- /k8s/namespace.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Namespace 4 | metadata: 5 | name: db 6 | -------------------------------------------------------------------------------- /k8s/storage.yaml: -------------------------------------------------------------------------------- 1 | # XFS is not supported by GKE COS, use Ubuntu 2 | # ref https://github.com/kubernetes/kubernetes/issues/47125 3 | --- 4 | kind: StorageClass 5 | apiVersion: storage.k8s.io/v1 6 | metadata: 7 | name: ssd 8 | namespace: db 9 | provisioner: kubernetes.io/gce-pd 10 | parameters: 11 | type: pd-ssd 12 | #fsType: xfs 13 | --- 14 | kind: StorageClass 15 | apiVersion: storage.k8s.io/v1 16 | metadata: 17 | name: hdd 18 | namespace: db 19 | provisioner: kubernetes.io/gce-pd 20 | parameters: 21 | type: pd-standard 22 | #fsType: xfs 23 | -------------------------------------------------------------------------------- /pkg/api/backup.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/dustin/go-humanize" 10 | "github.com/go-chi/chi" 11 | "github.com/go-chi/render" 12 | log "github.com/sirupsen/logrus" 13 | 14 | "github.com/stefanprodan/mgob/pkg/backup" 15 | "github.com/stefanprodan/mgob/pkg/config" 16 | "github.com/stefanprodan/mgob/pkg/notifier" 17 | ) 18 | 19 | func configCtx(data config.AppConfig, modules config.ModuleConfig) func(next http.Handler) http.Handler { 20 | return func(next http.Handler) http.Handler { 21 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 | r = r.WithContext(context.WithValue(r.Context(), "app.config", data)) 23 | r = r.WithContext(context.WithValue(r.Context(), "app.modules", modules)) 24 | next.ServeHTTP(w, r) 25 | }) 26 | } 27 | } 28 | 29 | func postBackup(w http.ResponseWriter, r *http.Request) { 30 | cfg := r.Context().Value("app.config").(config.AppConfig) 31 | modules := r.Context().Value("app.modules").(config.ModuleConfig) 32 | planID := chi.URLParam(r, "planID") 33 | plan, err := config.LoadPlan(cfg.ConfigPath, planID) 34 | if err != nil { 35 | render.Status(r, 500) 36 | render.JSON(w, r, map[string]string{"error": err.Error()}) 37 | return 38 | } 39 | 40 | log.WithField("plan", planID).Info("On demand backup started") 41 | 42 | res, err := backup.Run(plan, &cfg, &modules) 43 | if err != nil { 44 | log.WithField("plan", planID).Errorf("On demand backup failed %v", err) 45 | if err := notifier.SendNotification(fmt.Sprintf("%v on demand backup failed", planID), 46 | err.Error(), true, plan); err != nil { 47 | log.WithField("plan", plan.Name).Errorf("Notifier failed for on demand backup %v", err) 48 | } 49 | render.Status(r, 500) 50 | render.JSON(w, r, map[string]string{"error": err.Error()}) 51 | } else { 52 | log.WithField("plan", plan.Name).Infof("On demand backup finished in %v archive %v size %v", 53 | res.Duration, res.Name, humanize.Bytes(uint64(res.Size))) 54 | if err := notifier.SendNotification(fmt.Sprintf("%v on demand backup finished", plan.Name), 55 | fmt.Sprintf("%v backup finished in %v archive size %v", 56 | res.Name, res.Duration, humanize.Bytes(uint64(res.Size))), 57 | false, plan); err != nil { 58 | log.WithField("plan", plan.Name).Errorf("Notifier failed for on demand backup %v", err) 59 | } 60 | render.JSON(w, r, toBackupResult(res)) 61 | } 62 | } 63 | 64 | type backupResult struct { 65 | Plan string `json:"plan"` 66 | File string `json:"file"` 67 | Duration string `json:"duration"` 68 | Size string `json:"size"` 69 | Timestamp time.Time `json:"timestamp"` 70 | } 71 | 72 | func toBackupResult(res backup.Result) backupResult { 73 | return backupResult{ 74 | Plan: res.Plan, 75 | Duration: fmt.Sprintf("%v", res.Duration), 76 | File: res.Name, 77 | Size: humanize.Bytes(uint64(res.Size)), 78 | Timestamp: res.Timestamp, 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pkg/api/metrics.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/go-chi/chi" 7 | "github.com/prometheus/client_golang/prometheus/promhttp" 8 | ) 9 | 10 | func metricsRouter() http.Handler { 11 | promHandler := func(next http.Handler) http.Handler { return promhttp.Handler() } 12 | emptyHandler := func(w http.ResponseWriter, r *http.Request) {} 13 | r := chi.NewRouter() 14 | r.Use(promHandler) 15 | r.Get("/", emptyHandler) 16 | return r 17 | } 18 | -------------------------------------------------------------------------------- /pkg/api/server.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/go-chi/chi" 9 | "github.com/go-chi/chi/middleware" 10 | log "github.com/sirupsen/logrus" 11 | 12 | "github.com/stefanprodan/mgob/pkg/config" 13 | "github.com/stefanprodan/mgob/pkg/db" 14 | ) 15 | 16 | type HttpServer struct { 17 | Config *config.AppConfig 18 | Modules *config.ModuleConfig 19 | Stats *db.StatusStore 20 | } 21 | 22 | func (s *HttpServer) Start(version string) { 23 | 24 | r := chi.NewRouter() 25 | r.Use(middleware.Recoverer) 26 | if s.Config.LogLevel == "debug" { 27 | r.Use(middleware.DefaultLogger) 28 | } 29 | 30 | r.Mount("/metrics", metricsRouter()) 31 | r.Mount("/debug", middleware.Profiler()) 32 | 33 | r.Route("/version", func(r chi.Router) { 34 | r.Use(appVersionCtx(version)) 35 | r.Get("/", getVersion) 36 | }) 37 | 38 | r.Route("/status", func(r chi.Router) { 39 | r.Use(statusCtx(s.Stats)) 40 | r.Get("/", getStatus) 41 | r.Get("/{planID}", getPlanStatus) 42 | }) 43 | 44 | r.Route("/backup", func(r chi.Router) { 45 | r.Use(configCtx(*s.Config, *s.Modules)) 46 | r.Post("/{planID}", postBackup) 47 | }) 48 | 49 | FileServer(r, "/storage", http.Dir(s.Config.StoragePath)) 50 | 51 | log.Error(http.ListenAndServe(fmt.Sprintf("%s:%v", s.Config.Host, s.Config.Port), r)) 52 | } 53 | 54 | func FileServer(r chi.Router, path string, root http.FileSystem) { 55 | if strings.ContainsAny(path, "{}*") { 56 | panic("FileServer does not permit URL parameters.") 57 | } 58 | 59 | fs := http.StripPrefix(path, http.FileServer(root)) 60 | 61 | if path != "/" && path[len(path)-1] != '/' { 62 | r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP) 63 | path += "/" 64 | } 65 | path += "*" 66 | 67 | r.Get(path, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 68 | fs.ServeHTTP(w, r) 69 | })) 70 | } 71 | -------------------------------------------------------------------------------- /pkg/api/status.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/go-chi/chi" 8 | "github.com/go-chi/render" 9 | 10 | "github.com/stefanprodan/mgob/pkg/db" 11 | ) 12 | 13 | type appStatus []*db.Status 14 | 15 | func (a *appStatus) Render(w http.ResponseWriter, r *http.Request) error { 16 | return nil 17 | } 18 | 19 | func statusCtx(store *db.StatusStore) func(next http.Handler) http.Handler { 20 | return func(next http.Handler) http.Handler { 21 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 | data, err := store.GetAll() 23 | if err != nil { 24 | render.Status(r, 500) 25 | render.JSON(w, r, map[string]string{"error": err.Error()}) 26 | return 27 | } 28 | 29 | r = r.WithContext(context.WithValue(r.Context(), "app.status", appStatus(data))) 30 | next.ServeHTTP(w, r) 31 | }) 32 | } 33 | } 34 | 35 | func getStatus(w http.ResponseWriter, r *http.Request) { 36 | data := r.Context().Value("app.status").(appStatus) 37 | render.JSON(w, r, data) 38 | } 39 | 40 | func getPlanStatus(w http.ResponseWriter, r *http.Request) { 41 | data := r.Context().Value("app.status").(appStatus) 42 | planID := chi.URLParam(r, "planID") 43 | for _, s := range data { 44 | if s.Plan == planID { 45 | render.JSON(w, r, s) 46 | return 47 | } 48 | } 49 | 50 | render.Status(r, 404) 51 | render.JSON(w, r, map[string]string{"error": "Plan not found"}) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/api/version.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "runtime" 7 | "strconv" 8 | 9 | "github.com/go-chi/render" 10 | ) 11 | 12 | type appVersion map[string]string 13 | 14 | func (a *appVersion) Render(w http.ResponseWriter, r *http.Request) error { 15 | return nil 16 | } 17 | 18 | func appVersionCtx(version string) func(next http.Handler) http.Handler { 19 | return func(next http.Handler) http.Handler { 20 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 21 | data := appVersion{ 22 | "mgob_version": version, 23 | "repository": "github.com/stefanprodan/mgob", 24 | "go_version": runtime.Version(), 25 | "os": runtime.GOOS, 26 | "arch": runtime.GOARCH, 27 | "max_procs": strconv.FormatInt(int64(runtime.GOMAXPROCS(0)), 10), 28 | "goroutines": strconv.FormatInt(int64(runtime.NumGoroutine()), 10), 29 | "cpu_count": strconv.FormatInt(int64(runtime.NumCPU()), 10), 30 | } 31 | r = r.WithContext(context.WithValue(r.Context(), "app.version", data)) 32 | next.ServeHTTP(w, r) 33 | }) 34 | } 35 | } 36 | 37 | func getVersion(w http.ResponseWriter, r *http.Request) { 38 | data := r.Context().Value("app.version").(appVersion) 39 | render.JSON(w, r, data) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/backup/azure.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/codeskyblue/go-sh" 9 | "github.com/pkg/errors" 10 | 11 | "github.com/stefanprodan/mgob/pkg/config" 12 | ) 13 | 14 | func azureUpload(file string, plan config.Plan) (string, error) { 15 | azurefile := strings.TrimLeft(file, "!/") 16 | upload := fmt.Sprintf("az storage blob upload -c '%v' --file '%v' --name '%v' --connection-string '%v'", 17 | plan.Azure.ContainerName, file, azurefile, plan.Azure.ConnectionString) 18 | 19 | result, err := sh.Command("/bin/sh", "-c", upload).SetTimeout(time.Duration(plan.Scheduler.Timeout) * time.Minute).CombinedOutput() 20 | output := "" 21 | if len(result) > 0 { 22 | output = strings.Replace(string(result), "\n", " ", -1) 23 | } 24 | 25 | if err != nil { 26 | return "", errors.Wrapf(err, "Azure uploading %v to %v failed %v", file, plan.Azure.ContainerName, output) 27 | } 28 | 29 | if strings.Contains(output, "") { 30 | return "", errors.Errorf("Azure upload failed %v", output) 31 | } 32 | 33 | return strings.Replace(output, "\n", " ", -1), nil 34 | } 35 | -------------------------------------------------------------------------------- /pkg/backup/backup.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "time" 8 | 9 | "github.com/codeskyblue/go-sh" 10 | "github.com/pkg/errors" 11 | log "github.com/sirupsen/logrus" 12 | 13 | "github.com/stefanprodan/mgob/pkg/config" 14 | ) 15 | 16 | func Run(plan config.Plan, conf *config.AppConfig, modules *config.ModuleConfig) (Result, error) { 17 | tmpPath := conf.TmpPath 18 | storagePath := conf.StoragePath 19 | t1 := time.Now() 20 | planDir := fmt.Sprintf("%v/%v", storagePath, plan.Name) 21 | 22 | archive, mlog, err := dump(plan, tmpPath, t1.UTC()) 23 | log.WithFields(log.Fields{ 24 | "archive": archive, 25 | "mlog": mlog, 26 | "planDir": planDir, 27 | "err": err, 28 | }).Info("new dump") 29 | 30 | res := Result{ 31 | Plan: plan.Name, 32 | Timestamp: t1.UTC(), 33 | Status: 500, 34 | } 35 | _, res.Name = filepath.Split(archive) 36 | 37 | if err != nil { 38 | return res, err 39 | } 40 | 41 | err = sh.Command("mkdir", "-p", planDir).Run() 42 | if err != nil { 43 | return res, errors.Wrapf(err, "creating dir %v in %v failed", plan.Name, storagePath) 44 | } 45 | 46 | fi, err := os.Stat(archive) 47 | if err != nil { 48 | return res, errors.Wrapf(err, "stat file %v failed", archive) 49 | } 50 | res.Size = fi.Size() 51 | 52 | err = sh.Command("mv", archive, planDir).Run() 53 | if err != nil { 54 | return res, errors.Wrapf(err, "moving file from %v to %v failed", archive, planDir) 55 | } 56 | 57 | // check if log file exists, is not always created 58 | if _, err := os.Stat(mlog); os.IsNotExist(err) { 59 | log.Debug("appears no log file was generated") 60 | } else { 61 | err = sh.Command("mv", mlog, planDir).Run() 62 | if err != nil { 63 | return res, errors.Wrapf(err, "moving file from %v to %v failed", mlog, planDir) 64 | } 65 | } 66 | 67 | if plan.Scheduler.Retention > 0 { 68 | err = applyRetention(planDir, plan.Scheduler.Retention) 69 | if err != nil { 70 | return res, errors.Wrap(err, "retention job failed") 71 | } 72 | } 73 | 74 | file := filepath.Join(planDir, res.Name) 75 | 76 | if plan.Encryption != nil { 77 | encryptedFile := fmt.Sprintf("%v.encrypted", file) 78 | output, err := encrypt(file, encryptedFile, plan, conf) 79 | if err != nil { 80 | return res, err 81 | } else { 82 | removeUnencrypted(file, encryptedFile) 83 | file = encryptedFile 84 | log.WithField("plan", plan.Name).Infof("Encryption finished %v", output) 85 | } 86 | } 87 | 88 | if plan.SFTP != nil { 89 | sftpOutput, err := sftpUpload(file, plan) 90 | if err != nil { 91 | return res, err 92 | } else { 93 | log.WithField("plan", plan.Name).Info(sftpOutput) 94 | } 95 | } 96 | 97 | if plan.S3 != nil { 98 | s3Output, err := s3Upload(file, plan, conf.UseAwsCli) 99 | if err != nil { 100 | return res, err 101 | } else { 102 | log.WithField("plan", plan.Name).Infof("S3 upload finished %v", s3Output) 103 | } 104 | } 105 | 106 | if plan.GCloud != nil { 107 | gCloudOutput, err := gCloudUpload(file, plan) 108 | if err != nil { 109 | return res, err 110 | } else { 111 | log.WithField("plan", plan.Name).Infof("GCloud upload finished %v", gCloudOutput) 112 | } 113 | } 114 | 115 | if plan.Azure != nil { 116 | azureOutput, err := azureUpload(file, plan) 117 | if err != nil { 118 | return res, err 119 | } else { 120 | log.WithField("plan", plan.Name).Infof("Azure upload finished %v", azureOutput) 121 | } 122 | } 123 | 124 | if plan.Rclone != nil { 125 | rcloneOutput, err := rcloneUpload(file, plan) 126 | if err != nil { 127 | return res, err 128 | } else { 129 | log.WithField("plan", plan.Name).Infof("Rclone upload finished %v", rcloneOutput) 130 | } 131 | } 132 | 133 | t2 := time.Now() 134 | res.Status = 200 135 | res.Duration = t2.Sub(t1) 136 | return res, nil 137 | } 138 | -------------------------------------------------------------------------------- /pkg/backup/checks.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/codeskyblue/go-sh" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | func CheckMongodump() (string, error) { 11 | output, err := sh.Command("/bin/sh", "-c", "mongodump --version").CombinedOutput() 12 | if err != nil { 13 | ex := "" 14 | if len(output) > 0 { 15 | ex = strings.Replace(string(output), "\n", " ", -1) 16 | } 17 | return "", errors.Wrapf(err, "mongodump failed %v", ex) 18 | } 19 | 20 | return strings.Replace(string(output), "\n", " ", -1), nil 21 | } 22 | 23 | func CheckMinioClient() (string, error) { 24 | output, err := sh.Command("/bin/sh", "-c", "mc version").CombinedOutput() 25 | if err != nil { 26 | ex := "" 27 | if len(output) > 0 { 28 | ex = strings.Replace(string(output), "\n", " ", -1) 29 | } 30 | return "", errors.Wrapf(err, "mc failed %v", ex) 31 | } 32 | 33 | return strings.Replace(string(output), "\n", " ", -1), nil 34 | } 35 | 36 | func CheckAWSClient() (string, error) { 37 | output, err := sh.Command("/bin/sh", "-c", "aws --version").CombinedOutput() 38 | if err != nil { 39 | ex := "" 40 | if len(output) > 0 { 41 | ex = strings.Replace(string(output), "\n", " ", -1) 42 | } 43 | return "", errors.Wrapf(err, "aws failed %v", ex) 44 | } 45 | 46 | return strings.Replace(string(output), "\n", " ", -1), nil 47 | } 48 | 49 | func CheckGpg() (string, error) { 50 | output, err := sh.Command("/bin/sh", "-c", "gpg --version").CombinedOutput() 51 | if err != nil { 52 | ex := "" 53 | if len(output) > 0 { 54 | ex = strings.Replace(string(output), "\n", " ", -1) 55 | } 56 | return "", errors.Wrapf(err, "gpg failed %v", ex) 57 | } 58 | 59 | return strings.Replace(string(output), "\n", " ", -1), nil 60 | } 61 | 62 | func CheckGCloudClient() (string, error) { 63 | output, err := sh.Command("/bin/sh", "-c", "gcloud --version").CombinedOutput() 64 | if err != nil { 65 | ex := "" 66 | if len(output) > 0 { 67 | ex = strings.Replace(string(output), "\n", " ", -1) 68 | } 69 | return "", errors.Wrapf(err, "gcloud failed %v", ex) 70 | } 71 | 72 | return strings.Replace(string(output), "\n", " ", -1), nil 73 | } 74 | 75 | func CheckAzureClient() (string, error) { 76 | output, err := sh.Command("/bin/sh", "-c", "az --version | grep 'azure-cli'").CombinedOutput() 77 | if err != nil { 78 | ex := "" 79 | if len(output) > 0 { 80 | ex = strings.Replace(string(output), "\n", " ", -1) 81 | } 82 | return "", errors.Wrapf(err, "az failed %v", ex) 83 | } 84 | 85 | return strings.Replace(string(output), "\n", " ", -1), nil 86 | } 87 | 88 | func CheckRCloneClient() (string, error) { 89 | output, err := sh.Command("/bin/sh", "-c", "rclone version | grep 'rclone'").CombinedOutput() 90 | if err != nil { 91 | ex := "" 92 | if len(output) > 0 { 93 | ex = strings.Replace(string(output), "\n", " ", -1) 94 | } 95 | return "", errors.Wrapf(err, "rclone failed %v", ex) 96 | } 97 | 98 | return strings.Replace(string(output), "\n", " ", -1), nil 99 | } 100 | -------------------------------------------------------------------------------- /pkg/backup/encrypt.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "fmt" 5 | "github.com/codeskyblue/go-sh" 6 | "github.com/pkg/errors" 7 | log "github.com/sirupsen/logrus" 8 | "github.com/stefanprodan/mgob/pkg/config" 9 | "os" 10 | "regexp" 11 | "strings" 12 | ) 13 | 14 | func encrypt(file string, encryptedFile string, plan config.Plan, conf *config.AppConfig) (string, error) { 15 | if plan.Encryption.Gpg != nil { 16 | if !conf.HasGpg { 17 | return "", errors.Errorf("GPG configuration is present, but no GPG binary is found! Uploading unencrypted backup.") 18 | } 19 | return gpgEncrypt(file, encryptedFile, plan) 20 | } 21 | 22 | return "", errors.Errorf("Encryption config is not valid!") 23 | } 24 | 25 | func removeUnencrypted(file string, encryptedFile string) { 26 | // Check if encrypted file exists and remove original 27 | stat, err := os.Stat(encryptedFile) 28 | if err == nil && stat.Size() > 0 { 29 | os.Remove(file) 30 | } 31 | } 32 | 33 | func gpgEncrypt(file string, encryptedFile string, plan config.Plan) (string, error) { 34 | output := "" 35 | recipient := "" 36 | 37 | recipients := plan.Encryption.Gpg.Recipients 38 | 39 | keyFile := plan.Encryption.Gpg.KeyFile 40 | if keyFile != "" { 41 | keyFileStat, err := os.Stat(keyFile) 42 | if err == nil && !keyFileStat.IsDir() { 43 | // import key from file 44 | importCmd := fmt.Sprintf("gpg --batch --import %v", keyFile) 45 | 46 | result, err := sh.Command("/bin/sh", "-c", importCmd).CombinedOutput() 47 | if len(result) > 0 { 48 | output += strings.Replace(string(result), "\n", " ", -1) 49 | } 50 | if err != nil { 51 | return "", errors.Wrapf(err, "Importing encryption key for plan %v failed %s", plan.Name, output) 52 | } 53 | if !strings.Contains(output, "imported: 1") && !strings.Contains(output, "unchanged: 1") { 54 | return "", errors.Errorf("Importing encryption key failed %v", output) 55 | } 56 | 57 | re := regexp.MustCompile(`key ([0-9A-F]+):`) 58 | keyMatch := re.FindStringSubmatch(output) 59 | log.WithField("plan", plan.Name).Debugf("Import output: %v", output) 60 | log.WithField("plan", plan.Name).Debugf("Parsed key id: %v", keyMatch[1]) 61 | if keyMatch != nil { 62 | recipients = append(recipients, keyMatch[1]) 63 | } 64 | } 65 | } 66 | 67 | recipient = strings.Join(recipients, " -r ") 68 | 69 | if recipient == "" { 70 | return "", errors.Errorf("GPG configuration is present, but no encryption key is configured! %v", output) 71 | } 72 | 73 | keyServer := plan.Encryption.Gpg.KeyServer 74 | if keyServer == "" { 75 | keyServer = "hkps://keys.openpgp.org" 76 | } 77 | 78 | // encrypt file 79 | encryptCmd := fmt.Sprintf( 80 | "gpg -v --batch --yes --trust-model always --auto-key-locate local,%v -e -r %v -o %v %v", 81 | keyServer, recipient, encryptedFile, file) 82 | 83 | result, err := sh.Command("/bin/sh", "-c", encryptCmd).CombinedOutput() 84 | if len(result) > 0 { 85 | output += strings.Replace(string(result), "\n", " ", -1) 86 | } 87 | if err != nil { 88 | return "", errors.Wrapf(err, "Encryption for plan %v failed %s", plan.Name, output) 89 | } 90 | 91 | return output, nil 92 | } 93 | -------------------------------------------------------------------------------- /pkg/backup/gcloud.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/codeskyblue/go-sh" 9 | "github.com/pkg/errors" 10 | 11 | "github.com/stefanprodan/mgob/pkg/config" 12 | ) 13 | 14 | func gCloudUpload(file string, plan config.Plan) (string, error) { 15 | 16 | register := fmt.Sprintf("gcloud auth activate-service-account --key-file=%v", 17 | plan.GCloud.KeyFilePath) 18 | 19 | _, err := sh.Command("/bin/sh", "-c", register).CombinedOutput() 20 | if err != nil { 21 | return "", errors.Wrapf(err, "gcloud auth for plan %v failed", plan.Name) 22 | } 23 | 24 | upload := fmt.Sprintf("gsutil cp %v gs://%v", 25 | file, plan.GCloud.Bucket) 26 | 27 | result, err := sh.Command("/bin/sh", "-c", upload).SetTimeout(time.Duration(plan.Scheduler.Timeout) * time.Minute).CombinedOutput() 28 | output := "" 29 | if len(result) > 0 { 30 | output = strings.Replace(string(result), "\n", " ", -1) 31 | } 32 | 33 | if err != nil { 34 | return "", errors.Wrapf(err, "GCloud uploading %v to gs://%v failed %v", file, plan.GCloud.Bucket, output) 35 | } 36 | 37 | if strings.Contains(output, "") { 38 | return "", errors.Errorf("GCloud upload failed %v", output) 39 | } 40 | 41 | return strings.Replace(output, "\n", " ", -1), nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/backup/local.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "io/ioutil" 7 | "strings" 8 | "time" 9 | 10 | "github.com/codeskyblue/go-sh" 11 | "github.com/pkg/errors" 12 | log "github.com/sirupsen/logrus" 13 | 14 | "github.com/stefanprodan/mgob/pkg/config" 15 | ) 16 | 17 | func dump(plan config.Plan, tmpPath string, ts time.Time) (string, string, error) { 18 | archive := fmt.Sprintf("%v/%v-%v.gz", tmpPath, plan.Name, ts.Unix()) 19 | mlog := fmt.Sprintf("%v/%v-%v.log", tmpPath, plan.Name, ts.Unix()) 20 | dump := fmt.Sprintf("mongodump --archive=%v --gzip ", archive) 21 | 22 | if plan.Target.Uri != "" { 23 | // using uri (New in version 3.4.6) 24 | // host/port/username/password are incompatible with uri 25 | // https://docs.mongodb.com/manual/reference/program/mongodump/#cmdoption-mongodump-uri 26 | dump += fmt.Sprintf(`--uri "%v" `, plan.Target.Uri) 27 | } else { 28 | // use older host/port 29 | dump += fmt.Sprintf("--host %v --port %v ", plan.Target.Host, plan.Target.Port) 30 | 31 | if plan.Target.Username != "" && plan.Target.Password != "" { 32 | dump += fmt.Sprintf(`-u "%v" -p "%v" `, plan.Target.Username, plan.Target.Password) 33 | } 34 | } 35 | 36 | if plan.Target.Database != "" { 37 | dump += fmt.Sprintf("--db %v ", plan.Target.Database) 38 | } 39 | 40 | if plan.Target.Params != "" { 41 | dump += fmt.Sprintf("%v", plan.Target.Params) 42 | } 43 | 44 | // TODO: mask password 45 | log.Debugf("dump cmd: %v", dump) 46 | output, err := sh.Command("/bin/sh", "-c", dump).SetTimeout(time.Duration(plan.Scheduler.Timeout) * time.Minute).CombinedOutput() 47 | if err != nil { 48 | ex := "" 49 | if len(output) > 0 { 50 | ex = strings.Replace(string(output), "\n", " ", -1) 51 | } 52 | // Try and clean up tmp file after an error 53 | os.Remove(archive) 54 | return "", "", errors.Wrapf(err, "mongodump log %v", ex) 55 | } 56 | logToFile(mlog, output) 57 | 58 | return archive, mlog, nil 59 | } 60 | 61 | func logToFile(file string, data []byte) error { 62 | if len(data) > 0 { 63 | err := ioutil.WriteFile(file, data, 0644) 64 | if err != nil { 65 | return errors.Wrapf(err, "writing log %v failed", file) 66 | } 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func applyRetention(path string, retention int) error { 73 | gz := fmt.Sprintf("cd %v && rm -f $(ls -1t *.gz *.gz.encrypted | tail -n +%v)", path, retention+1) 74 | err := sh.Command("/bin/sh", "-c", gz).Run() 75 | if err != nil { 76 | return errors.Wrapf(err, "removing old gz files from %v failed", path) 77 | } 78 | 79 | log.Debug("apply retention") 80 | log := fmt.Sprintf("cd %v && rm -f $(ls -1t *.log | tail -n +%v)", path, retention+1) 81 | err = sh.Command("/bin/sh", "-c", log).Run() 82 | if err != nil { 83 | return errors.Wrapf(err, "removing old log files from %v failed", path) 84 | } 85 | 86 | return nil 87 | } 88 | 89 | // TmpCleanup remove files older than one day 90 | func TmpCleanup(path string) error { 91 | rm := fmt.Sprintf("find %v -not -name \"mgob.db\" -mtime +%v -type f -delete", path, 1) 92 | err := sh.Command("/bin/sh", "-c", rm).Run() 93 | if err != nil { 94 | return errors.Wrapf(err, "%v cleanup failed", path) 95 | } 96 | 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /pkg/backup/rclone.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | "time" 8 | 9 | "github.com/codeskyblue/go-sh" 10 | "github.com/pkg/errors" 11 | 12 | "github.com/stefanprodan/mgob/pkg/config" 13 | ) 14 | 15 | func rcloneUpload(file string, plan config.Plan) (string, error) { 16 | 17 | fileName := filepath.Base(file) 18 | 19 | configSection := plan.Rclone.ConfigSection 20 | if "" == configSection { 21 | configSection = plan.Name 22 | } 23 | 24 | upload := fmt.Sprintf("rclone --config=\"%v\" copy %v %v:%v/%v", 25 | plan.Rclone.ConfigFilePath, file, configSection, plan.Rclone.Bucket, fileName) 26 | 27 | result, err := sh.Command("/bin/sh", "-c", upload).SetTimeout(time.Duration(plan.Scheduler.Timeout) * time.Minute).CombinedOutput() 28 | output := "" 29 | if len(result) > 0 { 30 | output = strings.Replace(string(result), "\n", " ", -1) 31 | } 32 | 33 | if err != nil { 34 | return "", errors.Wrapf(err, "Rclone uploading %v to %v:%v failed %v", file, plan.Name, plan.Rclone.Bucket, output) 35 | } 36 | 37 | return strings.Replace(output, "\n", " ", -1), nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/backup/result.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import "time" 4 | 5 | type Result struct { 6 | Name string `json:"name"` 7 | Plan string `json:"plan"` 8 | Duration time.Duration `json:"duration"` 9 | Size int64 `json:"size"` 10 | Status int `json:"status"` 11 | Timestamp time.Time `json:"timestamp"` 12 | } 13 | -------------------------------------------------------------------------------- /pkg/backup/s3.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | "time" 8 | "net/url" 9 | 10 | "github.com/codeskyblue/go-sh" 11 | "github.com/pkg/errors" 12 | 13 | "github.com/stefanprodan/mgob/pkg/config" 14 | ) 15 | 16 | func s3Upload(file string, plan config.Plan, useAwsCli bool) (string, error) { 17 | 18 | s3Url, err := url.Parse(plan.S3.URL) 19 | 20 | if err != nil { 21 | return "", errors.Wrapf(err, "invalid S3 url for plan %v: %s", plan.Name, plan.S3.URL) 22 | } 23 | 24 | if useAwsCli && strings.HasSuffix(s3Url.Hostname(), "amazonaws.com") { 25 | return awsUpload(file, plan) 26 | } 27 | 28 | return minioUpload(file, plan) 29 | } 30 | 31 | func awsUpload(file string, plan config.Plan) (string, error) { 32 | 33 | output := "" 34 | if len(plan.S3.AccessKey) > 0 && len(plan.S3.SecretKey) > 0 { 35 | // Let's use credentials given 36 | configure := fmt.Sprintf("aws configure set aws_access_key_id %v && aws configure set aws_secret_access_key %v", 37 | plan.S3.AccessKey, plan.S3.SecretKey) 38 | 39 | result, err := sh.Command("/bin/sh", "-c", configure).CombinedOutput() 40 | if len(result) > 0 { 41 | output += strings.Replace(string(result), "\n", " ", -1) 42 | } 43 | if err != nil { 44 | return "", errors.Wrapf(err, "aws configure for plan %v failed %s", plan.Name, output) 45 | } 46 | } 47 | 48 | fileName := filepath.Base(file) 49 | 50 | encrypt := "" 51 | if len(plan.S3.KmsKeyId) > 0 { 52 | encrypt = fmt.Sprintf(" --sse aws:kms --sse-kms-key-id %v", plan.S3.KmsKeyId) 53 | } 54 | 55 | storage := "" 56 | if len(plan.S3.StorageClass) > 0 { 57 | storage = fmt.Sprintf(" --storage-class %v", plan.S3.StorageClass) 58 | } 59 | 60 | upload := fmt.Sprintf("aws --quiet s3 cp %v s3://%v/%v%v%v", 61 | file, plan.S3.Bucket, fileName, encrypt, storage) 62 | 63 | result, err := sh.Command("/bin/sh", "-c", upload).SetTimeout(time.Duration(plan.Scheduler.Timeout) * time.Minute).CombinedOutput() 64 | if len(result) > 0 { 65 | output += strings.Replace(string(result), "\n", " ", -1) 66 | } 67 | if err != nil { 68 | return "", errors.Wrapf(err, "S3 uploading %v to %v/%v failed %v", file, plan.Name, plan.S3.Bucket, output) 69 | } 70 | 71 | if strings.Contains(output, "") { 72 | return "", errors.Errorf("S3 upload failed %v", output) 73 | } 74 | 75 | return strings.Replace(output, "\n", " ", -1), nil 76 | } 77 | 78 | func minioUpload(file string, plan config.Plan) (string, error) { 79 | 80 | register := fmt.Sprintf("mc config host add %v %v %v %v --api %v", 81 | plan.Name, plan.S3.URL, plan.S3.AccessKey, plan.S3.SecretKey, plan.S3.API) 82 | 83 | result, err := sh.Command("/bin/sh", "-c", register).CombinedOutput() 84 | output := "" 85 | if len(result) > 0 { 86 | output = strings.Replace(string(result), "\n", " ", -1) 87 | } 88 | if err != nil { 89 | return "", errors.Wrapf(err, "mc config host for plan %v failed %s", plan.Name, output) 90 | } 91 | 92 | fileName := filepath.Base(file) 93 | 94 | upload := fmt.Sprintf("mc --quiet cp %v %v/%v/%v", 95 | file, plan.Name, plan.S3.Bucket, fileName) 96 | 97 | result, err = sh.Command("/bin/sh", "-c", upload).SetTimeout(time.Duration(plan.Scheduler.Timeout) * time.Minute).CombinedOutput() 98 | output = "" 99 | if len(result) > 0 { 100 | output = strings.Replace(string(result), "\n", " ", -1) 101 | } 102 | 103 | if err != nil { 104 | return "", errors.Wrapf(err, "S3 uploading %v to %v/%v failed %v", file, plan.Name, plan.S3.Bucket, output) 105 | } 106 | 107 | if strings.Contains(output, "") { 108 | return "", errors.Errorf("S3 upload failed %v", output) 109 | } 110 | 111 | return strings.Replace(output, "\n", " ", -1), nil 112 | } 113 | -------------------------------------------------------------------------------- /pkg/backup/sftp.go: -------------------------------------------------------------------------------- 1 | package backup 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "io/ioutil" 7 | "net" 8 | "os" 9 | "path/filepath" 10 | "time" 11 | 12 | "github.com/pkg/errors" 13 | "github.com/pkg/sftp" 14 | "golang.org/x/crypto/ssh" 15 | 16 | "github.com/stefanprodan/mgob/pkg/config" 17 | ) 18 | 19 | func sftpUpload(file string, plan config.Plan) (string, error) { 20 | t1 := time.Now() 21 | var ams []ssh.AuthMethod 22 | if plan.SFTP.Password != "" { 23 | ams = append(ams, ssh.Password(plan.SFTP.Password)) 24 | } 25 | 26 | if plan.SFTP.PrivateKey != "" { 27 | key, err := ioutil.ReadFile(plan.SFTP.PrivateKey) 28 | if err != nil { 29 | return "", errors.Wrapf(err, "Reading private_key from file %s", plan.SFTP.PrivateKey) 30 | } 31 | 32 | var signer ssh.Signer 33 | switch { 34 | case plan.SFTP.PrivateKey != "" && plan.SFTP.Passphrase != "": 35 | signer, err = ssh.ParsePrivateKeyWithPassphrase(key, []byte(plan.SFTP.Passphrase)) 36 | if err != nil { 37 | return "", errors.Wrapf(err, "Parsing private key from file %s", plan.SFTP.PrivateKey) 38 | } 39 | case plan.SFTP.PrivateKey != "": 40 | signer, err = ssh.ParsePrivateKey(key) 41 | if err != nil { 42 | return "", errors.Wrapf(err, "Parsing private key from file %s", plan.SFTP.PrivateKey) 43 | } 44 | } 45 | ams = append(ams, ssh.PublicKeys(signer)) 46 | } 47 | 48 | sshConf := &ssh.ClientConfig{ 49 | User: plan.SFTP.Username, 50 | Auth: ams, 51 | HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error { 52 | return nil 53 | }, 54 | } 55 | 56 | sshCon, err := ssh.Dial("tcp", fmt.Sprintf("%v:%v", plan.SFTP.Host, plan.SFTP.Port), sshConf) 57 | if err != nil { 58 | return "", errors.Wrapf(err, "SSH dial to %v:%v failed", plan.SFTP.Host, plan.SFTP.Port) 59 | } 60 | defer sshCon.Close() 61 | 62 | sftpClient, err := sftp.NewClient(sshCon) 63 | if err != nil { 64 | return "", errors.Wrapf(err, "SFTP client init %v:%v failed", plan.SFTP.Host, plan.SFTP.Port) 65 | } 66 | defer sftpClient.Close() 67 | 68 | f, err := os.Open(file) 69 | if err != nil { 70 | return "", errors.Wrapf(err, "Opening file %v failed", file) 71 | } 72 | defer f.Close() 73 | 74 | _, fname := filepath.Split(file) 75 | dstPath := filepath.Join(plan.SFTP.Dir, fname) 76 | sf, err := sftpClient.Create(dstPath) 77 | if err != nil { 78 | return "", errors.Wrapf(err, "SFTP %v:%v creating file %v failed", plan.SFTP.Host, plan.SFTP.Port, dstPath) 79 | } 80 | 81 | _, err = io.Copy(sf, f) 82 | if err != nil { 83 | return "", errors.Wrapf(err, "SFTP %v:%v upload file %v failed", plan.SFTP.Host, plan.SFTP.Port, dstPath) 84 | } 85 | sf.Close() 86 | 87 | //listSftpBackups(sftpClient, plan.SFTP.Dir) 88 | 89 | t2 := time.Now() 90 | msg := fmt.Sprintf("SFTP upload finished `%v` -> `%v` Duration: %v", 91 | file, dstPath, t2.Sub(t1)) 92 | return msg, nil 93 | } 94 | 95 | func listSftpBackups(client *sftp.Client, dir string) error { 96 | list, err := client.ReadDir(fmt.Sprintf("/%v", dir)) 97 | if err != nil { 98 | return errors.Wrapf(err, "SFTP reading %v dir failed", dir) 99 | } 100 | 101 | for _, item := range list { 102 | fmt.Println(item.Name()) 103 | } 104 | 105 | return nil 106 | } 107 | -------------------------------------------------------------------------------- /pkg/config/app.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type AppConfig struct { 4 | LogLevel string `json:"log_level"` 5 | JSONLog bool `json:"json_log"` 6 | Host string `json:"host"` 7 | Port int `json:"port"` 8 | ConfigPath string `json:"config_path"` 9 | StoragePath string `json:"storage_path"` 10 | TmpPath string `json:"tmp_path"` 11 | DataPath string `json:"data_path"` 12 | Version string `json:"version"` 13 | UseAwsCli bool `json:"use_aws_cli"` 14 | HasGpg bool `json:"has_gpg"` 15 | } 16 | -------------------------------------------------------------------------------- /pkg/config/modules.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type ModuleConfig struct { 4 | AWSClient bool `envconfig:"en_aws_cli" default:"false"` 5 | AzureClient bool `envconfig:"en_azure" default:"false"` 6 | GCloudClient bool `envconfig:"en_gcloud" default:"false"` 7 | GnuPG bool `envconfig:"en_gpg" default:"true"` 8 | MinioClient bool `envconfig:"en_minio" default:"false"` 9 | RCloneClient bool `envconfig:"en_rclone" default:"false"` 10 | } 11 | -------------------------------------------------------------------------------- /pkg/config/plan.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/pkg/errors" 10 | yaml "gopkg.in/yaml.v2" 11 | ) 12 | 13 | type Plan struct { 14 | Name string `yaml:"name"` 15 | Target Target `yaml:"target"` 16 | Scheduler Scheduler `yaml:"scheduler"` 17 | Encryption *Encryption `yaml:"encryption"` 18 | S3 *S3 `yaml:"s3"` 19 | GCloud *GCloud `yaml:"gcloud"` 20 | Rclone *Rclone `yaml:"rclone"` 21 | Azure *Azure `yaml:"azure"` 22 | SFTP *SFTP `yaml:"sftp"` 23 | SMTP *SMTP `yaml:"smtp"` 24 | Slack *Slack `yaml:"slack"` 25 | } 26 | 27 | type Target struct { 28 | Database string `yaml:"database"` 29 | Host string `yaml:"host"` 30 | Uri string `yaml:"uri"` 31 | Password string `yaml:"password"` 32 | Port int `yaml:"port"` 33 | Username string `yaml:"username"` 34 | Params string `yaml:"params"` 35 | } 36 | 37 | type Scheduler struct { 38 | Cron string `yaml:"cron"` 39 | Retention int `yaml:"retention"` 40 | Timeout int `yaml:"timeout"` 41 | } 42 | 43 | type Encryption struct { 44 | Gpg *Gpg `yaml:"gpg"` 45 | } 46 | 47 | type Gpg struct { 48 | KeyServer string `yaml:"keyServer"` 49 | Recipients []string `yaml:"recipients"` 50 | KeyFile string `yaml:"keyFile"` 51 | } 52 | 53 | type S3 struct { 54 | Bucket string `yaml:"bucket"` 55 | AccessKey string `yaml:"accessKey"` 56 | API string `yaml:"api"` 57 | SecretKey string `yaml:"secretKey"` 58 | URL string `yaml:"url"` 59 | KmsKeyId string `yaml:"kmsKeyId"` 60 | StorageClass string `yaml:"storageClass" validate:"omitempty,oneof=STANDARD REDUCED_REDUNDANCY STANDARD_IA ONE-ZONE_IA INTELLIGENT_TIERING GLACIER DEEP_ARCHIVE` 61 | } 62 | 63 | type GCloud struct { 64 | Bucket string `yaml:"bucket"` 65 | KeyFilePath string `yaml:"keyFilePath"` 66 | } 67 | 68 | type Rclone struct { 69 | Bucket string `yaml:"bucket"` 70 | ConfigFilePath string `yaml:"configFilePath"` 71 | ConfigSection string `yaml:"configSection"` 72 | } 73 | 74 | type Azure struct { 75 | ContainerName string `yaml:"containerName"` 76 | ConnectionString string `yaml:"connectionString"` 77 | } 78 | 79 | type SFTP struct { 80 | Dir string `yaml:"dir"` 81 | Host string `yaml:"host"` 82 | Password string `yaml:"password"` 83 | PrivateKey string `yaml:"private_key"` 84 | Passphrase string `yaml:"passphrase"` 85 | Port int `yaml:"port"` 86 | Username string `yaml:"username"` 87 | } 88 | 89 | type SMTP struct { 90 | Server string `yaml:"server"` 91 | Port string `yaml:"port"` 92 | Password string `yaml:"password"` 93 | Username string `yaml:"username"` 94 | From string `yaml:"from"` 95 | To []string `yaml:"to"` 96 | } 97 | 98 | type Slack struct { 99 | URL string `yaml:"url"` 100 | Channel string `yaml:"channel"` 101 | Username string `yaml:"username"` 102 | WarnOnly bool `yaml:"warnOnly"` 103 | } 104 | 105 | func LoadPlan(dir string, name string) (Plan, error) { 106 | plan := Plan{} 107 | planPath := "" 108 | err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error { 109 | if strings.Contains(path, name+".yml") || strings.Contains(path, name+".yaml") { 110 | planPath = path 111 | } 112 | return nil 113 | }) 114 | 115 | if err != nil { 116 | return plan, errors.Wrapf(err, "Reading from %v failed", dir) 117 | } 118 | 119 | if len(planPath) < 1 { 120 | return plan, errors.Errorf("Plan %v not found", name) 121 | } 122 | 123 | data, err := ioutil.ReadFile(planPath) 124 | if err != nil { 125 | return plan, errors.Wrapf(err, "Reading %v failed", planPath) 126 | } 127 | 128 | if err := yaml.Unmarshal(data, &plan); err != nil { 129 | return plan, errors.Wrapf(err, "Parsing %v failed", planPath) 130 | } 131 | _, filename := filepath.Split(planPath) 132 | plan.Name = strings.TrimSuffix(filename, filepath.Ext(filename)) 133 | 134 | return plan, nil 135 | } 136 | 137 | func LoadPlans(dir string) ([]Plan, error) { 138 | files := []string{} 139 | err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error { 140 | if strings.Contains(path, "yml") || strings.Contains(path, "yaml") { 141 | files = append(files, path) 142 | } 143 | return nil 144 | }) 145 | 146 | if err != nil { 147 | return nil, errors.Wrapf(err, "Reading from %v failed", dir) 148 | } 149 | 150 | plans := make([]Plan, 0) 151 | 152 | for _, path := range files { 153 | var plan Plan 154 | data, err := ioutil.ReadFile(path) 155 | if err != nil { 156 | return nil, errors.Wrapf(err, "Reading %v failed", path) 157 | } 158 | 159 | if err := yaml.Unmarshal(data, &plan); err != nil { 160 | return nil, errors.Wrapf(err, "Parsing %v failed", path) 161 | } 162 | _, filename := filepath.Split(path) 163 | plan.Name = strings.TrimSuffix(filename, filepath.Ext(filename)) 164 | 165 | duplicate := false 166 | for _, p := range plans { 167 | if p.Name == plan.Name { 168 | duplicate = true 169 | break 170 | } 171 | } 172 | if duplicate { 173 | continue 174 | } 175 | 176 | plans = append(plans, plan) 177 | 178 | } 179 | if len(plans) < 1 { 180 | return nil, errors.Errorf("No backup plans found in %v", dir) 181 | } 182 | 183 | return plans, nil 184 | } 185 | -------------------------------------------------------------------------------- /pkg/db/stats.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/boltdb/bolt" 8 | "github.com/pkg/errors" 9 | log "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type Status struct { 13 | Plan string `json:"plan"` 14 | NextRun time.Time `json:"next_run"` 15 | LastRun *time.Time `json:"last_run,omitempty"` 16 | LastRunStatus string `json:"last_run_status,omitempty"` 17 | LastRunLog string `json:"last_run_log,omitempty"` 18 | } 19 | 20 | type StatusStore struct { 21 | *Store 22 | bucket []byte 23 | } 24 | 25 | // NewStatusStore creates bucket if not found 26 | func NewStatusStore(store *Store) (*StatusStore, error) { 27 | bucket := []byte("scheduler_status") 28 | 29 | err := store.NewBucket(bucket) 30 | if err != nil { 31 | return nil, errors.Wrap(err, "Status store bucket init failed") 32 | } 33 | 34 | return &StatusStore{store, bucket}, nil 35 | } 36 | 37 | // Put upserts job status 38 | func (db *StatusStore) Put(status *Status) error { 39 | 40 | buf, err := json.Marshal(status) 41 | if err != nil { 42 | return errors.Wrap(err, "Status store json marshal failed") 43 | } 44 | 45 | return db.Update(func(tx *bolt.Tx) error { 46 | b := tx.Bucket(db.bucket) 47 | err = b.Put([]byte(status.Plan), buf) 48 | return err 49 | }) 50 | } 51 | 52 | // Sync plans found on disk with db 53 | func (db *StatusStore) Sync(stats []*Status) error { 54 | return db.Update(func(tx *bolt.Tx) error { 55 | b := tx.Bucket(db.bucket) 56 | 57 | dbStats := make([]*Status, 0) 58 | 59 | // get all jobs from db 60 | err := b.ForEach(func(k, v []byte) error { 61 | var status Status 62 | err := json.Unmarshal(v, &status) 63 | if err != nil { 64 | return errors.Wrap(err, "Status store json unmarshal failed") 65 | } 66 | dbStats = append(dbStats, &status) 67 | return nil 68 | }) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | for _, newS := range stats { 74 | found := false 75 | for _, oldS := range dbStats { 76 | // update next run for existing job 77 | if newS.Plan == oldS.Plan { 78 | oldS.NextRun = newS.NextRun 79 | buf, err := json.Marshal(oldS) 80 | if err != nil { 81 | return errors.Wrapf(err, "Json marshal for %v failed", oldS.Plan) 82 | } 83 | err = b.Put([]byte(oldS.Plan), buf) 84 | if err != nil { 85 | return errors.Wrapf(err, "Updating %v to store failed", oldS.Plan) 86 | } 87 | log.WithField("plan", oldS.Plan).Infof("Next run at %v", oldS.NextRun) 88 | found = true 89 | } 90 | } 91 | 92 | // insert new job 93 | if !found { 94 | log.WithField("plan", newS.Plan).Info("New job found, saving to store") 95 | buf, err := json.Marshal(newS) 96 | if err != nil { 97 | return errors.Wrapf(err, "Json marshal for %v failed", newS.Plan) 98 | } 99 | err = b.Put([]byte(newS.Plan), buf) 100 | if err != nil { 101 | return errors.Wrapf(err, "Saving %v to store failed", newS.Plan) 102 | } 103 | log.WithField("plan", newS.Plan).Infof("Next run at %v", newS.NextRun) 104 | } 105 | } 106 | 107 | // remove jobs not found on disk 108 | for _, oldS := range dbStats { 109 | found := false 110 | for _, newS := range stats { 111 | if oldS.Plan == newS.Plan { 112 | found = true 113 | } 114 | } 115 | 116 | if !found { 117 | log.WithField("plan", oldS.Plan).Info("Plan not found on disk, removing from store") 118 | err = b.Delete([]byte(oldS.Plan)) 119 | if err != nil { 120 | return errors.Wrapf(err, "Removing %v from store failed", oldS.Plan) 121 | } 122 | } 123 | } 124 | 125 | return nil 126 | 127 | }) 128 | } 129 | 130 | // GetAll loads all jobs stats from db 131 | func (db *StatusStore) GetAll() ([]*Status, error) { 132 | stats := make([]*Status, 0) 133 | 134 | err := db.View(func(tx *bolt.Tx) error { 135 | b := tx.Bucket([]byte(db.bucket)) 136 | 137 | return b.ForEach(func(k, v []byte) error { 138 | var status Status 139 | err := json.Unmarshal(v, &status) 140 | if err != nil { 141 | return errors.Wrap(err, "Status store json unmarshal failed") 142 | } 143 | stats = append(stats, &status) 144 | return nil 145 | }) 146 | }) 147 | 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | return stats, nil 153 | } 154 | -------------------------------------------------------------------------------- /pkg/db/store.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/boltdb/bolt" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type Store struct { 11 | *bolt.DB 12 | } 13 | 14 | // Open creates or opens a bolt db at the specified path. 15 | func Open(path string) (*Store, error) { 16 | config := &bolt.Options{Timeout: 1 * time.Second} 17 | d, err := bolt.Open(path, 0600, config) 18 | if err != nil { 19 | return nil, errors.Wrapf(err, "Opening store %s failed", path) 20 | } 21 | 22 | return &Store{d}, nil 23 | } 24 | 25 | func (db *Store) NewBucket(name []byte) error { 26 | return db.Update(func(tx *bolt.Tx) error { 27 | _, err := tx.CreateBucketIfNotExists(name) 28 | if err != nil { 29 | return err 30 | } 31 | return nil 32 | }) 33 | } 34 | 35 | func (db *Store) DeleteBucket(name []byte) error { 36 | return db.Update(func(tx *bolt.Tx) error { 37 | return tx.DeleteBucket(name) 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import "github.com/prometheus/client_golang/prometheus" 4 | 5 | type BackupMetrics struct { 6 | Total *prometheus.CounterVec 7 | Size *prometheus.GaugeVec 8 | Latency *prometheus.SummaryVec 9 | } 10 | 11 | func New(namespace string, subsystem string) *BackupMetrics { 12 | prom := &BackupMetrics{} 13 | 14 | prom.Total = prometheus.NewCounterVec( 15 | prometheus.CounterOpts{ 16 | Namespace: namespace, 17 | Subsystem: subsystem, 18 | Name: "backup_total", 19 | Help: "The total number of backups.", 20 | }, 21 | []string{"plan", "status"}, 22 | ) 23 | 24 | prom.Size = prometheus.NewGaugeVec( 25 | prometheus.GaugeOpts{ 26 | Namespace: namespace, 27 | Subsystem: subsystem, 28 | Name: "backup_size", 29 | Help: "The size of backup.", 30 | }, 31 | []string{"plan", "status"}, 32 | ) 33 | 34 | prom.Latency = prometheus.NewSummaryVec( 35 | prometheus.SummaryOpts{ 36 | Namespace: namespace, 37 | Subsystem: subsystem, 38 | Name: "backup_latency", 39 | Help: "Backup duration in seconds.", 40 | }, 41 | []string{"plan", "status"}, 42 | ) 43 | 44 | prometheus.MustRegister(prom.Total) 45 | prometheus.MustRegister(prom.Size) 46 | prometheus.MustRegister(prom.Latency) 47 | 48 | return prom 49 | } 50 | -------------------------------------------------------------------------------- /pkg/notifier/notifier.go: -------------------------------------------------------------------------------- 1 | package notifier 2 | 3 | import "github.com/stefanprodan/mgob/pkg/config" 4 | 5 | func SendNotification(subject string, body string, warn bool, plan config.Plan) error { 6 | 7 | var err error 8 | if plan.SMTP != nil { 9 | err = sendEmailNotification(subject, body, plan.SMTP) 10 | } 11 | if plan.Slack != nil { 12 | err = sendSlackNotification(subject, body, warn, plan.Slack) 13 | } 14 | return err 15 | } 16 | -------------------------------------------------------------------------------- /pkg/notifier/slack.go: -------------------------------------------------------------------------------- 1 | package notifier 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | 10 | "github.com/pkg/errors" 11 | 12 | "github.com/stefanprodan/mgob/pkg/config" 13 | ) 14 | 15 | type slackPayload struct { 16 | Channel string `json:"channel"` 17 | Username string `json:"username"` 18 | IconUrl string `json:"icon_url"` 19 | IconEmoji string `json:"icon_emoji"` 20 | Text string `json:"text,omitempty"` 21 | Attachments []slackAttachment `json:"attachments,omitempty"` 22 | } 23 | 24 | type slackAttachment struct { 25 | Color string `json:"color"` 26 | Title string `json:"title"` 27 | Pretext string `json:"pretext"` 28 | Text string `json:"text"` 29 | MrkdwnIn []string `json:"mrkdwn_in"` 30 | } 31 | 32 | func sendSlackNotification(subject string, body string, warn bool, cfg *config.Slack) error { 33 | if !warn && cfg.WarnOnly { 34 | return nil 35 | } 36 | 37 | payload := slackPayload{ 38 | Channel: cfg.Channel, 39 | Username: cfg.Username, 40 | } 41 | 42 | var emoji, color string 43 | if warn { 44 | emoji = ":x:" 45 | color = "danger" 46 | } else { 47 | emoji = ":white_check_mark:" 48 | color = "good" 49 | } 50 | 51 | title := "backup log" 52 | pretext := fmt.Sprintf("%s *%s*", emoji, subject) 53 | 54 | a := slackAttachment{ 55 | Color: color, 56 | Title: title, 57 | Pretext: pretext, 58 | Text: body, 59 | MrkdwnIn: []string{"text", "pretext"}, 60 | } 61 | 62 | payload.Attachments = []slackAttachment{a} 63 | 64 | data, err := json.Marshal(payload) 65 | if err != nil { 66 | return errors.Wrapf(err, "Marshalling slack payload failed") 67 | } 68 | 69 | b := bytes.NewBuffer(data) 70 | 71 | if res, err := http.Post(cfg.URL, "application/json", b); err != nil { 72 | return errors.Wrapf(err, "Sending data to slack failed") 73 | } else { 74 | defer res.Body.Close() 75 | statusCode := res.StatusCode 76 | if statusCode != 200 { 77 | body, _ := ioutil.ReadAll(res.Body) 78 | return errors.Errorf("Sending data to slack failed %v", string(body)) 79 | } 80 | } 81 | 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /pkg/notifier/smtp.go: -------------------------------------------------------------------------------- 1 | package notifier 2 | 3 | import ( 4 | "fmt" 5 | "net/smtp" 6 | "strings" 7 | 8 | "github.com/pkg/errors" 9 | 10 | "github.com/stefanprodan/mgob/pkg/config" 11 | ) 12 | 13 | func sendEmailNotification(subject string, body string, config *config.SMTP) error { 14 | 15 | msg := "From: \"MGOB\" <" + config.From + ">\r\n" + 16 | "To: " + strings.Join(config.To, ", ") + "\r\n" + 17 | "Subject: " + subject + "\r\n\r\n" + 18 | body + "\r\n" 19 | 20 | addr := fmt.Sprintf("%v:%v", config.Server, config.Port) 21 | 22 | // auth is set to nil by default 23 | // workaround for error given if auth is disabled on the smtp server 24 | // notifier error: "smtp: server doesn't support AUTH" 25 | var auth smtp.Auth 26 | if config.Username != "" { 27 | auth = smtp.PlainAuth("", config.Username, config.Password, config.Server) 28 | } 29 | 30 | if err := smtp.SendMail(addr, auth, config.From, config.To, []byte(msg)); err != nil { 31 | return errors.Wrapf(err, "sending email notification failed") 32 | } 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /pkg/scheduler/scheduler.go: -------------------------------------------------------------------------------- 1 | package scheduler 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/dustin/go-humanize" 8 | "github.com/pkg/errors" 9 | "github.com/robfig/cron" 10 | log "github.com/sirupsen/logrus" 11 | 12 | "github.com/stefanprodan/mgob/pkg/backup" 13 | "github.com/stefanprodan/mgob/pkg/config" 14 | "github.com/stefanprodan/mgob/pkg/db" 15 | "github.com/stefanprodan/mgob/pkg/metrics" 16 | "github.com/stefanprodan/mgob/pkg/notifier" 17 | ) 18 | 19 | type Scheduler struct { 20 | Cron *cron.Cron 21 | Plans []config.Plan 22 | Config *config.AppConfig 23 | Modules *config.ModuleConfig 24 | Stats *db.StatusStore 25 | metrics *metrics.BackupMetrics 26 | } 27 | 28 | func New(plans []config.Plan, conf *config.AppConfig, modules *config.ModuleConfig, stats *db.StatusStore) *Scheduler { 29 | s := &Scheduler{ 30 | Cron: cron.New(), 31 | Plans: plans, 32 | Config: conf, 33 | Modules: modules, 34 | Stats: stats, 35 | metrics: metrics.New("mgob", "scheduler"), 36 | } 37 | 38 | return s 39 | } 40 | 41 | func (s *Scheduler) Start() error { 42 | for _, plan := range s.Plans { 43 | schedule, err := cron.ParseStandard(plan.Scheduler.Cron) 44 | if err != nil { 45 | return errors.Wrapf(err, "Invalid cron %v for plan %v", plan.Scheduler.Cron, plan.Name) 46 | } 47 | s.Cron.Schedule(schedule, backupJob{plan.Name, plan, s.Config, s.Modules, s.Stats, s.metrics, s.Cron}) 48 | } 49 | 50 | s.Cron.AddFunc("0 0 */1 * *", func() { 51 | backup.TmpCleanup(s.Config.TmpPath) 52 | }) 53 | 54 | s.Cron.Start() 55 | stats := make([]*db.Status, 0) 56 | for _, e := range s.Cron.Entries() { 57 | switch e.Job.(type) { 58 | case backupJob: 59 | status := &db.Status{ 60 | Plan: e.Job.(backupJob).name, 61 | NextRun: e.Next, 62 | } 63 | stats = append(stats, status) 64 | default: 65 | log.Infof("Next tmp cleanup run at %v", e.Next) 66 | } 67 | } 68 | 69 | if err := s.Stats.Sync(stats); err != nil { 70 | log.Errorf("Status store sync failed %v", err) 71 | } 72 | 73 | return nil 74 | } 75 | 76 | type backupJob struct { 77 | name string 78 | plan config.Plan 79 | conf *config.AppConfig 80 | modules *config.ModuleConfig 81 | stats *db.StatusStore 82 | metrics *metrics.BackupMetrics 83 | cron *cron.Cron 84 | } 85 | 86 | func (b backupJob) Run() { 87 | log.WithField("plan", b.plan.Name).Info("Backup started") 88 | status := "200" 89 | var backupLog string 90 | t1 := time.Now() 91 | 92 | res, err := backup.Run(b.plan, b.conf, b.modules) 93 | if err != nil { 94 | status = "500" 95 | backupLog = fmt.Sprintf("Backup failed %v", err) 96 | log.WithField("plan", b.plan.Name).Error(backupLog) 97 | 98 | if err := notifier.SendNotification(fmt.Sprintf("%v backup failed", b.plan.Name), 99 | err.Error(), true, b.plan); err != nil { 100 | log.WithField("plan", b.plan.Name).Errorf("Notifier failed %v", err) 101 | } 102 | } else { 103 | backupLog = fmt.Sprintf("Backup finished in %v archive %v size %v", 104 | res.Duration, res.Name, humanize.Bytes(uint64(res.Size))) 105 | 106 | log.WithField("plan", b.plan.Name).Info(backupLog) 107 | if err := notifier.SendNotification(fmt.Sprintf("%v backup finished", b.plan.Name), 108 | fmt.Sprintf("%v backup finished in %v archive size %v", 109 | res.Name, res.Duration, humanize.Bytes(uint64(res.Size))), 110 | false, b.plan); err != nil { 111 | log.WithField("plan", b.plan.Name).Errorf("Notifier failed %v", err) 112 | } 113 | } 114 | 115 | t2 := time.Now() 116 | b.metrics.Total.WithLabelValues(b.plan.Name, status).Inc() 117 | b.metrics.Size.WithLabelValues(b.plan.Name, status).Set(float64(res.Size)) 118 | b.metrics.Latency.WithLabelValues(b.plan.Name, status).Observe(t2.Sub(t1).Seconds()) 119 | 120 | s := &db.Status{ 121 | LastRun: &res.Timestamp, 122 | LastRunStatus: status, 123 | Plan: b.plan.Name, 124 | LastRunLog: backupLog, 125 | } 126 | 127 | for _, e := range b.cron.Entries() { 128 | switch e.Job.(type) { 129 | case backupJob: 130 | if e.Job.(backupJob).name == b.plan.Name { 131 | s.NextRun = e.Next 132 | break 133 | } 134 | } 135 | } 136 | 137 | log.WithField("plan", b.plan.Name).Infof("Next run at %v", s.NextRun) 138 | if err := b.stats.Put(s); err != nil { 139 | log.WithField("plan", b.plan.Name).Errorf("Status store failed %v", err) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /screens/gke-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanprodan/mgob/7431cafcb3b2cfe6404551947eb1365f1a07b5b2/screens/gke-diagram.png -------------------------------------------------------------------------------- /screens/mgob-gcp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stefanprodan/mgob/7431cafcb3b2cfe6404551947eb1365f1a07b5b2/screens/mgob-gcp.png -------------------------------------------------------------------------------- /test/config/mongo-dev.yml: -------------------------------------------------------------------------------- 1 | target: 2 | host: "172.18.7.40" 3 | port: 27017 4 | database: "syros" 5 | username: "" 6 | password: "" 7 | scheduler: 8 | cron: "*/3 * * * *" 9 | retention: 5 10 | timeout: 1 -------------------------------------------------------------------------------- /test/config/mongo-test.yml: -------------------------------------------------------------------------------- 1 | target: 2 | host: "172.18.7.40" 3 | port: 27017 4 | database: "mongoclient" 5 | username: "" 6 | password: "" 7 | scheduler: 8 | cron: "*/2 * * * *" 9 | retention: 5 10 | timeout: 60 11 | s3: 12 | url: "https://play.minio.io:9000" 13 | bucket: "bktest" 14 | accessKey: "Q3AM3UQ867SPQQA43P2F" 15 | secretKey: "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" 16 | api: "S3v4" -------------------------------------------------------------------------------- /test/travis/mongo-test.yml: -------------------------------------------------------------------------------- 1 | scheduler: 2 | cron: "* * * * *" 3 | retention: 5 4 | timeout: 60 5 | target: 6 | host: "127.0.0.1" 7 | port: 27017 8 | database: test 9 | s3: 10 | url: "http://127.0.0.1:9000" 11 | bucket: "backup" 12 | accessKey: "AKIAIOSFODNN7EXAMPLE" 13 | secretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" 14 | api: "S3v4" 15 | sftp: 16 | host: "127.0.0.1" 17 | port: 20022 18 | username: test 19 | password: test 20 | dir: backup -------------------------------------------------------------------------------- /test/travis/sftp-authorization-test.yml: -------------------------------------------------------------------------------- 1 | scheduler: 2 | cron: "* * * * *" 3 | retention: 5 4 | timeout: 60 5 | target: 6 | host: "127.0.0.1" 7 | port: 27017 8 | database: test 9 | sftp: 10 | host: "127.0.0.1" 11 | port: 20023 12 | username: test 13 | private_key: /etc/ssh/ssh_host_rsa_key 14 | dir: backup --------------------------------------------------------------------------------