├── .dockerignore ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE.md └── stale.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── config.json ├── config.with-mount.json ├── go.mod ├── go.sum ├── main.go ├── rclone ├── driver │ ├── driver.go │ ├── driver_test.go │ └── tools.go ├── integration │ └── integration_test.go └── rclone.go └── support ├── docker └── Dockerfile └── how-to └── nextcloud.md /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.git* 3 | plugin/ 4 | support/ 5 | 6 | /docker-volume-rclone 7 | /docker-volume-rclone* 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | liberapay: sapk 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | - Plugin version (or commit ref) : 7 | - Docker version : 8 | - Plugin type : legacy/managed 9 | - Operating system: 10 | 11 | ## Description 12 | 13 | ... 14 | 15 | ## Logs 16 | 17 | 18 | 19 | ## Tests 20 | 21 | 25 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 60 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - todo 8 | - enhancement 9 | - pinned 10 | - security 11 | # Label to use when marking an issue as stale 12 | staleLabel: stale 13 | # Comment to post when marking an issue as stale. Set to `false` to disable 14 | markComment: > 15 | This issue has been automatically marked as stale because it has not had 16 | recent activity. It will be closed if no further activity occurs. Thank you 17 | for your contributions. 18 | # Comment to post when closing a stale issue. Set to `false` to disable 19 | closeComment: false -------------------------------------------------------------------------------- /.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 | /docker-volume-rclone 27 | /docker-volume-rclone-* 28 | /coverage.* 29 | /build 30 | /.gopath 31 | /plugin 32 | /vendor -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: true 3 | services: 4 | - docker 5 | addons: 6 | apt: 7 | packages: 8 | - upx-ucl 9 | 10 | go: 11 | - "1.15" 12 | #- tip 13 | 14 | env: 15 | - GO111MODULE=on DOCKER_CLI_EXPERIMENTAL=enabled 16 | 17 | before_install: 18 | - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 19 | - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu 20 | $(lsb_release -cs) stable" 21 | - sudo apt-get update 22 | - sudo apt-get -y install docker-ce 23 | - docker buildx 24 | - curl https://rclone.org/install.sh | sudo bash 25 | 26 | install: 27 | - make dev-deps 28 | - docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 29 | - docker buildx create --use --driver-opt image=moby/buildkit:master 30 | - docker buildx inspect --bootstrap 31 | - docker buildx ls 32 | script: 33 | - make lint 34 | - make build 35 | - make test 36 | - "./docker-volume-rclone" 37 | after_success: 38 | - bash <(curl -s https://codecov.io/bash) 39 | - docker --version 40 | - make docker-plugin 41 | - PLUGIN_CONFIG=config.with-mount.json PLUGIN_TAG=with-mount make docker-plugin 42 | - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"; fi 43 | - if [ "$TRAVIS_BRANCH" = "master" && "$TRAVIS_EVENT_TYPE" != "pull_request"]; then make docker-plugin-push; fi 44 | - if [ "$TRAVIS_BRANCH" = "master" && "$TRAVIS_EVENT_TYPE" != "pull_request"]; PLUGIN_TAG=with-mount make docker-plugin-push; fi 45 | #- make docker-buildx-plugin 46 | #- if [ "$TRAVIS_BRANCH" = "master" && "$TRAVIS_EVENT_TYPE" != "pull_request"]; PLUGIN_TAG=latest-linux-386 make docker-plugin-push; fi 47 | #- if [ "$TRAVIS_BRANCH" = "master" && "$TRAVIS_EVENT_TYPE" != "pull_request"]; PLUGIN_TAG=latest-linux-amd64 make docker-plugin-push; fi 48 | #- if [ "$TRAVIS_BRANCH" = "master" && "$TRAVIS_EVENT_TYPE" != "pull_request"]; PLUGIN_TAG=latest-linux-arm64 make docker-plugin-push; fi 49 | #- if [ "$TRAVIS_BRANCH" = "master" && "$TRAVIS_EVENT_TYPE" != "pull_request"]; PLUGIN_TAG=latest-linux-arm-v7 make docker-plugin-push; fi 50 | #- PLUGIN_CONFIG=config.with-mount.json PLUGIN_TAG=with-mount make docker-buildx-plugin 51 | #- if [ "$TRAVIS_BRANCH" = "master" && "$TRAVIS_EVENT_TYPE" != "pull_request"]; PLUGIN_TAG=with-mount-linux-386 make docker-plugin-push; fi 52 | #- if [ "$TRAVIS_BRANCH" = "master" && "$TRAVIS_EVENT_TYPE" != "pull_request"]; PLUGIN_TAG=with-mount-linux-amd64 make docker-plugin-push; fi 53 | #- if [ "$TRAVIS_BRANCH" = "master" && "$TRAVIS_EVENT_TYPE" != "pull_request"]; PLUGIN_TAG=with-mount-linux-arm64 make docker-plugin-push; fi 54 | #- if [ "$TRAVIS_BRANCH" = "master" && "$TRAVIS_EVENT_TYPE" != "pull_request"]; PLUGIN_TAG=with-mount-linux-arm-v7 make docker-plugin-push; fi 55 | 56 | before_deploy: 57 | - make compress 58 | - PLUGIN_TAG=$(git describe --tags --abbrev=0) make docker-plugin 59 | - PLUGIN_CONFIG=config.with-mount.json PLUGIN_TAG=$(git describe --tags --abbrev=0)-with-mount make docker-plugin 60 | #- PLUGIN_TAG=$(git describe --tags --abbrev=0) make docker-buildx-plugin 61 | #- PLUGIN_CONFIG=config.with-mount.json PLUGIN_TAG=$(git describe --tags --abbrev=0)-with-mount make docker-buildx-plugin 62 | - PLUGIN_TAG=$(git describe --tags --abbrev=0) make docker-plugin-push 63 | #- PLUGIN_TAG=$(git describe --tags --abbrev=0)-linux-386 make docker-plugin-push 64 | #- PLUGIN_TAG=$(git describe --tags --abbrev=0)-linux-amd64 make docker-plugin-push 65 | #- PLUGIN_TAG=$(git describe --tags --abbrev=0)-linux-arm64 make docker-plugin-push 66 | #- PLUGIN_TAG=$(git describe --tags --abbrev=0)-linux-arm-v7 make docker-plugin-push 67 | - PLUGIN_TAG=$(git describe --tags --abbrev=0)-with-mount make docker-plugin-push 68 | #- PLUGIN_TAG=$(git describe --tags --abbrev=0)-with-mount-linux-386 make docker-plugin-push 69 | #- PLUGIN_TAG=$(git describe --tags --abbrev=0)-with-mount-linux-amd64 make docker-plugin-push 70 | #- PLUGIN_TAG=$(git describe --tags --abbrev=0)-with-mount-linux-arm64 make docker-plugin-push 71 | #- PLUGIN_TAG=$(git describe --tags --abbrev=0)-with-mount-linux-arm-v7 make docker-plugin-push 72 | deploy: 73 | provider: releases 74 | api_key: 75 | secure: Hn6VHF90T7UHwodKJTcXai4mNKwPMtmw32Y/UzHIEdcjz4xED3LpAHa54FiknWP7DbYPTl7XDYIH3dUK20U16cKY+GF5Z16v0cDtqOvRiQJajeQ3W496U5T0XiQra7UrCTCnCHzt3C/cLk/UototJcybUERe7s7CCIuRiWX3WfwmbWITp7cAw9VKP2LUyle7+GYTKa/pS52zTnbj7+XnnuBqNr90kUUM05DqcpSeTCnucftLnW7FHa3KC0HmyI7a/vRSptdUPHpzH9+Uqr+G04hwxjLzF/LANs5Z2IaJj7KAB4fR0Na1rbCV2hMkndeILuMO+QiZOwpGk6YD23BfWL504TTdotwwrxrBUyWDVppNxbopL1zhGR9T1dXyGZnsPI0bJ4Z+HFc0Oh+VDIZxRO5oXFOmFWc/CGWgYbEwZ17M1elTLAcVATrL/n+sAcYo9xVcP+CJT+kQjJisYQ9CXEaiY25TJmNjg6Ha28fB+dCRwlTwoxXOhINzv8AWyTMBun83KZNXK4xJ0zYkJkB9jOpx3GPpr1XjMYXWr9oNZIreGIKWX113ULRJ+0SvMcd0tobzVd428ZlvIYuC1CmeP0iuSgzwmX3C60Jlxz4EnwRL3YAsIX/24BU//9elp/WZbATrUOyLTy9KVcUGFALmW0k7w2oTFGKY9DlCSbrdslk= 76 | file: "./docker-volume-rclone" 77 | skip_cleanup: true 78 | on: 79 | tags: true 80 | repo: sapk/docker-volume-rclone 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Antoine GIRARD 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 | #Inspired from : https://github.com/littlemanco/boilr-makefile/blob/master/template/Makefile, https://github.com/geetarista/go-boilerplate/blob/master/Makefile, https://github.com/nascii/go-boilerplate/blob/master/GNUmakefile https://github.com/cloudflare/hellogopher/blob/master/Makefile 2 | #PATH=$(PATH:):$(GOPATH)/bin 3 | 4 | #Auto set GOPATH value 5 | GOPATH ?=$(shell go env GOPATH) 6 | 7 | APP_NAME=docker-volume-rclone 8 | APP_VERSION=$(shell git describe --tags --abbrev=0) 9 | 10 | PLUGIN_USER ?= sapk 11 | PLUGIN_NAME ?= plugin-rclone 12 | PLUGIN_TAG ?= latest 13 | PLUGIN_IMAGE ?= $(PLUGIN_USER)/$(PLUGIN_NAME):$(PLUGIN_TAG) 14 | PLUGIN_CONFIG ?= config.json 15 | 16 | GIT_HASH=$(shell git rev-parse --short HEAD) 17 | GIT_BRANCH=$(shell git rev-parse --abbrev-ref HEAD) 18 | DATE := $(shell date -u '+%Y-%m-%d-%H%M-UTC') 19 | PWD=$(shell pwd) 20 | 21 | ARCHIVE=$(APP_NAME)-$(APP_VERSION)-$(GIT_HASH).tar.gz 22 | #DEPS = $(go list -f '{{range .TestImports}}{{.}} {{end}}' ./...) 23 | LDFLAGS = \ 24 | -s -w \ 25 | -X main.Version=$(APP_VERSION) -X main.Branch=$(GIT_BRANCH) -X main.Commit=$(GIT_HASH) -X main.BuildTime=$(DATE) 26 | 27 | DOC_PORT = 6060 28 | #GOOS=linux 29 | 30 | ERROR_COLOR=\033[31;01m 31 | NO_COLOR=\033[0m 32 | OK_COLOR=\033[32;01m 33 | WARN_COLOR=\033[33;01m 34 | 35 | GO111MODULE=on 36 | 37 | all: build compress done 38 | 39 | build: clean format compile 40 | 41 | docker-plugin: docker-rootfs docker-plugin-create 42 | 43 | docker-buildx-plugin: docker-buildx-rootfs docker-plugin-create-linux-arm64 docker-plugin-create-linux-arm-v7 44 | 45 | docker-image: 46 | @echo -e "$(OK_COLOR)==> Docker build image : ${PLUGIN_IMAGE} $(NO_COLOR)" 47 | docker build --no-cache --pull -t ${PLUGIN_IMAGE} -f support/docker/Dockerfile . 48 | 49 | docker-rootfs: docker-image 50 | @echo -e "$(OK_COLOR)==> create rootfs directory in ./plugin/default/rootfs$(NO_COLOR)" 51 | @mkdir -p ./plugin/default/rootfs 52 | @cntr=${PLUGIN_USER}-${PLUGIN_NAME}-${PLUGIN_TAG}-$$(date +'%Y%m%d-%H%M%S'); \ 53 | docker create --name $$cntr ${PLUGIN_IMAGE}; \ 54 | docker export $$cntr | tar -x -C ./plugin/default/rootfs; \ 55 | docker rm -vf $$cntr 56 | @echo -e "### copy ${PLUGIN_CONFIG} to ./plugin/default$(NO_COLOR)" 57 | @cp ${PLUGIN_CONFIG} ./plugin/default/config.json 58 | 59 | docker-plugin-create: 60 | @echo -e "$(OK_COLOR)==> Remove existing plugin : ${PLUGIN_IMAGE} if exists$(NO_COLOR)" 61 | @docker plugin rm -f ${PLUGIN_IMAGE} || true 62 | @echo -e "$(OK_COLOR)==> Create new plugin : ${PLUGIN_IMAGE} from ./plugin/default$(NO_COLOR)" 63 | docker plugin create ${PLUGIN_IMAGE} ./plugin/default 64 | 65 | docker-plugin-push: 66 | @echo -e "$(OK_COLOR)==> push plugin : ${PLUGIN_IMAGE}$(NO_COLOR)" 67 | docker plugin push ${PLUGIN_IMAGE} 68 | 69 | docker-plugin-enable: 70 | @echo -e "$(OK_COLOR)==> Enable plugin ${PLUGIN_IMAGE}$(NO_COLOR)" 71 | docker plugin enable ${PLUGIN_IMAGE} 72 | 73 | docker-buildx-rootfs: docker-buildx-rootfs-build docker-buildx-rootfs-organize-linux-arm64 docker-buildx-rootfs-organize-linux-arm-v7 74 | 75 | docker-buildx-rootfs-build: clean-buildx 76 | @echo -e "$(OK_COLOR)==> create cross-platform rootfs directories in ./plugin/rootfs.tar$(NO_COLOR)" 77 | @mkdir -p ./plugin/ 78 | @docker buildx build --progress plain --platform linux/arm64,linux/arm/v7 -o type=tar,dest=./plugin/rootfs.tar -f support/docker/Dockerfile . 79 | @tar -xf ./plugin/rootfs.tar -C ./plugin/ 80 | @rm ./plugin/rootfs.tar 81 | 82 | docker-buildx-rootfs-organize-%: 83 | @mkdir -p ./plugin/$(subst -,_,$*)/rootfs 84 | @mv ./plugin/$(subst -,_,$*)/bin ./plugin/$(subst -,_,$*)/rootfs/ 85 | @mv ./plugin/$(subst -,_,$*)/data ./plugin/$(subst -,_,$*)/rootfs/ 86 | @mv ./plugin/$(subst -,_,$*)/dev ./plugin/$(subst -,_,$*)/rootfs/ 87 | @mv ./plugin/$(subst -,_,$*)/etc ./plugin/$(subst -,_,$*)/rootfs/ 88 | @mv ./plugin/$(subst -,_,$*)/home ./plugin/$(subst -,_,$*)/rootfs/ 89 | @mv ./plugin/$(subst -,_,$*)/lib ./plugin/$(subst -,_,$*)/rootfs/ 90 | @mv ./plugin/$(subst -,_,$*)/media ./plugin/$(subst -,_,$*)/rootfs/ 91 | @mv ./plugin/$(subst -,_,$*)/mnt ./plugin/$(subst -,_,$*)/rootfs/ 92 | @mv ./plugin/$(subst -,_,$*)/opt ./plugin/$(subst -,_,$*)/rootfs/ 93 | @mv ./plugin/$(subst -,_,$*)/proc ./plugin/$(subst -,_,$*)/rootfs/ 94 | @mv ./plugin/$(subst -,_,$*)/root ./plugin/$(subst -,_,$*)/rootfs/ 95 | @mv ./plugin/$(subst -,_,$*)/run ./plugin/$(subst -,_,$*)/rootfs/ 96 | @mv ./plugin/$(subst -,_,$*)/sbin ./plugin/$(subst -,_,$*)/rootfs/ 97 | @mv ./plugin/$(subst -,_,$*)/srv ./plugin/$(subst -,_,$*)/rootfs/ 98 | @mv ./plugin/$(subst -,_,$*)/sys ./plugin/$(subst -,_,$*)/rootfs/ 99 | @mv ./plugin/$(subst -,_,$*)/tmp ./plugin/$(subst -,_,$*)/rootfs/ 100 | @mv ./plugin/$(subst -,_,$*)/usr ./plugin/$(subst -,_,$*)/rootfs/ 101 | @mv ./plugin/$(subst -,_,$*)/var ./plugin/$(subst -,_,$*)/rootfs/ 102 | @cp ${PLUGIN_CONFIG} ./plugin/$(subst -,_,$*)/config.json 103 | 104 | docker-plugin-create-%: 105 | @echo -e "$(OK_COLOR)==> Remove existing plugin : ${PLUGIN_IMAGE} if exists$(NO_COLOR)" 106 | @docker plugin rm -f "${PLUGIN_IMAGE}-$(subst _,-,$*)" || true 107 | @echo -e "$(OK_COLOR)==> Create new plugin : ${PLUGIN_IMAGE} from ./plugin/$(subst -,_,$*)$(NO_COLOR)" 108 | docker plugin create "${PLUGIN_IMAGE}-$(subst _,-,$*)" ./plugin/$(subst -,_,$*) 109 | 110 | compile: 111 | @echo -e "$(OK_COLOR)==> Building...$(NO_COLOR)" 112 | go build -v -ldflags "$(LDFLAGS)" 113 | 114 | release: clean deps format 115 | @mkdir build 116 | @echo -e "$(OK_COLOR)==> Building for linux 32 ...$(NO_COLOR)" 117 | CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -o build/${APP_NAME}-linux-386 -ldflags "$(LDFLAGS)" 118 | @echo -e "$(OK_COLOR)==> Trying to compress binary ...$(NO_COLOR)" 119 | @upx --brute build/${APP_NAME}-linux-386 || upx-ucl --brute build/${APP_NAME}-linux-386 || echo -e "$(WARN_COLOR)==> No tools found to compress binary.$(NO_COLOR)" 120 | 121 | @echo -e "$(OK_COLOR)==> Building for linux 64 ...$(NO_COLOR)" 122 | GO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/${APP_NAME}-linux-amd64 -ldflags "$(LDFLAGS)" 123 | @echo -e "$(OK_COLOR)==> Trying to compress binary ...$(NO_COLOR)" 124 | @upx --brute build/${APP_NAME}-linux-amd64 || upx-ucl --brute build/${APP_NAME}-linux-amd64 || echo -e "$(WARN_COLOR)==> No tools found to compress binary.$(NO_COLOR)" 125 | 126 | @echo -e "$(OK_COLOR)==> Building for linux arm ...$(NO_COLOR)" 127 | CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -o build/${APP_NAME}-linux-armv6 -ldflags "$(LDFLAGS)" 128 | @echo -e "$(OK_COLOR)==> Trying to compress binary ...$(NO_COLOR)" 129 | @upx --brute build/${APP_NAME}-linux-armv6 || upx-ucl --brute build/${APP_NAME}-linux-armv6 || echo -e "$(WARN_COLOR)==> No tools found to compress binary.$(NO_COLOR)" 130 | 131 | @echo -e "$(OK_COLOR)==> Building for darwin32 ...$(NO_COLOR)" 132 | CGO_ENABLED=0 GOOS=darwin GOARCH=386 go build -o build/${APP_NAME}-darwin-386 -ldflags "$(LDFLAGS)" 133 | @echo -e "$(OK_COLOR)==> Trying to compress binary ...$(NO_COLOR)" 134 | @upx --brute build/${APP_NAME}-darwin-386 || upx-ucl --brute build/${APP_NAME}-darwin-386 || echo -e "$(WARN_COLOR)==> No tools found to compress binary.$(NO_COLOR)" 135 | 136 | @echo -e "$(OK_COLOR)==> Building for darwin64 ...$(NO_COLOR)" 137 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o build/${APP_NAME}-darwin-amd64 -ldflags "$(LDFLAGS)" 138 | @echo -e "$(OK_COLOR)==> Trying to compress binary ...$(NO_COLOR)" 139 | @upx --brute build/${APP_NAME}-darwin-amd64 || upx-ucl --brute build/${APP_NAME}-darwin-amd64 || echo -e "$(WARN_COLOR)==> No tools found to compress binary.$(NO_COLOR)" 140 | 141 | # @echo -e "$(OK_COLOR)==> Building for win32 ...$(NO_COLOR)" 142 | # CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -o build/${APP_NAME}-win-386 -ldflags "$(LDFLAGS)" 143 | # @echo -e "$(OK_COLOR)==> Trying to compress binary ...$(NO_COLOR)" 144 | # @upx --brute build/${APP_NAME}-win-386 || upx-ucl --brute build/${APP_NAME}-win-386 || echo -e "$(WARN_COLOR)==> No tools found to compress binary.$(NO_COLOR)" 145 | 146 | # @echo -e "$(OK_COLOR)==> Building for win64 ...$(NO_COLOR)" 147 | # CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o build/${APP_NAME}-win-amd64 -ldflags "$(LDFLAGS)" 148 | # @echo -e "$(OK_COLOR)==> Trying to compress binary ...$(NO_COLOR)" 149 | # @upx --brute build/${APP_NAME}-win-amd64 || upx-ucl --brute build/${APP_NAME}-win-amd64 || echo -e "$(WARN_COLOR)==> No tools found to compress binary.$(NO_COLOR)" 150 | 151 | @echo -e "$(OK_COLOR)==> Archiving ...$(NO_COLOR)" 152 | @tar -zcvf build/$(ARCHIVE) LICENSE README.md build/ 153 | 154 | clean: 155 | @if [ -x $(APP_NAME) ]; then rm $(APP_NAME); fi 156 | @if [ -d build ]; then rm -R build; fi 157 | @rm -rf ./plugin 158 | @go clean ./... 159 | 160 | clean-buildx: 161 | @rm -rf ./plugin/linux_* 162 | 163 | compress: 164 | @echo -e "$(OK_COLOR)==> Trying to compress binary ...$(NO_COLOR)" 165 | @upx --brute $(APP_NAME) || upx-ucl --brute $(APP_NAME) || echo -e "$(WARN_COLOR)==> No tools found to compress binary.$(NO_COLOR)" 166 | 167 | format: 168 | @echo -e "$(OK_COLOR)==> Formatting...$(NO_COLOR)" 169 | go fmt ./rclone/... 170 | 171 | test: dev-deps format 172 | @echo -e "$(OK_COLOR)==> Running tests...$(NO_COLOR)" 173 | go vet ./rclone/... || true 174 | go test -v -race -coverprofile=coverage.unit.out -covermode=atomic ./rclone/driver 175 | go test -v -race -coverprofile=coverage.inte.out -covermode=atomic ./rclone/integration 176 | gocovmerge `ls coverage.*.out` > coverage.out 177 | go tool cover -html=coverage.out -o coverage.html 178 | 179 | docs: 180 | @echo -e "$(OK_COLOR)==> Serving docs at http://localhost:$(DOC_PORT).$(NO_COLOR)" 181 | @godoc -http=:$(DOC_PORT) 182 | 183 | lint: dev-deps 184 | gometalinter --deadline=5m --concurrency=2 --vendor --disable=gotype --errors ./... 185 | gometalinter --deadline=5m --concurrency=2 --vendor --disable=gotype ./... || echo "Something could be improved !" 186 | # gometalinter --deadline=5m --concurrency=2 --vendor ./... # disable gotype temporary 187 | 188 | dev-deps: 189 | @echo -e "$(OK_COLOR)==> Installing developement dependencies...$(NO_COLOR)" 190 | @GO111MODULE=off go get github.com/nsf/gocode 191 | @GO111MODULE=off go get github.com/alecthomas/gometalinter 192 | @GO111MODULE=off go get github.com/wadey/gocovmerge 193 | @GO111MODULE=off $(GOPATH)/bin/gometalinter --install > /dev/null 194 | 195 | update-dev-deps: 196 | @echo -e "$(OK_COLOR)==> Installing/Updating developement dependencies...$(NO_COLOR)" 197 | GO111MODULE=off go get -u github.com/nsf/gocode 198 | GO111MODULE=off go get -u github.com/alecthomas/gometalinter 199 | GO111MODULE=off go get -u github.com/wadey/gocovmerge 200 | GO111MODULE=off $(GOPATH)/bin/gometalinter --install --update 201 | 202 | deps: 203 | @echo -e "$(OK_COLOR)==> Installing dependencies ...$(NO_COLOR)" 204 | go mod download 205 | 206 | update-deps: dev-deps 207 | @echo -e "$(OK_COLOR)==> Updating all dependencies ...$(NO_COLOR)" 208 | go get -u -v ./... 209 | 210 | done: 211 | @echo -e "$(OK_COLOR)==> Done.$(NO_COLOR)" 212 | 213 | .PHONY: all build docker-plugin docker-plugin-enable docker-plugin-push docker-plugin-create docker-rootfs docker-image compile release clean compress format test docs lint dev-deps update-dev-deps deps update-deps done docker-buildx-rootfs docker-buildx-rootfs-build clean-buildx docker-buildx-plugin docker-plugin-create-% docker-buildx-rootfs-organize-% 214 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-volume-rclone [![License](https://img.shields.io/badge/license-MIT-red.svg)](https://github.com/sapk/docker-volume-rclone/blob/master/LICENSE) ![Project Status](http://img.shields.io/badge/status-beta-orange.svg) 2 | [![GitHub release](https://img.shields.io/github/release/sapk/docker-volume-rclone.svg)](https://github.com/sapk/docker-volume-rclone/releases) [![Go Report Card](https://goreportcard.com/badge/github.com/sapk/docker-volume-rclone)](https://goreportcard.com/report/github.com/sapk/docker-volume-rclone) 3 | [![codecov](https://codecov.io/gh/sapk/docker-volume-rclone/branch/master/graph/badge.svg)](https://codecov.io/gh/sapk/docker-volume-rclone) 4 | master : [![Travis master](https://api.travis-ci.org/sapk/docker-volume-rclone.svg?branch=master)](https://travis-ci.org/sapk/docker-volume-rclone) develop : [![Travis develop](https://api.travis-ci.org/sapk/docker-volume-rclone.svg?branch=develop)](https://travis-ci.org/sapk/docker-volume-rclone) 5 | 6 | 7 | Use Rclone as a backend for docker volume. This permit to easely mount a lot of cloud provider (https://rclone.org/overview/). 8 | 9 | Status : **BETA (work and in use but still need improvements)** 10 | 11 | Use Rclone cli in the plugin container so it depend on fuse on the host. 12 | 13 | ## Docker plugin (Easy method) [![Docker Pulls](https://img.shields.io/docker/pulls/sapk/plugin-rclone.svg)](https://hub.docker.com/r/sapk/plugin-rclone) [![ImageLayers Size](https://img.shields.io/imagelayers/image-size/sapk/plugin-rclone/latest.svg)](https://hub.docker.com/r/sapk/plugin-rclone) 14 | ``` 15 | docker plugin install sapk/plugin-rclone 16 | docker volume create --driver sapk/plugin-rclone --opt config="$(base64 ~/.config/rclone/rclone.conf)" --opt remote=some-remote:bucket/path --name test 17 | docker run -v test:/mnt --rm -ti ubuntu 18 | ``` 19 | 20 | ## Build 21 | ``` 22 | make 23 | ``` 24 | 25 | ## Start daemon 26 | ``` 27 | ./docker-volume-rclone daemon 28 | OR in a docker container 29 | docker run -d --device=/dev/fuse:/dev/fuse --cap-add=SYS_ADMIN --cap-add=MKNOD -v /run/docker/plugins:/run/docker/plugins -v /var/lib/docker-volumes/rclone:/var/lib/docker-volumes/rclone:shared sapk/docker-volume-rclone 30 | ``` 31 | 32 | For more advance params : ```./docker-volume-rclone --help OR ./docker-volume-rclone daemon --help``` 33 | ``` 34 | Run listening volume drive deamon to listen for mount request 35 | 36 | Usage: 37 | docker-volume-rclone daemon [flags] 38 | 39 | Global Flags: 40 | -b, --basedir string Mounted volume base directory (default "/var/lib/docker-volumes/rclone") 41 | -v, --verbose Turns on verbose logging 42 | ``` 43 | 44 | ## Create and Mount volume 45 | ``` 46 | docker volume create --driver rclone --opt config="$(base64 ~/.config/rclone/rclone.conf)" --opt remote=some-remote:bucket/path --name test 47 | docker run -v test:/mnt --rm -ti ubuntu 48 | ``` 49 | 50 | ## Allow acces to non-root user 51 | Some image doesn't run with the root user (and for good reason). To allow the volume to be accesible to the container user you need to add some mount option: `--opt args="--uid 1001 --gid 1001 --allow-root --allow-other"`. 52 | 53 | For example, to run an ubuntu image with an non root user (uid 33) and mount a volume: 54 | ``` 55 | docker volume create --driver sapk/plugin-rclone --opt config="$(base64 ~/.config/rclone/rclone.conf)" --opt args="--uid 33 --gid 33 --allow-root --allow-other" --opt remote=some-remote:bucket/path --name test 56 | docker run -i -t -u 33:33 --rm -v test:/mnt ubuntu /bin/ls -lah /mnt 57 | ``` 58 | 59 | ## Docker-compose 60 | First put your rclone config in a env variable: 61 | ``` 62 | export RCLONE_CONF_BASE64=$(base64 ~/.config/rclone/rclone.conf) 63 | ``` 64 | And setup you docker-compose.yml file like that 65 | ``` 66 | volumes: 67 | some_vol: 68 | driver: sapk/plugin-rclone 69 | driver_opts: 70 | config: "${RCLONE_CONF_BASE64}" 71 | args: "--read-only --fast-list" 72 | remote: "some-remote:bucket/path" 73 | ``` 74 | You can also hard-code your config in the docker-compose file in place of the env variable. 75 | 76 | ## Healthcheck 77 | The docker plugin volume protocol doesn't allow the plugin to inform the container or the docker host that the volume is not available anymore. 78 | To ensure that the volume is always live, It is recommended to setup an healthcheck to verify that the mount is responding. 79 | 80 | You can add an healthcheck like this example: 81 | ``` 82 | services: 83 | server: 84 | image: my_image 85 | healthcheck: 86 | test: ls /my/rclone/mount/folder || exit 1 87 | interval: 1m 88 | timeout: 15s 89 | retries: 3 90 | start_period: 15s 91 | ``` 92 | 93 | ## Inspired from : 94 | - https://github.com/ContainX/docker-volume-netshare/ 95 | - https://github.com/vieux/docker-volume-sshfs/ 96 | - https://github.com/sapk/docker-volume-gvfs 97 | - https://github.com/calavera/docker-volume-glusterfs 98 | - https://github.com/codedellemc/rexray 99 | 100 | ## How to debug docker managed plugin : 101 | ``` 102 | #Restart plugin in debug mode 103 | docker plugin disable sapk/plugin-rclone 104 | docker plugin set sapk/plugin-rclone DEBUG=1 105 | docker plugin enable sapk/plugin-rclone 106 | 107 | #Get files under /var/log of plugin 108 | runc --root /var/run/docker/plugins/runtime-root/plugins.moby list 109 | runc --root /var/run/docker/plugins/runtime-root/plugins.moby exec -t $CONTAINER_ID cat /var/log/rclone.log 110 | runc --root /var/run/docker/plugins/runtime-root/plugins.moby exec -t $CONTAINER_ID cat /var/log/docker-volume-rclone.log 111 | ``` 112 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Rclone plugin for Docker", 3 | "documentation": "https://docs.docker.com/engine/extend/plugins/", 4 | "entrypoint": [ 5 | "/usr/local/bin/docker-volume-rclone", 6 | "daemon" 7 | ], 8 | "env": [ 9 | { 10 | "name": "DEBUG", 11 | "settable": [ 12 | "value" 13 | ], 14 | "value": "0" 15 | } 16 | ], 17 | "interface": { 18 | "socket": "rclone.sock", 19 | "types": [ 20 | "docker.volumedriver/1.0" 21 | ] 22 | }, 23 | "linux": { 24 | "capabilities": [ 25 | "CAP_SYS_ADMIN" 26 | ], 27 | "devices": [ 28 | { 29 | "path": "/dev/fuse" 30 | } 31 | ] 32 | }, 33 | "network": { 34 | "type": "host" 35 | }, 36 | "propagatedmount": "/var/lib/docker-volumes/rclone" 37 | } 38 | -------------------------------------------------------------------------------- /config.with-mount.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Rclone plugin for Docker", 3 | "documentation": "https://docs.docker.com/engine/extend/plugins/", 4 | "entrypoint": [ 5 | "/usr/local/bin/docker-volume-rclone", 6 | "daemon" 7 | ], 8 | "env": [ 9 | { 10 | "name": "DEBUG", 11 | "settable": [ 12 | "value" 13 | ], 14 | "value": "0" 15 | } 16 | ], 17 | "interface": { 18 | "socket": "rclone.sock", 19 | "types": [ 20 | "docker.volumedriver/1.0" 21 | ] 22 | }, 23 | "mounts": [ 24 | { 25 | "name": "cache", 26 | "description": "Cache folder to use for cache mount type", 27 | "destination": "/var/cache/rclone", 28 | "source": "/var/cache/rclone", 29 | "settable": [ 30 | "source", 31 | "destination" 32 | ], 33 | "type": "bind" 34 | } 35 | ], 36 | "linux": { 37 | "capabilities": [ 38 | "CAP_SYS_ADMIN" 39 | ], 40 | "devices": [ 41 | { 42 | "path": "/dev/fuse" 43 | } 44 | ] 45 | }, 46 | "network": { 47 | "type": "host" 48 | }, 49 | "propagatedmount": "/var/lib/docker-volumes/rclone" 50 | } 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sapk/docker-volume-rclone 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/Microsoft/go-winio v0.4.15 // indirect 7 | github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect 8 | github.com/docker/go-connections v0.4.0 9 | github.com/docker/go-plugins-helpers v0.0.0-20200102110956-c9a8a2d92ccc 10 | github.com/fsnotify/fsnotify v1.4.9 // indirect 11 | github.com/magiconair/properties v1.8.4 // indirect 12 | github.com/mitchellh/mapstructure v1.3.3 // indirect 13 | github.com/pelletier/go-toml v1.8.1 // indirect 14 | github.com/rs/zerolog v1.20.0 15 | github.com/sapk/docker-volume-helpers v0.0.0-20181203012140-afb03797d7bf 16 | github.com/spf13/afero v1.4.1 // indirect 17 | github.com/spf13/cast v1.3.1 // indirect 18 | github.com/spf13/cobra v1.1.1 19 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 20 | github.com/spf13/viper v1.7.1 21 | github.com/stretchr/testify v1.4.0 22 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b // indirect 23 | golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65 // indirect 24 | golang.org/x/text v0.3.4 // indirect 25 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 26 | gopkg.in/ini.v1 v1.62.0 // indirect 27 | gopkg.in/yaml.v2 v2.3.0 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /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/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 9 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 10 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 14 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 15 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 16 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 17 | github.com/Microsoft/go-winio v0.4.15 h1:qkLXKzb1QoVatRyd/YlXZ/Kg0m5K3SPuoD82jjSOaBc= 18 | github.com/Microsoft/go-winio v0.4.15/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= 19 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 20 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 21 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 22 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 23 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 24 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 25 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 26 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 27 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 28 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 29 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 30 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 31 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 32 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 33 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 34 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 35 | github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= 36 | github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 37 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 38 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 39 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 40 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 41 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 42 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 43 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 44 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 45 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 46 | github.com/docker/go-plugins-helpers v0.0.0-20200102110956-c9a8a2d92ccc h1:/A+mPcpajLsWiX9gSnzdVKM/IzZoYiNqXHe83z50k2c= 47 | github.com/docker/go-plugins-helpers v0.0.0-20200102110956-c9a8a2d92ccc/go.mod h1:LFyLie6XcDbyKGeVK6bHe+9aJTYCxWLBg5IrJZOaXKA= 48 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 49 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 50 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 51 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 52 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 53 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 54 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 55 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 56 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 57 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 58 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 59 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 60 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 61 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 62 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 63 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 64 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 65 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 66 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 67 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 68 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 69 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 70 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 71 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 72 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 73 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 74 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 75 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 76 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 77 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 78 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 79 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 80 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 81 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 82 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 83 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 84 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 85 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 86 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 87 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 88 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 89 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 90 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 91 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 92 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 93 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 94 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 95 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 96 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 97 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 98 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 99 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 100 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 101 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 102 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 103 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 104 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 105 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 106 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 107 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 108 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 109 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 110 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 111 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 112 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 113 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 114 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 115 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 116 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 117 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 118 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 119 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 120 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 121 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 122 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 123 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 124 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 125 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 126 | github.com/magiconair/properties v1.8.4 h1:8KGKTcQQGm0Kv7vEbKFErAoAOFyyacLStRtQSeYtvkY= 127 | github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= 128 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 129 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 130 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 131 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 132 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 133 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 134 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 135 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 136 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 137 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 138 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 139 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 140 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 141 | github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8= 142 | github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 143 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 144 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 145 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 146 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 147 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 148 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 149 | github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM= 150 | github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc= 151 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 152 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 153 | github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= 154 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 155 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 156 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 157 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 158 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 159 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 160 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 161 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 162 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 163 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 164 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 165 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 166 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 167 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 168 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 169 | github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= 170 | github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= 171 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 172 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 173 | github.com/sapk/docker-volume-helpers v0.0.0-20181203012140-afb03797d7bf h1:cX9uA0qp+5W/gszM9Y0lHk8lEtvmph2ImdAoFL6iVPk= 174 | github.com/sapk/docker-volume-helpers v0.0.0-20181203012140-afb03797d7bf/go.mod h1:Sn42NciYjOR7TRoe8PF5NlRjAgrkuzLqJiGlXJ0gPac= 175 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 176 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 177 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 178 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 179 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 180 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 181 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 182 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 183 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 184 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 185 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 186 | github.com/spf13/afero v1.4.1 h1:asw9sl74539yqavKaglDM5hFpdJVK0Y5Dr/JOgQ89nQ= 187 | github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= 188 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 189 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 190 | github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= 191 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 192 | github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4= 193 | github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= 194 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 195 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 196 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 197 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 198 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 199 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 200 | github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 201 | github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= 202 | github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 203 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 204 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 205 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 206 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 207 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 208 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 209 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 210 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 211 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 212 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 213 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 214 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 215 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 216 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 217 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 218 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 219 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 220 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 221 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 222 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 223 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 224 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 225 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 226 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 227 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 228 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 229 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 230 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 231 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 232 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 233 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 234 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 235 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 236 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 237 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 238 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 239 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 240 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 241 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 242 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 243 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 244 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 245 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 246 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 247 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 248 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 249 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 250 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 251 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 252 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 253 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 254 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 255 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 256 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 257 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 258 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= 259 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 260 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 261 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 262 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 263 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 264 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 265 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 266 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 267 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 268 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 269 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 270 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 271 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 272 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 273 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 274 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 275 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 276 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 277 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 278 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 279 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 280 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 281 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 282 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 283 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 284 | golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65 h1:Qo9oJ566/Sq7N4hrGftVXs8GI2CXBCuOd4S2wHE/e0M= 285 | golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 286 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 287 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 288 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 289 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 290 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 291 | golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= 292 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 293 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 294 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 295 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 296 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 297 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 298 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 299 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 300 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 301 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 302 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 303 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 304 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 305 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 306 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 307 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 308 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 309 | golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 310 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 311 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 312 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 313 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 314 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 315 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 316 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 317 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 318 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 319 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 320 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 321 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 322 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 323 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 324 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 325 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 326 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 327 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 328 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 329 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 330 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 331 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 332 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 333 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 334 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 335 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 336 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 337 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 338 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 339 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 340 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 341 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 342 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 343 | gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= 344 | gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 345 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 346 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 347 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 348 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 349 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 350 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 351 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 352 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 353 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 354 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 355 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 356 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 357 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 358 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 359 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/sapk/docker-volume-rclone/rclone" 5 | ) 6 | 7 | var ( 8 | //Version version of app set by build flag 9 | Version string 10 | //Branch git branch of app set by build flag 11 | Branch string 12 | //Commit git commit of app set by build flag 13 | Commit string 14 | //BuildTime build time of app set by build flag 15 | BuildTime string 16 | ) 17 | 18 | func main() { 19 | rclone.Version = Version 20 | rclone.Commit = Commit 21 | rclone.Branch = Branch 22 | rclone.BuildTime = BuildTime 23 | rclone.Start() 24 | } 25 | -------------------------------------------------------------------------------- /rclone/driver/driver.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "github.com/docker/go-plugins-helpers/volume" 14 | "github.com/rs/zerolog" 15 | "github.com/rs/zerolog/log" 16 | "github.com/spf13/viper" 17 | 18 | "github.com/sapk/docker-volume-helpers/tools" 19 | ) 20 | 21 | var ( 22 | //MountTimeout timeout before killing a mount try in seconds 23 | MountTimeout = 30 24 | //CfgVersion current config version compat 25 | CfgVersion = 1 26 | //CfgFolder config folder 27 | CfgFolder = "/etc/docker-volumes/rclone/" 28 | ) 29 | 30 | type rcloneMountpoint struct { 31 | Path string `json:"path"` 32 | Connections int `json:"connections"` 33 | Context context.Context `json:"-"` 34 | } 35 | 36 | func (m *rcloneMountpoint) isMounted() (bool, error) { 37 | //TODO Better check for remote /var/lib/docker-volumes/rclone/mountpath fuse.rclone ro,nosuid,nodev,relatime,user_id=0,group_id=0 0 0 38 | buf, err := ioutil.ReadFile("/proc/mounts") 39 | if err != nil { 40 | return false, err 41 | } 42 | log.Debug().Msgf("isMounted Path: path: %s %v", m.Path, strings.Contains(string(buf), " "+m.Path+" fuse.rclone")) 43 | return strings.Contains(string(buf), " "+m.Path+" fuse.rclone"), nil 44 | } 45 | 46 | type rcloneVolume struct { 47 | Config string `json:"config"` 48 | Args string `json:"args"` 49 | Remote string `json:"remote"` 50 | Mount string `json:"mount"` 51 | Connections int `json:"connections"` 52 | CreatedAt string `json:"created_at"` 53 | } 54 | 55 | //RcloneDriver the global driver responding to call 56 | type RcloneDriver struct { 57 | sync.RWMutex 58 | root string 59 | persitence *viper.Viper 60 | volumes map[string]*rcloneVolume 61 | mounts map[string]*rcloneMountpoint 62 | } 63 | 64 | //Init start all needed deps and serve response to API call 65 | func Init(root string) *RcloneDriver { 66 | d := &RcloneDriver{ 67 | root: root, 68 | persitence: viper.New(), 69 | volumes: make(map[string]*rcloneVolume), 70 | mounts: make(map[string]*rcloneMountpoint), 71 | } 72 | 73 | d.persitence.SetDefault("volumes", map[string]*rcloneVolume{}) 74 | d.persitence.SetConfigName("persistence") 75 | d.persitence.SetConfigType("json") 76 | d.persitence.AddConfigPath(CfgFolder) 77 | if err := d.persitence.ReadInConfig(); err != nil { // Handle errors reading the config file 78 | log.Warn().Err(err).Msg("No persistence file found, I will start with a empty list of volume") 79 | } else { 80 | log.Debug().Msg("Retrieving volume list from persistence file.") 81 | 82 | var version int 83 | err := d.persitence.UnmarshalKey("version", &version) 84 | if err != nil || version != CfgVersion { 85 | log.Warn().Err(err).Msg("Unable to decode version of persistence") 86 | d.volumes = make(map[string]*rcloneVolume) 87 | d.mounts = make(map[string]*rcloneMountpoint) 88 | } else { //We have the same version 89 | err := d.persitence.UnmarshalKey("volumes", &d.volumes) 90 | if err != nil { 91 | log.Warn().Err(err).Msg("Unable to decode into struct -> start with empty list") 92 | d.volumes = make(map[string]*rcloneVolume) 93 | } 94 | err = d.persitence.UnmarshalKey("mounts", &d.mounts) 95 | if err != nil { 96 | log.Warn().Err(err).Msg("Unable to decode into struct -> start with empty list") 97 | d.mounts = make(map[string]*rcloneMountpoint) 98 | } 99 | } 100 | } 101 | return d 102 | } 103 | 104 | //Create create and init the requested volume 105 | func (d *RcloneDriver) Create(r *volume.CreateRequest) error { 106 | log.Debug().Msgf("Entering Create: name: %s, options %v", r.Name, r.Options) 107 | d.Lock() 108 | defer d.Unlock() 109 | 110 | if r.Options == nil || r.Options["config"] == "" || r.Options["remote"] == "" { 111 | return fmt.Errorf("config and remote option required") 112 | } 113 | 114 | v := &rcloneVolume{ 115 | Config: r.Options["config"], 116 | Remote: r.Options["remote"], 117 | Args: r.Options["args"], 118 | Mount: GetMountName(d, r), 119 | Connections: 0, 120 | CreatedAt: time.Now().Format(time.RFC3339), 121 | } 122 | 123 | if _, ok := d.mounts[v.Mount]; !ok { //This mountpoint doesn't allready exist -> create it 124 | m := &rcloneMountpoint{ 125 | Path: filepath.Join(d.root, v.Mount), 126 | Connections: 0, 127 | } 128 | 129 | _, err := os.Lstat(m.Path) //Create folder if not exist. This will also failed if already exist 130 | if os.IsNotExist(err) { 131 | if err = os.MkdirAll(m.Path, 0700); err != nil { 132 | return err 133 | } 134 | } else if err != nil { 135 | return err 136 | } 137 | isempty, err := tools.FolderIsEmpty(m.Path) 138 | if err != nil { 139 | return err 140 | } 141 | if !isempty { 142 | return fmt.Errorf("%v already exist and is not empty", m.Path) 143 | } 144 | d.mounts[v.Mount] = m 145 | } 146 | 147 | d.volumes[r.Name] = v 148 | log.Debug().Msgf("Volume Created: %v", v) 149 | return d.saveConfig() 150 | } 151 | 152 | //List volumes handled by the driver 153 | func (d *RcloneDriver) List() (*volume.ListResponse, error) { 154 | log.Debug().Msgf("Entering List") 155 | d.Lock() 156 | defer d.Unlock() 157 | 158 | var vols []*volume.Volume 159 | for name, v := range d.volumes { 160 | log.Debug().Msgf("Volume found: %v", v) 161 | m, ok := d.mounts[v.Mount] 162 | if !ok { 163 | return nil, fmt.Errorf("volume mount %s not found for %s", v.Mount, v.Remote) 164 | } 165 | log.Debug().Msgf("Mount found: %v", m) 166 | vols = append(vols, &volume.Volume{Name: name, Mountpoint: m.Path, CreatedAt: v.CreatedAt}) 167 | } 168 | return &volume.ListResponse{Volumes: vols}, nil 169 | } 170 | 171 | //Get get info on the requested volume 172 | func (d *RcloneDriver) Get(r *volume.GetRequest) (*volume.GetResponse, error) { 173 | log.Debug().Msgf("Entering Get: name: %s", r.Name) 174 | d.Lock() 175 | defer d.Unlock() 176 | 177 | v, ok := d.volumes[r.Name] 178 | if !ok { 179 | return nil, fmt.Errorf("volume %s not found", r.Name) 180 | } 181 | log.Debug().Msgf("Volume found: %v", v) 182 | 183 | m, ok := d.mounts[v.Mount] 184 | if !ok { 185 | return nil, fmt.Errorf("volume mount %s not found for %s", v.Mount, r.Name) 186 | } 187 | log.Debug().Msgf("Mount found: %v", m) 188 | 189 | return &volume.GetResponse{Volume: &volume.Volume{Name: r.Name, Mountpoint: m.Path, CreatedAt: v.CreatedAt}}, nil 190 | } 191 | 192 | //Remove remove the requested volume 193 | func (d *RcloneDriver) Remove(r *volume.RemoveRequest) error { 194 | log.Debug().Msgf("Entering Remove: name: %s", r.Name) 195 | d.Lock() 196 | defer d.Unlock() 197 | v, ok := d.volumes[r.Name] 198 | if !ok { 199 | return fmt.Errorf("volume %s not found", r.Name) 200 | } 201 | log.Debug().Msgf("Volume found: %v", v) 202 | 203 | m, ok := d.mounts[v.Mount] 204 | if !ok { 205 | return fmt.Errorf("volume mount %s not found for %s", v.Mount, r.Name) 206 | } 207 | log.Debug().Msgf("Mount found: %v", m) 208 | 209 | //Unmount 210 | mounted, err := m.isMounted() 211 | if err != nil { 212 | return err 213 | } 214 | if mounted { //Only if mounted 215 | if _, err := d.runCmd(fmt.Sprintf(`umount -l "%s"`, m.Path)); err != nil { 216 | time.Sleep(15 * time.Second) //Wait a little adn force unmount 217 | if _, err := d.runCmd(fmt.Sprintf(`umount -f "%s"`, m.Path)); err != nil { 218 | return err 219 | } 220 | } 221 | if m.Context != nil { 222 | m.Context.Done() 223 | } 224 | } 225 | 226 | if _, err := os.Stat(m.Path); !os.IsNotExist(err) { 227 | //Remove mount point 228 | if err := os.Remove(m.Path); err != nil { 229 | return err 230 | } 231 | } 232 | delete(d.mounts, v.Mount) 233 | delete(d.volumes, r.Name) 234 | return d.saveConfig() 235 | } 236 | 237 | //Path get path of the requested volume 238 | func (d *RcloneDriver) Path(r *volume.PathRequest) (*volume.PathResponse, error) { 239 | log.Debug().Msgf("Entering Path: name: %s", r.Name) 240 | d.RLock() 241 | defer d.RUnlock() 242 | 243 | v, ok := d.volumes[r.Name] 244 | if !ok { 245 | return nil, fmt.Errorf("volume %s not found", r.Name) 246 | } 247 | log.Debug().Msgf("Volume found: %v", v) 248 | 249 | m, ok := d.mounts[v.Mount] 250 | if !ok { 251 | return nil, fmt.Errorf("volume mount %s not found for %s", v.Mount, r.Name) 252 | } 253 | log.Debug().Msgf("Mount found: %v", m) 254 | 255 | return &volume.PathResponse{Mountpoint: m.Path}, nil 256 | } 257 | 258 | //Mount mount the requested volume 259 | func (d *RcloneDriver) Mount(r *volume.MountRequest) (*volume.MountResponse, error) { 260 | log.Debug().Msgf("Entering Mount: %v", r) 261 | d.Lock() 262 | defer d.Unlock() 263 | 264 | v, ok := d.volumes[r.Name] 265 | if !ok { 266 | return nil, fmt.Errorf("volume %s not found", r.Name) 267 | } 268 | 269 | m, ok := d.mounts[v.Mount] 270 | if !ok { 271 | return nil, fmt.Errorf("volume mount %s not found for %s", v.Mount, r.Name) 272 | } 273 | 274 | ready, err := m.isMounted() 275 | if err != nil { 276 | return nil, err 277 | } 278 | if ready { 279 | v.Connections++ 280 | m.Connections++ 281 | if err := d.saveConfig(); err != nil { 282 | return nil, err 283 | } 284 | return &volume.MountResponse{Mountpoint: m.Path}, nil 285 | } 286 | 287 | //If not 288 | //Reset (maybe a reboot) 289 | v.Connections = 0 290 | m.Connections = 0 291 | 292 | //TODO write temp file before and don't use base64 293 | //TODO locate rclone binary (/usr/bin/rclone, /usr/local/bin/rclone) 294 | var cmd string 295 | if zerolog.GlobalLevel() == zerolog.DebugLevel { 296 | cmd = fmt.Sprintf("/usr/bin/rclone --log-file /var/log/rclone.%d.log --config=<(echo \"%s\"| base64 -d) %s mount \"%s\" \"%s\" & sleep 5s", time.Now().Unix(), v.Config, v.Args, v.Remote, m.Path) 297 | } else { 298 | cmd = fmt.Sprintf("/usr/bin/rclone --config=<(echo \"%s\"| base64 -d) %s mount \"%s\" \"%s\" & sleep 5s", v.Config, v.Args, v.Remote, m.Path) 299 | } 300 | 301 | m.Context, err = d.runCmd(cmd) 302 | if err != nil { 303 | return nil, err 304 | } 305 | 306 | /* TODO test more this before using it. 307 | cmdCheck := fmt.Sprintf("mount | grep %s > /dev/null", m.Path) 308 | folderMounted := false 309 | for !folderMounted { 310 | log.Debug().Msgf("Waiting for mount: %s", m.Path) 311 | time.Sleep(5 * time.Second) 312 | folderMounted = (nil == d.runCmd(cmdCheck)) 313 | } 314 | */ 315 | //Temporary fix 316 | time.Sleep(15 * time.Second) 317 | 318 | v.Connections++ 319 | m.Connections++ 320 | if err := d.saveConfig(); err != nil { 321 | return nil, err 322 | } 323 | return &volume.MountResponse{Mountpoint: m.Path}, nil 324 | } 325 | 326 | //Unmount unmount the requested volume 327 | func (d *RcloneDriver) Unmount(r *volume.UnmountRequest) error { 328 | log.Debug().Msgf("Entering Unmount: %v", r) 329 | d.Lock() 330 | defer d.Unlock() 331 | 332 | v, ok := d.volumes[r.Name] 333 | if !ok { 334 | return fmt.Errorf("volume %s not found", r.Name) 335 | } 336 | 337 | m, ok := d.mounts[v.Mount] 338 | if !ok { 339 | return fmt.Errorf("volume mount %s not found for %s", v.Mount, r.Name) 340 | } 341 | 342 | mounted, err := m.isMounted() 343 | if err != nil { 344 | return err 345 | } 346 | if !mounted { //Force reset if not mounted 347 | m.Connections = 0 348 | v.Connections = 0 349 | } else { 350 | if m.Connections <= 1 { 351 | if _, err := d.runCmd(fmt.Sprintf(`umount -l "%s"`, m.Path)); err != nil { 352 | time.Sleep(15 * time.Second) //Wait a little adn force unmount 353 | if _, err := d.runCmd(fmt.Sprintf(`umount -f "%s"`, m.Path)); err != nil { 354 | return err 355 | } 356 | } 357 | if m.Context != nil { 358 | m.Context.Done() 359 | } 360 | m.Connections = 0 361 | v.Connections = 0 362 | } else { 363 | m.Connections-- 364 | v.Connections-- 365 | } 366 | } 367 | 368 | return d.saveConfig() 369 | } 370 | 371 | //Capabilities Send capabilities of the local driver 372 | func (d *RcloneDriver) Capabilities() *volume.CapabilitiesResponse { 373 | log.Debug().Msgf("Entering Capabilities") 374 | return &volume.CapabilitiesResponse{ 375 | Capabilities: volume.Capability{ 376 | Scope: "local", 377 | }, 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /rclone/driver/driver_test.go: -------------------------------------------------------------------------------- 1 | package driver_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "io/ioutil" 8 | "math/rand" 9 | "net/http" 10 | "os" 11 | "path/filepath" 12 | "testing" 13 | 14 | "github.com/docker/go-connections/sockets" 15 | "github.com/docker/go-plugins-helpers/volume" 16 | 17 | "github.com/sapk/docker-volume-rclone/rclone" 18 | "github.com/sapk/docker-volume-rclone/rclone/driver" 19 | 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | func TestInit(t *testing.T) { 24 | d := driver.Init("/tmp/test-root") 25 | if d == nil { 26 | t.Error("Expected to be not null, got ", d) 27 | } 28 | /* 29 | if _, err := os.Stat(cfgFolder + "gluster-persistence.json"); err != nil { 30 | t.Error("Expected file to exist, got ", err) 31 | } 32 | */ 33 | } 34 | 35 | func TestMountName(t *testing.T) { 36 | name := driver.GetMountName(&driver.RcloneDriver{}, &volume.CreateRequest{ 37 | Name: "test", 38 | Options: map[string]string{ 39 | "remote": "some-remote:bucket/", 40 | }, 41 | }) 42 | 43 | if name != "test" { 44 | t.Error("Expected to be test, got ", name) 45 | } 46 | } 47 | 48 | //Inspired from https://github.com/docker/go-plugins-helpers/blob/master/volume/api_test.go 49 | const ( 50 | createPath = "/VolumeDriver.Create" 51 | getPath = "/VolumeDriver.Get" 52 | listPath = "/VolumeDriver.List" 53 | removePath = "/VolumeDriver.Remove" 54 | hostVirtualPath = "/VolumeDriver.Path" 55 | mountPath = "/VolumeDriver.Mount" 56 | unmountPath = "/VolumeDriver.Unmount" 57 | capabilitiesPath = "/VolumeDriver.Capabilities" 58 | ) 59 | 60 | func TestHandler(t *testing.T) { 61 | //Setup 62 | driver.CfgFolder = filepath.Join(t.TempDir(), "config") 63 | volumePath := filepath.Join(t.TempDir(), "volume") 64 | dataPath := filepath.Join(t.TempDir(), "data") 65 | assert.NoError(t, os.MkdirAll(dataPath, 0700)) 66 | testFilePath := filepath.Join(dataPath, "test.file") 67 | testData := make([]byte, 42) 68 | rand.Read(testData) 69 | ioutil.WriteFile(testFilePath, testData, 0666) 70 | 71 | //Start in-memory handler 72 | d := driver.Init(filepath.Join(volumePath, rclone.PluginAlias)) 73 | h := volume.NewHandler(d) 74 | l := sockets.NewInmemSocket("test", 0) 75 | go h.Serve(l) 76 | defer l.Close() 77 | 78 | client := &http.Client{Transport: &http.Transport{ 79 | Dial: l.Dial, 80 | }} 81 | 82 | // Create No Option 83 | resp, err := pluginRequest(client, createPath, &volume.CreateRequest{Name: "foo"}) 84 | assert.NoError(t, err) 85 | var vResp volume.ErrorResponse 86 | assert.NoError(t, json.NewDecoder(resp).Decode(&vResp)) 87 | assert.Equal(t, "config and remote option required", vResp.Err) 88 | 89 | // Create No Remote 90 | resp, err = pluginRequest(client, createPath, &volume.CreateRequest{Name: "foo", Options: map[string]string{ 91 | "config": "TODO", 92 | }}) 93 | assert.NoError(t, err) 94 | assert.NoError(t, json.NewDecoder(resp).Decode(&vResp)) 95 | assert.Equal(t, "config and remote option required", vResp.Err) 96 | // Create No Config 97 | resp, err = pluginRequest(client, createPath, &volume.CreateRequest{Name: "foo", Options: map[string]string{ 98 | "remote": "TODO", 99 | }}) 100 | assert.NoError(t, err) 101 | assert.NoError(t, json.NewDecoder(resp).Decode(&vResp)) 102 | assert.Equal(t, "config and remote option required", vResp.Err) 103 | 104 | // Create 105 | resp, err = pluginRequest(client, createPath, &volume.CreateRequest{Name: "foo", Options: map[string]string{ 106 | "config": "W3Rlc3RpbmddCnR5cGUgPSBsb2NhbAoK", 107 | "remote": "testing:" + dataPath, 108 | "args": "", 109 | }}) 110 | assert.NoError(t, err) 111 | assert.NoError(t, json.NewDecoder(resp).Decode(&vResp)) 112 | assert.Equal(t, "config and remote option required", vResp.Err) 113 | 114 | //TODO test args 115 | 116 | // Get 117 | resp, err = pluginRequest(client, getPath, &volume.GetRequest{Name: "foo"}) 118 | assert.NoError(t, err) 119 | var gResp *volume.GetResponse 120 | assert.NoError(t, json.NewDecoder(resp).Decode(&gResp)) 121 | assert.Equal(t, "foo", gResp.Volume.Name) 122 | 123 | // List 124 | resp, err = pluginRequest(client, listPath, nil) 125 | assert.NoError(t, err) 126 | var lResp *volume.ListResponse 127 | assert.NoError(t, json.NewDecoder(resp).Decode(&lResp)) 128 | assert.Equal(t, 1, len(lResp.Volumes)) 129 | assert.Equal(t, "foo", lResp.Volumes[0].Name) 130 | 131 | // Path 132 | resp, err = pluginRequest(client, hostVirtualPath, &volume.PathRequest{Name: "foo"}) 133 | assert.NoError(t, err) 134 | var pResp *volume.PathResponse 135 | assert.NoError(t, json.NewDecoder(resp).Decode(&pResp)) 136 | assert.Equal(t, filepath.Join(volumePath, "rclone", "foo"), pResp.Mountpoint) 137 | 138 | if os.Getenv("CI") == "true" { 139 | //Re-List 140 | resp, err = pluginRequest(client, listPath, nil) 141 | assert.NoError(t, err) 142 | assert.NoError(t, json.NewDecoder(resp).Decode(&lResp)) 143 | assert.Equal(t, 1, len(lResp.Volumes)) 144 | } else { 145 | // Mount 146 | resp, err = pluginRequest(client, mountPath, &volume.MountRequest{Name: "foo"}) 147 | assert.NoError(t, err) 148 | var mResp *volume.PathResponse 149 | assert.NoError(t, json.NewDecoder(resp).Decode(&mResp)) 150 | assert.Equal(t, filepath.Join(volumePath, "rclone", "foo"), mResp.Mountpoint) 151 | 152 | //Check content 153 | filePathInVol := filepath.Join(mResp.Mountpoint, "test.file") 154 | dataDetected, err := ioutil.ReadFile(filePathInVol) 155 | assert.NoError(t, err) 156 | assert.Equal(t, testData, dataDetected) 157 | // Unmount 158 | resp, err = pluginRequest(client, unmountPath, &volume.UnmountRequest{Name: "foo"}) 159 | assert.NoError(t, err) 160 | var uResp volume.ErrorResponse 161 | assert.NoError(t, json.NewDecoder(resp).Decode(&uResp)) 162 | assert.Equal(t, "", uResp.Err) 163 | // Remove 164 | resp, err = pluginRequest(client, removePath, &volume.RemoveRequest{Name: "foo"}) 165 | assert.NoError(t, err) 166 | var rmResp volume.ErrorResponse 167 | assert.NoError(t, json.NewDecoder(resp).Decode(&rmResp)) 168 | assert.Equal(t, "", rmResp.Err) 169 | //Re-List 170 | resp, err = pluginRequest(client, listPath, nil) 171 | assert.NoError(t, err) 172 | assert.NoError(t, json.NewDecoder(resp).Decode(&lResp)) 173 | assert.Equal(t, 0, len(lResp.Volumes)) 174 | } 175 | // Capabilities 176 | resp, err = pluginRequest(client, capabilitiesPath, nil) 177 | assert.NoError(t, err) 178 | var cResp *volume.CapabilitiesResponse 179 | assert.NoError(t, json.NewDecoder(resp).Decode(&cResp)) 180 | assert.Equal(t, "local", cResp.Capabilities.Scope) 181 | } 182 | 183 | func pluginRequest(client *http.Client, method string, req interface{}) (io.Reader, error) { 184 | b, err := json.Marshal(req) 185 | if err != nil { 186 | return nil, err 187 | } 188 | if req == nil { 189 | b = []byte{} 190 | } 191 | resp, err := client.Post("http://localhost"+method, "application/json", bytes.NewReader(b)) 192 | if err != nil { 193 | return nil, err 194 | } 195 | 196 | return resp.Body, nil 197 | } 198 | -------------------------------------------------------------------------------- /rclone/driver/tools.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "os/exec" 10 | 11 | "github.com/docker/go-plugins-helpers/volume" 12 | "github.com/rs/zerolog/log" 13 | ) 14 | 15 | //RclonePersistence represent struct of persistence file 16 | type RclonePersistence struct { 17 | Version int `json:"version"` 18 | Volumes map[string]*rcloneVolume `json:"volumes"` 19 | Mounts map[string]*rcloneMountpoint `json:"mounts"` 20 | } 21 | 22 | func (d *RcloneDriver) saveConfig() error { 23 | fi, err := os.Lstat(CfgFolder) 24 | if os.IsNotExist(err) { 25 | if err = os.MkdirAll(CfgFolder, 0700); err != nil { 26 | return err 27 | } 28 | } else if err != nil { 29 | return err 30 | } 31 | if fi != nil && !fi.IsDir() { 32 | return fmt.Errorf("%v already exist and it's not a directory", d.root) 33 | } 34 | b, err := json.Marshal(RclonePersistence{Version: CfgVersion, Volumes: d.volumes, Mounts: d.mounts}) 35 | if err != nil { 36 | log.Warn().Err(err).Msg("Unable to encode persistence struct") 37 | } 38 | //log.Debug("Writing persistence struct, %v", b, d.volumes) 39 | err = ioutil.WriteFile(CfgFolder+"/persistence.json", b, 0600) 40 | if err != nil { 41 | log.Warn().Err(err).Msg("Unable to write persistence struct, %s") 42 | } 43 | //TODO display error messages 44 | return err 45 | } 46 | 47 | // run deamon in context of this gvfs drive with custome env 48 | func (d *RcloneDriver) runCmd(cmd string) (context.Context, error) { 49 | log.Debug().Msg(cmd) 50 | /* 51 | cli := exec.Command("/bin/bash", "-c", cmd) 52 | stdoutStderr, err := cli.CombinedOutput() 53 | log.Debugf("%s", stdoutStderr) 54 | return err 55 | */ 56 | ctx := context.Background() 57 | return ctx, exec.CommandContext(ctx, "/bin/bash", "-c", cmd).Run() 58 | //TODO output log 59 | } 60 | 61 | //GetMountName return the translated volume name 62 | func GetMountName(d *RcloneDriver, r *volume.CreateRequest) string { 63 | return r.Name 64 | } 65 | -------------------------------------------------------------------------------- /rclone/integration/integration_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "io/ioutil" 8 | "net" 9 | "net/http" 10 | "os/user" 11 | "path/filepath" 12 | "testing" 13 | "time" 14 | 15 | "github.com/docker/go-plugins-helpers/volume" 16 | 17 | "github.com/sapk/docker-volume-rclone/rclone" 18 | 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | //Inspired from https://github.com/docker/go-plugins-helpers/blob/master/volume/api_test.go 23 | const ( 24 | //createPath = "/VolumeDriver.Create" 25 | //getPath = "/VolumeDriver.Get" 26 | //listPath = "/VolumeDriver.List" 27 | //removePath = "/VolumeDriver.Remove" 28 | //hostVirtualPath = "/VolumeDriver.Path" 29 | //mountPath = "/VolumeDriver.Mount" 30 | //unmountPath = "/VolumeDriver.Unmount" 31 | capabilitiesPath = "/VolumeDriver.Capabilities" 32 | ) 33 | 34 | func startDaemon(t *testing.T) { 35 | //Launch 36 | cmd := rclone.NewRootCmd() 37 | cmd.SetArgs([]string{"daemon"}) 38 | go cmd.Execute() 39 | 40 | time.Sleep(10 * time.Millisecond) 41 | } 42 | 43 | func TestIntergation(t *testing.T) { 44 | //Start one at a time 45 | t.Run("cmd/version", testCmdVersion) 46 | t.Run("cmd/daemon", testCmdDeamon) 47 | } 48 | 49 | func testCmdDeamon(t *testing.T) { 50 | u, err := user.Current() 51 | assert.NoError(t, err) 52 | if u.Uid != "0" { 53 | t.Skipf("Skipping daemon tests since you are not root") 54 | } 55 | 56 | startDaemon(t) 57 | 58 | t.Run("Capabilities", testCapabilities) 59 | //TODO add more 60 | 61 | time.Sleep(10 * time.Millisecond) 62 | } 63 | 64 | func testCapabilities(t *testing.T) { 65 | dial, err := net.Dial("unix", filepath.Join("/run/docker/plugins", "rclone.sock")) 66 | assert.NoError(t, err) 67 | 68 | client := &http.Client{Transport: &http.Transport{ 69 | Dial: func(network, addr string) (net.Conn, error) { 70 | return dial, nil 71 | }, 72 | }} 73 | 74 | // Capabilities 75 | resp, err := pluginRequest(client, capabilitiesPath, nil) 76 | assert.NoError(t, err) 77 | var cResp *volume.CapabilitiesResponse 78 | assert.NoError(t, json.NewDecoder(resp).Decode(&cResp)) 79 | assert.Equal(t, "local", cResp.Capabilities.Scope) 80 | } 81 | 82 | func testCmdVersion(t *testing.T) { 83 | rclone.Version = "TESTING" 84 | 85 | cmd := rclone.NewRootCmd() 86 | b := bytes.NewBufferString("") 87 | cmd.SetOut(b) 88 | cmd.SetArgs([]string{"version"}) 89 | cmd.Execute() 90 | out, err := ioutil.ReadAll(b) 91 | assert.NoError(t, err) 92 | assert.Equal(t, "\nVersion: TESTING - Branch: - Commit: - BuildTime: \n\n", string(out), "The version returned by CLI is invalid") 93 | } 94 | 95 | func pluginRequest(client *http.Client, method string, req interface{}) (io.Reader, error) { 96 | b, err := json.Marshal(req) 97 | if err != nil { 98 | return nil, err 99 | } 100 | if req == nil { 101 | b = []byte{} 102 | } 103 | resp, err := client.Post("http://localhost"+method, "application/json", bytes.NewReader(b)) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | return resp.Body, nil 109 | } 110 | -------------------------------------------------------------------------------- /rclone/rclone.go: -------------------------------------------------------------------------------- 1 | package rclone 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/docker/go-plugins-helpers/volume" 9 | "github.com/rs/zerolog" 10 | "github.com/rs/zerolog/log" 11 | "github.com/spf13/cobra" 12 | 13 | "github.com/sapk/docker-volume-rclone/rclone/driver" 14 | ) 15 | 16 | const ( 17 | //VerboseFlag flag to set more verbose level 18 | VerboseFlag = "verbose" 19 | //BasedirFlag flag to set the basedir of mounted volumes 20 | BasedirFlag = "basedir" 21 | longHelp = ` 22 | docker-volume-rclone (Rclone Volume Driver Plugin) 23 | Provides docker volume support for Rclone. 24 | == Version: %s - Branch: %s - Commit: %s - BuildTime: %s == 25 | ` 26 | ) 27 | 28 | var ( 29 | //Version version of running code 30 | Version string 31 | //Branch branch of running code 32 | Branch string 33 | //Commit commit of running code 34 | Commit string 35 | //BuildTime build time of running code 36 | BuildTime string 37 | //PluginAlias plugin alias name in docker 38 | PluginAlias = "rclone" 39 | baseDir = "" 40 | daemonCmd = &cobra.Command{ 41 | Use: "daemon", 42 | Short: "Run listening volume drive deamon to listen for mount request", 43 | Run: DaemonStart, 44 | } 45 | versionCmd = &cobra.Command{ 46 | Use: "version", 47 | Short: "Display current version and build date", 48 | RunE: func(cmd *cobra.Command, args []string) error { 49 | _, err := fmt.Fprintf(cmd.OutOrStdout(), "\nVersion: %s - Branch: %s - Commit: %s - BuildTime: %s\n\n", Version, Branch, Commit, BuildTime) 50 | return err 51 | }, 52 | } 53 | ) 54 | 55 | //NewRootCmd setup the cobra root command 56 | func NewRootCmd() *cobra.Command { 57 | rootCmd := &cobra.Command{ 58 | Use: "docker-volume-rclone", 59 | Short: "Rclone - Docker volume driver plugin", 60 | Long: longHelp, 61 | PersistentPreRun: setupLogger, 62 | } 63 | rootCmd.PersistentFlags().BoolP(VerboseFlag, "v", os.Getenv("DEBUG") == "1", "Turns on verbose logging") 64 | rootCmd.PersistentFlags().StringVarP(&baseDir, BasedirFlag, "b", filepath.Join(volume.DefaultDockerRootDirectory, PluginAlias), "Mounted volume base directory") 65 | 66 | rootCmd.Long = fmt.Sprintf(longHelp, Version, Branch, Commit, BuildTime) 67 | rootCmd.AddCommand(versionCmd, daemonCmd) 68 | 69 | return rootCmd 70 | } 71 | 72 | //Start start the program 73 | func Start() { 74 | rootCmd := NewRootCmd() 75 | if err := rootCmd.Execute(); err != nil { 76 | log.Fatal().Err(err) 77 | } 78 | } 79 | 80 | //DaemonStart Start the deamon 81 | func DaemonStart(cmd *cobra.Command, args []string) { 82 | d := driver.Init(baseDir) 83 | log.Debug().Msgf("driver: %v", d) 84 | h := volume.NewHandler(d) 85 | log.Debug().Msgf("handler: %v", h) 86 | err := h.ServeUnix(PluginAlias, 0) 87 | if err != nil { 88 | log.Debug().Err(err) 89 | } 90 | } 91 | 92 | func setupLogger(cmd *cobra.Command, args []string) { 93 | logger := zerolog.New(cmd.OutOrStdout()) 94 | if verbose, _ := cmd.Flags().GetBool(VerboseFlag); verbose { 95 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 96 | //Activate log to file in debug mode 97 | f, err := os.OpenFile("/var/log/docker-volume-rclone.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 98 | if err != nil { 99 | log.Fatal().Err(err) 100 | } 101 | //log.SetOutput(f) 102 | //logger := zerolog.New(os.Stderr).With().Timestamp().Logger() 103 | logger = zerolog.New(f).With().Timestamp().Logger() 104 | } else { 105 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 106 | } 107 | 108 | log.Logger = logger 109 | } 110 | -------------------------------------------------------------------------------- /support/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG RCLONE_VER=1.53 2 | ARG BUILDPLATFORM=linux/amd64 3 | 4 | FROM --platform=$BUILDPLATFORM golang:alpine AS build-env 5 | 6 | ENV CGO_ENABLED=0 7 | ARG TARGETPLATFORM=linux/amd64 8 | 9 | COPY . /docker-volume-rclone 10 | WORKDIR /docker-volume-rclone 11 | 12 | RUN apk add --no-cache make git 13 | RUN GOARCH=$(echo $TARGETPLATFORM | cut -d '/' -f2) make clean build 14 | 15 | FROM rclone/rclone:$RCLONE_VER 16 | LABEL maintainer="Antoine GIRARD " 17 | 18 | RUN apk add --no-cache bash \ 19 | && mkdir -p /var/lib/docker-volumes/rclone /etc/docker-volumes/rclone /var/cache/rclone \ 20 | && ln -s /usr/local/bin/rclone /usr/bin/rclone 21 | COPY --from=build-env /docker-volume-rclone/docker-volume-rclone /usr/local/bin/docker-volume-rclone 22 | 23 | RUN /usr/local/bin/docker-volume-rclone version 24 | 25 | ENTRYPOINT [ "/usr/local/bin/docker-volume-rclone" ] 26 | CMD [ "daemon" ] 27 | -------------------------------------------------------------------------------- /support/how-to/nextcloud.md: -------------------------------------------------------------------------------- 1 | Guide to mount a nextcloud webdav access to docker container. 2 | 3 | ### 1. Installation of rclone 4 | Go to https://rclone.org/install/ 5 | 6 | 7 | ### 2. Configure webdav remote 8 | ``` 9 | rclone config 10 | e/n/d/r/c/s/q> n 11 | name> nextcloud 12 | Storage> webdav 13 | url> https://your.nextcloud.com/remote.php/webdav/ 14 | vendor> 1 15 | user> your_username 16 | y/g/n> y 17 | password: /* use an application password under securty of your account settings */ 18 | password: /* repeat */ 19 | y/e/d> y 20 | e/n/d/r/c/s/q> q 21 | 22 | rclone listremotes 23 | nextcloud: 24 | ``` 25 | Full details : https://rclone.org/webdav/ 26 | 27 | 28 | ### 3. Installation of plugin 29 | ``` 30 | docker plugin install sapk/plugin-rclone 31 | Plugin "sapk/plugin-rclone" is requesting the following privileges: 32 | - network: [host] 33 | - device: [/dev/fuse] 34 | - capabilities: [CAP_SYS_ADMIN] 35 | Do you grant the above permissions? [y/N] y 36 | ``` 37 | 38 | ### 4. Configure the volume 39 | ``` 40 | docker volume create --driver sapk/plugin-rclone --opt config="$(base64 ~/.config/rclone/rclone.conf)" --opt remote=nextcloud: --name nextcloud 41 | ``` 42 | 43 | ### 4. Start the container and enjoy ! 44 | ``` 45 | docker run -v nextcloud:/mnt --rm -ti ubuntu 46 | ``` 47 | 48 | --------------------------------------------------------------------------------