├── .dockerignore ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE.md ├── .gitignore ├── .gitmodules ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── common └── common_driver.go ├── config.json ├── gluster ├── driver │ ├── driver.go │ ├── driver_test.go │ ├── tools.go │ └── tools_test.go ├── gluster.go └── integration │ ├── docker │ └── gluster-cluster │ │ ├── docker-compose.yml │ │ └── glusterd.vol │ └── integration_test.go ├── main.go └── support └── docker └── Dockerfile /.dockerignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.git* 3 | plugin/ 4 | support/ 5 | 6 | /docker-volume-gluster 7 | /docker-volume-gluster* 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 | -------------------------------------------------------------------------------- /.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-gluster 27 | /docker-volume-gluster-* 28 | /coverage.* 29 | /build 30 | /.gopath 31 | /plugin 32 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/github.com/Sirupsen/logrus"] 2 | path = vendor/github.com/Sirupsen/logrus 3 | url = https://github.com/Sirupsen/logrus 4 | [submodule "vendor/github.com/docker/go-plugins-helpers"] 5 | path = vendor/github.com/docker/go-plugins-helpers 6 | url = https://github.com/docker/go-plugins-helpers 7 | [submodule "vendor/github.com/coreos/go-systemd"] 8 | path = vendor/github.com/coreos/go-systemd 9 | url = https://github.com/coreos/go-systemd 10 | [submodule "vendor/github.com/docker/go-connections"] 11 | path = vendor/github.com/docker/go-connections 12 | url = https://github.com/docker/go-connections 13 | [submodule "vendor/golang.org/x/net"] 14 | path = vendor/golang.org/x/net 15 | url = https://go.googlesource.com/net 16 | [submodule "vendor/github.com/spf13/cobra"] 17 | path = vendor/github.com/spf13/cobra 18 | url = https://github.com/spf13/cobra 19 | [submodule "vendor/github.com/spf13/pflag"] 20 | path = vendor/github.com/spf13/pflag 21 | url = https://github.com/spf13/pflag 22 | [submodule "vendor/github.com/spf13/viper"] 23 | path = vendor/github.com/spf13/viper 24 | url = https://github.com/spf13/viper 25 | [submodule "vendor/github.com/fsnotify/fsnotify"] 26 | path = vendor/github.com/fsnotify/fsnotify 27 | url = https://github.com/fsnotify/fsnotify 28 | [submodule "vendor/golang.org/x/sys"] 29 | path = vendor/golang.org/x/sys 30 | url = https://go.googlesource.com/sys 31 | [submodule "vendor/github.com/hashicorp/hcl"] 32 | path = vendor/github.com/hashicorp/hcl 33 | url = https://github.com/hashicorp/hcl 34 | [submodule "vendor/github.com/magiconair/properties"] 35 | path = vendor/github.com/magiconair/properties 36 | url = https://github.com/magiconair/properties 37 | [submodule "vendor/github.com/mitchellh/mapstructure"] 38 | path = vendor/github.com/mitchellh/mapstructure 39 | url = https://github.com/mitchellh/mapstructure 40 | [submodule "vendor/github.com/pelletier/go-toml"] 41 | path = vendor/github.com/pelletier/go-toml 42 | url = https://github.com/pelletier/go-toml 43 | [submodule "vendor/github.com/pelletier/go-buffruneio"] 44 | path = vendor/github.com/pelletier/go-buffruneio 45 | url = https://github.com/pelletier/go-buffruneio 46 | [submodule "vendor/github.com/spf13/afero"] 47 | path = vendor/github.com/spf13/afero 48 | url = https://github.com/spf13/afero 49 | [submodule "vendor/golang.org/x/text"] 50 | path = vendor/golang.org/x/text 51 | url = https://go.googlesource.com/text 52 | [submodule "vendor/github.com/spf13/cast"] 53 | path = vendor/github.com/spf13/cast 54 | url = https://github.com/spf13/cast 55 | [submodule "vendor/github.com/spf13/jwalterweatherman"] 56 | path = vendor/github.com/spf13/jwalterweatherman 57 | url = https://github.com/spf13/jwalterweatherman 58 | [submodule "vendor/gopkg.in/yaml.v2"] 59 | path = vendor/gopkg.in/yaml.v2 60 | url = https://gopkg.in/yaml.v2 61 | [submodule "vendor/golang.org/x/crypto"] 62 | path = vendor/golang.org/x/crypto 63 | url = https://go.googlesource.com/crypto 64 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: true 3 | services: 4 | - docker 5 | addons: 6 | apt: 7 | packages: 8 | - upx-ucl 9 | go: 10 | - 1.8 11 | - 1.9 12 | - "1.10" 13 | 14 | before_install: 15 | - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 16 | - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" 17 | - sudo apt-get update 18 | - sudo apt-get install -qq pkg-config fuse 19 | - sudo modprobe fuse 20 | - sudo chmod 666 /dev/fuse 21 | - sudo apt-get -y install docker-ce glusterfs-client 22 | install: 23 | - make dev-deps 24 | 25 | script: 26 | - make lint 27 | - make build 28 | - make test-unit 29 | # - sudo -E bash -c 'eval "$(gimme $TRAVIS_GO_VERSION)" && make test' 30 | # - sudo ls -lah /var/log/glusterfs/ 31 | # - sudo cat /var/log/glusterfs/* 32 | - ./docker-volume-gluster 33 | 34 | after_success: 35 | - bash <(curl -s https://codecov.io/bash) -f coverage.* 36 | 37 | before_deploy: 38 | - make compress 39 | - make release 40 | - docker --version 41 | - make docker-plugin 42 | - PLUGIN_TAG=$(git describe --tags --abbrev=0) make docker-plugin 43 | - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"; fi 44 | - make docker-plugin-push 45 | - PLUGIN_TAG=$(git describe --tags --abbrev=0) make docker-plugin-push 46 | 47 | deploy: 48 | provider: releases 49 | api_key: 50 | secure: SAD6luuJhgzS7ZavymCxLaTlCbxp9VQax0uf4ozWByt0uwsLKtDM4Je4c9z9bx40PmEBKOpSoso4umL4AcY1yrQ2UTMJD+GxedmqDnge2ibfhHZMTxeDkLF3JLsACfQmibNzN/+b9nNdgyzQYnB4+OvKOmEvtHWwNP7qOuU+9QTNK/rNt5MBXk29aJFL1/t3oIvCfHl1D96pssE6uVURcXiPGkgNcSLBUewYwPwqjGqgG/xu/lgQEwpW6AE4OfwJLpdGMR/V1DUNMNs47Gt7Vh+PBRm/M7cZUYyc+CRuXuSM+PKr9xlH8mFHu6IrcM4n5Sy94A7ldB5dfn9HA+WLXEOhSej3NB06sFkjITnKSWCVCGVzzXuH5CMiwBDRKmhFLunFe9Wcz52W2wIumesaNuZOm9LbiBV47UicggeiZkhar1tvjQoFgw6R57+Wxdhdon7fwVO3euRSH7fm0RyEN1PgO92U5/gHgOO+Khlsl3GPqWNgyqeh0Ej4Ke9mFB0VjO+a8uNAQlrp+v94kQSWqpXUD30/fnZhu+sBURJ1pikH/WAhcnKEvYk7Kq5/kSuYqO0dxxEDrHh00Gwo/uHhhnOp3BNe/v1/TseSS4FOxaLrbTKCDRU+B38bsi8FoZ7J68xK9dT19TF+6ZXqka1LNIyV4ITCjkRenHMfCWWrF78= 51 | file_glob: true 52 | file: 53 | - "./docker-volume-gluster" 54 | - build/*.tar.gz 55 | skip_cleanup: true 56 | on: 57 | tags: true 58 | -------------------------------------------------------------------------------- /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 | APP_NAME=docker-volume-gluster 4 | APP_VERSION=$(shell git describe --tags --abbrev=0) 5 | APP_USERREPO=github.com/sapk 6 | APP_PACKAGE=$(APP_USERREPO)/$(APP_NAME) 7 | 8 | 9 | PLUGIN_USER ?= sapk 10 | PLUGIN_NAME ?= plugin-gluster 11 | PLUGIN_TAG ?= latest 12 | PLUGIN_IMAGE ?= $(PLUGIN_USER)/$(PLUGIN_NAME):$(PLUGIN_TAG) 13 | 14 | GIT_HASH=$(shell git rev-parse --short HEAD) 15 | GIT_BRANCH=$(shell git rev-parse --abbrev-ref HEAD) 16 | DATE := $(shell date -u '+%Y-%m-%d-%H%M-UTC') 17 | PWD=$(shell pwd) 18 | 19 | ARCHIVE=$(APP_NAME)-$(APP_VERSION)-$(GIT_HASH).tar.gz 20 | #DEPS = $(go list -f '{{range .TestImports}}{{.}} {{end}}' ./...) 21 | LDFLAGS = \ 22 | -s -w \ 23 | -X main.Version=$(APP_VERSION) -X main.Branch=$(GIT_BRANCH) -X main.Commit=$(GIT_HASH) -X main.BuildTime=$(DATE) 24 | 25 | FAKE_GOPATH = $(PWD)/.gopath 26 | FAKE_PACKAGE = $(FAKE_GOPATH)/src/$(APP_PACKAGE) 27 | 28 | GO15VENDOREXPERIMENT=1 29 | DOC_PORT = 6060 30 | #GOOS=linux 31 | 32 | ERROR_COLOR=\033[31;01m 33 | NO_COLOR=\033[0m 34 | OK_COLOR=\033[32;01m 35 | WARN_COLOR=\033[33;01m 36 | 37 | 38 | all: build compress done 39 | 40 | build: deps clean format compile 41 | 42 | docker-plugin: docker-rootfs docker-plugin-create 43 | 44 | docker-image: 45 | @echo -e "$(OK_COLOR)==> Docker build image : ${PLUGIN_IMAGE} $(NO_COLOR)" 46 | docker build -t ${PLUGIN_IMAGE} -f support/docker/Dockerfile . 47 | 48 | docker-rootfs: docker-image 49 | @echo -e "$(OK_COLOR)==> create rootfs directory in ./plugin/rootfs$(NO_COLOR)" 50 | @mkdir -p ./plugin/rootfs 51 | @cntr=${PLUGIN_USER}-${PLUGIN_NAME}-${PLUGIN_TAG}-$$(date +'%Y%m%d-%H%M%S'); \ 52 | docker create --name $$cntr ${PLUGIN_IMAGE}; \ 53 | docker export $$cntr | tar -x -C ./plugin/rootfs; \ 54 | docker rm -vf $$cntr 55 | @echo -e "### copy config.json to ./plugin/$(NO_COLOR)" 56 | @cp config.json ./plugin/ 57 | 58 | docker-plugin-create: 59 | @echo -e "$(OK_COLOR)==> Remove existing plugin : ${PLUGIN_IMAGE} if exists$(NO_COLOR)" 60 | @docker plugin rm -f ${PLUGIN_IMAGE} || true 61 | @echo -e "$(OK_COLOR)==> Create new plugin : ${PLUGIN_IMAGE} from ./plugin$(NO_COLOR)" 62 | docker plugin create ${PLUGIN_IMAGE} ./plugin 63 | 64 | docker-plugin-push: 65 | @echo -e "$(OK_COLOR)==> push plugin : ${PLUGIN_IMAGE}$(NO_COLOR)" 66 | docker plugin push ${PLUGIN_IMAGE} 67 | 68 | docker-plugin-enable: 69 | @echo -e "$(OK_COLOR)==> Enable plugin ${PLUGIN_IMAGE}$(NO_COLOR)" 70 | docker plugin enable ${PLUGIN_IMAGE} 71 | 72 | set-build: 73 | @if [ ! -d $(PWD)/.gopath/src/$(APP_USERREPO) ]; then mkdir -p $(PWD)/.gopath/src/$(APP_USERREPO); fi 74 | @if [ ! -d $(PWD)/.gopath/src/$(APP_PACKAGE) ]; then ln -s $(PWD) $(PWD)/.gopath/src/$(APP_PACKAGE); fi 75 | 76 | compile: set-build 77 | @echo -e "$(OK_COLOR)==> Building...$(NO_COLOR)" 78 | cd $(FAKE_PACKAGE) && GOPATH=$(FAKE_GOPATH) go build -v -ldflags "$(LDFLAGS)" 79 | 80 | release: clean set-build deps format 81 | @mkdir build 82 | @echo -e "$(OK_COLOR)==> Building for linux 32 ...$(NO_COLOR)" 83 | cd $(FAKE_PACKAGE) && GOPATH=$(FAKE_GOPATH) CGO_ENABLED=0 GOOS=linux GOARCH=386 go build -o build/${APP_NAME}-linux-386 -ldflags "$(LDFLAGS)" 84 | @echo -e "$(OK_COLOR)==> Trying to compress binary ...$(NO_COLOR)" 85 | @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)" 86 | 87 | @echo -e "$(OK_COLOR)==> Building for linux 64 ...$(NO_COLOR)" 88 | cd $(FAKE_PACKAGE) && GOPATH=$(FAKE_GOPATH) GO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o build/${APP_NAME}-linux-amd64 -ldflags "$(LDFLAGS)" 89 | @echo -e "$(OK_COLOR)==> Trying to compress binary ...$(NO_COLOR)" 90 | @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)" 91 | 92 | @echo -e "$(OK_COLOR)==> Building for linux arm ...$(NO_COLOR)" 93 | cd $(FAKE_PACKAGE) && GOPATH=$(FAKE_GOPATH) CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -o build/${APP_NAME}-linux-armv6 -ldflags "$(LDFLAGS)" 94 | @echo -e "$(OK_COLOR)==> Trying to compress binary ...$(NO_COLOR)" 95 | @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)" 96 | 97 | @echo -e "$(OK_COLOR)==> Building for darwin32 ...$(NO_COLOR)" 98 | cd $(FAKE_PACKAGE) && GOPATH=$(FAKE_GOPATH) CGO_ENABLED=0 GOOS=darwin GOARCH=386 go build -o build/${APP_NAME}-darwin-386 -ldflags "$(LDFLAGS)" 99 | @echo -e "$(OK_COLOR)==> Trying to compress binary ...$(NO_COLOR)" 100 | @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)" 101 | 102 | @echo -e "$(OK_COLOR)==> Building for darwin64 ...$(NO_COLOR)" 103 | cd $(FAKE_PACKAGE) && GOPATH=$(FAKE_GOPATH) CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o build/${APP_NAME}-darwin-amd64 -ldflags "$(LDFLAGS)" 104 | @echo -e "$(OK_COLOR)==> Trying to compress binary ...$(NO_COLOR)" 105 | @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)" 106 | 107 | # @echo -e "$(OK_COLOR)==> Building for win32 ...$(NO_COLOR)" 108 | # cd $(FAKE_PACKAGE) && GOPATH=$(FAKE_GOPATH) CGO_ENABLED=0 GOOS=windows GOARCH=386 go build -o build/${APP_NAME}-win-386 -ldflags "$(LDFLAGS)" 109 | # @echo -e "$(OK_COLOR)==> Trying to compress binary ...$(NO_COLOR)" 110 | # @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)" 111 | 112 | # @echo -e "$(OK_COLOR)==> Building for win64 ...$(NO_COLOR)" 113 | # cd $(FAKE_PACKAGE) && GOPATH=$(FAKE_GOPATH) CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o build/${APP_NAME}-win-amd64 -ldflags "$(LDFLAGS)" 114 | # @echo -e "$(OK_COLOR)==> Trying to compress binary ...$(NO_COLOR)" 115 | # @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)" 116 | 117 | @echo -e "$(OK_COLOR)==> Archiving ...$(NO_COLOR)" 118 | @tar -zcvf build/$(ARCHIVE) LICENSE README.md build/$(APP_NAME)-* 119 | 120 | clean: 121 | @if [ -x $(APP_NAME) ]; then rm $(APP_NAME); fi 122 | @if [ -d build ]; then rm -R build; fi 123 | @if [ -d $(FAKE_GOPATH) ]; then rm -R $(FAKE_GOPATH); fi 124 | @rm -rf ./plugin 125 | 126 | compress: 127 | @echo -e "$(OK_COLOR)==> Trying to compress binary ...$(NO_COLOR)" 128 | @upx --brute $(APP_NAME) || upx-ucl --brute $(APP_NAME) || echo -e "$(WARN_COLOR)==> No tools found to compress binary.$(NO_COLOR)" 129 | 130 | format: 131 | @echo -e "$(OK_COLOR)==> Formatting...$(NO_COLOR)" 132 | go fmt ./gluster/... 133 | 134 | test: test-unit test-integration 135 | @echo -e "$(OK_COLOR)==> Running test...$(NO_COLOR)" 136 | gocovmerge coverage.unit.out coverage.inte.out > coverage.all 137 | # go tool cover -html=coverage.all -o coverage.html 138 | 139 | test-unit: dev-deps deps format 140 | @echo -e "$(OK_COLOR)==> Running unit tests...$(NO_COLOR)" 141 | go vet ./gluster/... || true 142 | go test -v -race -coverprofile=coverage.unit.out -covermode=atomic ./gluster/driver 143 | 144 | test-integration: dev-deps deps format 145 | @echo -e "$(OK_COLOR)==> Running integration tests...$(NO_COLOR)" 146 | go test -v -timeout 1h -race -coverprofile=coverage.inte.out -covermode=atomic -coverpkg ./gluster/driver ./gluster/integration 147 | 148 | docs: 149 | @echo -e "$(OK_COLOR)==> Serving docs at http://localhost:$(DOC_PORT).$(NO_COLOR)" 150 | @godoc -http=:$(DOC_PORT) 151 | 152 | lint: dev-deps 153 | gometalinter --deadline=5m --concurrency=2 --vendor --disable=gotype --errors ./... 154 | gometalinter --deadline=5m --concurrency=2 --vendor --disable=gotype ./... || echo "Something could be improved !" 155 | # gometalinter --deadline=5m --concurrency=2 --vendor ./... # disable gotype temporary 156 | 157 | dev-deps: 158 | @echo -e "$(OK_COLOR)==> Installing developement dependencies...$(NO_COLOR)" 159 | @go get github.com/nsf/gocode 160 | @go get github.com/alecthomas/gometalinter 161 | @go get github.com/dpw/vendetta #Vendoring 162 | @go get github.com/wadey/gocovmerge 163 | @$(GOPATH)/bin/gometalinter --install > /dev/null 164 | 165 | update-dev-deps: 166 | @echo -e "$(OK_COLOR)==> Installing/Updating developement dependencies...$(NO_COLOR)" 167 | go get -u github.com/nsf/gocode 168 | go get -u github.com/alecthomas/gometalinter 169 | go get -u github.com/dpw/vendetta #Vendoring 170 | go get -u github.com/wadey/gocovmerge 171 | $(GOPATH)/bin/gometalinter --install --update 172 | 173 | deps: 174 | @echo -e "$(OK_COLOR)==> Installing dependencies ...$(NO_COLOR)" 175 | @git submodule update --init --recursive 176 | # @$(GOPATH)/bin/vendetta -n $(APP_PACKAGE) 177 | # @go get -d -v ./... 178 | 179 | update-deps: dev-deps 180 | @echo -e "$(OK_COLOR)==> Updating all dependencies ...$(NO_COLOR)" 181 | $(GOPATH)/bin/vendetta -n $(APP_PACKAGE) -u 182 | #@go get -d -v -u ./... 183 | 184 | 185 | done: 186 | @echo -e "$(OK_COLOR)==> Done.$(NO_COLOR)" 187 | 188 | .PHONY: all build compile clean compress format test docs lint dev-deps update-dev-deps deps update-deps done 189 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # docker-volume-gluster [![License](https://img.shields.io/badge/license-MIT-red.svg)](https://github.com/sapk/docker-volume-gluster/blob/master/LICENSE) ![Project Status](http://img.shields.io/badge/status-alpha-red.svg) 2 | [![GitHub release](https://img.shields.io/github/release/sapk/docker-volume-gluster.svg)](https://github.com/sapk/docker-volume-gluster/releases) [![Go Report Card](https://goreportcard.com/badge/github.com/sapk/docker-volume-gluster)](https://goreportcard.com/report/github.com/sapk/docker-volume-gluster) 3 | [![codecov](https://codecov.io/gh/sapk/docker-volume-gluster/branch/master/graph/badge.svg)](https://codecov.io/gh/sapk/docker-volume-gluster) 4 | master : [![Travis master](https://api.travis-ci.org/sapk/docker-volume-gluster.svg?branch=master)](https://travis-ci.org/sapk/docker-volume-gluster) develop : [![Travis develop](https://api.travis-ci.org/sapk/docker-volume-gluster.svg?branch=develop)](https://travis-ci.org/sapk/docker-volume-gluster) 5 | 6 | 7 | Use GlusterFS as a backend for docker volume 8 | 9 | Status : **proof of concept (working)** 10 | 11 | Use GlusterFS cli in the plugin container so it depend on fuse on the host. 12 | 13 | ## Docker plugin (New & Easy method) [![Docker Pulls](https://img.shields.io/docker/pulls/sapk/plugin-gluster.svg)](https://hub.docker.com/r/sapk/plugin-gluster) [![ImageLayers Size](https://img.shields.io/imagelayers/image-size/sapk/plugin-gluster/latest.svg)](https://hub.docker.com/r/sapk/plugin-gluster) 14 | ``` 15 | docker plugin install sapk/plugin-gluster 16 | docker volume create --driver sapk/plugin-gluster --opt voluri=":" --name test 17 | docker run -v test:/mnt --rm -ti ubuntu 18 | ``` 19 | 20 | ## Create and Mount volume 21 | ``` 22 | docker volume create --driver sapk/plugin-gluster --opt voluri=",,:" --name test 23 | docker run -v test:/mnt --rm -ti ubuntu 24 | ``` 25 | 26 | ## Docker-compose 27 | ``` 28 | volumes: 29 | some_vol: 30 | driver: sapk/plugin-gluster 31 | driver_opts: 32 | voluri: ":" 33 | ``` 34 | 35 | 36 | ## Additionnal docker-plugin config 37 | ``` 38 | docker plugin disable sapk/plugin-gluster 39 | 40 | docker plugin set sapk/plugin-gluster DEBUG=1 #Activate --verbose 41 | docker plugin set sapk/plugin-gluster MOUNT_UNIQ=1 #Activate --mount-uniq 42 | 43 | docker plugin enable sapk/plugin-gluster 44 | ``` 45 | 46 | 47 | 48 | ## Legacy plugin installation 49 | For Docker version 1.12 or below, the managed plugin system is not supported. This also happens if the plugin is not installed via 50 | `docker plugin install`. 51 | [Docker's new plugin system](https://docs.docker.com/engine/extend/) is the preferred way to add drivers and plugins, where the plugin is just 52 | an image downloaded from registry containing the executable and needed configuration files. You can run both legacy and new plugins 53 | in Docker versions above 1.12, but be aware that legacy plugins will not show up on `docker plugin ls`. They will be listed instead under `plugins` on `docker info`. 54 | 55 | That way, the driver's name will be just `gluster` (in both the CLI and Compose environments): 56 | 57 | #### Build 58 | ``` 59 | make 60 | ``` 61 | 62 | #### Start daemon 63 | ``` 64 | ./docker-volume-gluster daemon 65 | OR in a docker container 66 | 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/gluster:/var/lib/docker-volumes/gluster:shared sapk/docker-volume-gluster 67 | ``` 68 | 69 | For more advance params : ```./docker-volume-gluster --help OR ./docker-volume-gluster daemon --help``` 70 | ``` 71 | Run listening volume drive deamon to listen for mount request 72 | 73 | Usage: 74 | docker-volume-gluster daemon [flags] 75 | 76 | Flags: 77 | -h, --help help for daemon 78 | --mount-uniq Set mountpoint based on definition and not the name of volume 79 | 80 | Global Flags: 81 | -b, --basedir string Mounted volume base directory (default "/var/lib/docker-volumes/gluster") 82 | -v, --verbose Turns on verbose logging 83 | ``` 84 | 85 | #### Create and Mount volume 86 | ``` 87 | docker volume create --driver gluster --opt voluri=":" --name test 88 | docker run -v test:/mnt --rm -ti ubuntu 89 | ``` 90 | 91 | 92 | 93 | ## Performances : 94 | As tested [here](https://github.com/sapk/docker-volume-gluster/issues/10#issuecomment-350126471), this plugin provide same performances as a gluster volume mounted on host via docker bind mount. 95 | 96 | ## Inspired from : 97 | - https://github.com/ContainX/docker-volume-netshare/ 98 | - https://github.com/vieux/docker-volume-sshfs/ 99 | - https://github.com/sapk/docker-volume-gvfs 100 | - https://github.com/calavera/docker-volume-glusterfs 101 | - https://github.com/codedellemc/rexray 102 | -------------------------------------------------------------------------------- /common/common_driver.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "sync" 8 | 9 | log "github.com/Sirupsen/logrus" 10 | 11 | "github.com/docker/go-plugins-helpers/volume" 12 | ) 13 | 14 | //Driver needed interface for some commons interactions 15 | type Driver interface { 16 | GetLock() *sync.RWMutex 17 | GetVolumes() map[string]Volume 18 | GetMounts() map[string]Mount 19 | SaveConfig() error 20 | RunCmd(string) error 21 | } 22 | 23 | //Volume needed interface for some commons interactions 24 | type Volume interface { 25 | increasable 26 | GetMount() string 27 | GetRemote() string 28 | GetStatus() map[string]interface{} 29 | } 30 | 31 | //Mount needed interface for some commons interactions 32 | type Mount interface { 33 | increasable 34 | GetPath() string 35 | } 36 | 37 | type increasable interface { 38 | GetConnections() int 39 | SetConnections(int) 40 | } 41 | 42 | func getMount(d Driver, mPath string) (Mount, error) { 43 | m, ok := d.GetMounts()[mPath] 44 | if !ok { 45 | return nil, fmt.Errorf("mount %s not found", mPath) 46 | } 47 | log.Debugf("Mount found: %s", m) 48 | return m, nil 49 | } 50 | 51 | func getVolumeMount(d Driver, vName string) (Volume, Mount, error) { 52 | v, ok := d.GetVolumes()[vName] 53 | if !ok { 54 | return nil, nil, fmt.Errorf("volume %s not found", vName) 55 | } 56 | log.Debugf("Volume found: %s", v) 57 | m, err := getMount(d, v.GetMount()) 58 | return v, m, err 59 | } 60 | 61 | //List wrapper around github.com/docker/go-plugins-helpers/volume 62 | func List(d Driver) (*volume.ListResponse, error) { 63 | log.Debugf("Entering List") 64 | d.GetLock().Lock() 65 | defer d.GetLock().Unlock() 66 | var vols []*volume.Volume 67 | for name, v := range d.GetVolumes() { 68 | log.Debugf("Volume found: %s", v) 69 | m, err := getMount(d, v.GetMount()) 70 | if err != nil { 71 | return nil, err 72 | } 73 | vols = append(vols, &volume.Volume{Name: name, Status: v.GetStatus(), Mountpoint: m.GetPath()}) 74 | } 75 | return &volume.ListResponse{Volumes: vols}, nil 76 | } 77 | 78 | //Get wrapper around github.com/docker/go-plugins-helpers/volume 79 | func Get(d Driver, vName string) (Volume, Mount, error) { 80 | log.Debugf("Entering Get: name: %s", vName) 81 | d.GetLock().RLock() 82 | defer d.GetLock().RUnlock() 83 | return getVolumeMount(d, vName) 84 | } 85 | 86 | //Remove wrapper around github.com/docker/go-plugins-helpers/volume 87 | func Remove(d Driver, vName string) error { 88 | log.Debugf("Entering Remove: name: %s", vName) 89 | d.GetLock().Lock() 90 | defer d.GetLock().Unlock() 91 | v, m, err := getVolumeMount(d, vName) 92 | if err != nil { 93 | return err 94 | } 95 | if v.GetConnections() == 0 { 96 | if m.GetConnections() == 0 { 97 | if err := os.Remove(m.GetPath()); err != nil && !strings.Contains(err.Error(), "no such file or directory") { 98 | return err 99 | } 100 | delete(d.GetMounts(), v.GetMount()) 101 | } 102 | delete(d.GetVolumes(), vName) 103 | return d.SaveConfig() 104 | } 105 | return fmt.Errorf("volume %s is currently used by a container", vName) 106 | } 107 | 108 | //MountExist wrapper around github.com/docker/go-plugins-helpers/volume 109 | func MountExist(d Driver, vName string) (Volume, Mount, error) { 110 | log.Debugf("Entering MountExist: name: %s", vName) 111 | d.GetLock().Lock() 112 | defer d.GetLock().Unlock() 113 | return getVolumeMount(d, vName) 114 | } 115 | 116 | func SetN(val int, oList ...increasable) { 117 | for _, o := range oList { 118 | o.SetConnections(val) 119 | } 120 | } 121 | 122 | func AddN(val int, oList ...increasable) { 123 | for _, o := range oList { 124 | o.SetConnections(o.GetConnections() + val) 125 | } 126 | } 127 | 128 | //Unmount wrapper around github.com/docker/go-plugins-helpers/volume 129 | func Unmount(d Driver, vName string) error { 130 | log.Debugf("Entering Unmount: name: %s", vName) 131 | d.GetLock().Lock() 132 | defer d.GetLock().Unlock() 133 | v, m, err := getVolumeMount(d, vName) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | if m.GetConnections() <= 1 { 139 | cmd := fmt.Sprintf("/usr/bin/umount %s", m.GetPath()) 140 | if err := d.RunCmd(cmd); err != nil { 141 | return err 142 | } 143 | SetN(0, m, v) 144 | } else { 145 | AddN(-1, m, v) 146 | } 147 | 148 | return d.SaveConfig() 149 | } 150 | 151 | //Capabilities wrapper around github.com/docker/go-plugins-helpers/volume 152 | func Capabilities() *volume.CapabilitiesResponse { 153 | log.Debugf("Entering Capabilities") 154 | return &volume.CapabilitiesResponse{ 155 | Capabilities: volume.Capability{ 156 | Scope: "global", 157 | }, 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "GlusterFS plugin for Docker", 3 | "documentation": "https://docs.docker.com/engine/extend/plugins/", 4 | "entrypoint": [ 5 | "/usr/bin/docker-volume-gluster", 6 | "daemon" 7 | ], 8 | "env": [ 9 | { 10 | "name": "DEBUG", 11 | "settable": [ 12 | "value" 13 | ], 14 | "value": "0" 15 | }, 16 | { 17 | "name": "MOUNT_UNIQ", 18 | "settable": [ 19 | "value" 20 | ], 21 | "value": "0" 22 | } 23 | ], 24 | "Args": { 25 | "Description": "Arguments to be passed to the plugin", 26 | "Name": "args", 27 | "Settable": ["value"], 28 | "Value": [] 29 | }, 30 | "interface": { 31 | "socket": "gluster.sock", 32 | "types": [ 33 | "docker.volumedriver/1.0" 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/gluster" 50 | } 51 | -------------------------------------------------------------------------------- /gluster/driver/driver.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/sapk/docker-volume-gluster/common" 11 | 12 | log "github.com/Sirupsen/logrus" 13 | "github.com/docker/go-plugins-helpers/volume" 14 | "github.com/spf13/viper" 15 | ) 16 | 17 | var ( 18 | //MountTimeout timeout before killing a mount try in seconds 19 | MountTimeout = 30 20 | //CfgVersion current config version compat 21 | CfgVersion = 1 22 | //CfgFolder config folder 23 | CfgFolder = "/etc/docker-volumes/gluster/" 24 | ) 25 | 26 | type GlusterMountpoint struct { 27 | Path string `json:"path"` 28 | Connections int `json:"connections"` 29 | } 30 | 31 | func (d *GlusterMountpoint) GetPath() string { 32 | return d.Path 33 | } 34 | 35 | func (d *GlusterMountpoint) GetConnections() int { 36 | return d.Connections 37 | } 38 | 39 | func (d *GlusterMountpoint) SetConnections(n int) { 40 | d.Connections = n 41 | } 42 | 43 | type GlusterVolume struct { 44 | VolumeURI string `json:"voluri"` 45 | Mount string `json:"mount"` 46 | Connections int `json:"connections"` 47 | } 48 | 49 | func (v *GlusterVolume) GetMount() string { 50 | return v.Mount 51 | } 52 | 53 | func (v *GlusterVolume) GetRemote() string { 54 | return v.VolumeURI 55 | } 56 | 57 | func (v *GlusterVolume) GetConnections() int { 58 | return v.Connections 59 | } 60 | 61 | func (v *GlusterVolume) SetConnections(n int) { 62 | v.Connections = n 63 | } 64 | 65 | func (v *GlusterVolume) GetStatus() map[string]interface{} { 66 | return map[string]interface{}{ 67 | "TODO": "List", 68 | } 69 | } 70 | 71 | //GlusterDriver the global driver responding to call 72 | type GlusterDriver struct { 73 | lock sync.RWMutex 74 | root string 75 | mountUniqName bool 76 | persitence *viper.Viper 77 | volumes map[string]*GlusterVolume 78 | mounts map[string]*GlusterMountpoint 79 | } 80 | 81 | func (d *GlusterDriver) GetVolumes() map[string]common.Volume { 82 | vi := make(map[string]common.Volume, len(d.volumes)) 83 | for k, i := range d.volumes { 84 | vi[k] = i 85 | } 86 | return vi 87 | } 88 | 89 | func (d *GlusterDriver) GetMounts() map[string]common.Mount { 90 | mi := make(map[string]common.Mount, len(d.mounts)) 91 | for k, i := range d.mounts { 92 | mi[k] = i 93 | } 94 | return mi 95 | } 96 | 97 | func (d *GlusterDriver) GetLock() *sync.RWMutex { 98 | return &d.lock 99 | } 100 | 101 | //Init start all needed deps and serve response to API call 102 | func Init(root string, mountUniqName bool) *GlusterDriver { 103 | log.Debugf("Init gluster driver at %s, UniqName: %v", root, mountUniqName) 104 | d := &GlusterDriver{ 105 | root: root, 106 | mountUniqName: mountUniqName, 107 | persitence: viper.New(), 108 | volumes: make(map[string]*GlusterVolume), 109 | mounts: make(map[string]*GlusterMountpoint), 110 | } 111 | 112 | d.persitence.SetDefault("volumes", map[string]*GlusterVolume{}) 113 | d.persitence.SetConfigName("persistence") 114 | d.persitence.SetConfigType("json") 115 | d.persitence.AddConfigPath(CfgFolder) 116 | if err := d.persitence.ReadInConfig(); err != nil { // Handle errors reading the config file 117 | log.Warn("No persistence file found, I will start with a empty list of volume.", err) 118 | } else { 119 | log.Debug("Retrieving volume list from persistence file.") 120 | 121 | var version int 122 | err := d.persitence.UnmarshalKey("version", &version) 123 | if err != nil || version != CfgVersion { 124 | log.Warn("Unable to decode version of persistence, %v", err) 125 | d.volumes = make(map[string]*GlusterVolume) 126 | d.mounts = make(map[string]*GlusterMountpoint) 127 | } else { //We have the same version 128 | err := d.persitence.UnmarshalKey("volumes", &d.volumes) 129 | if err != nil { 130 | log.Warn("Unable to decode into struct -> start with empty list, %v", err) 131 | d.volumes = make(map[string]*GlusterVolume) 132 | } 133 | err = d.persitence.UnmarshalKey("mounts", &d.mounts) 134 | if err != nil { 135 | log.Warn("Unable to decode into struct -> start with empty list, %v", err) 136 | d.mounts = make(map[string]*GlusterMountpoint) 137 | } 138 | } 139 | } 140 | return d 141 | } 142 | 143 | //Create create and init the requested volume 144 | func (d *GlusterDriver) Create(r *volume.CreateRequest) error { 145 | log.Debugf("Entering Create: name: %s, options %v", r.Name, r.Options) 146 | 147 | if r.Options == nil || r.Options["voluri"] == "" { 148 | return fmt.Errorf("voluri option required") 149 | } 150 | r.Options["voluri"] = strings.Trim(r.Options["voluri"], "\"") 151 | if !isValidURI(r.Options["voluri"]) { 152 | return fmt.Errorf("voluri option is malformated") 153 | } 154 | 155 | d.GetLock().Lock() 156 | defer d.GetLock().Unlock() 157 | 158 | v := &GlusterVolume{ 159 | VolumeURI: r.Options["voluri"], 160 | Mount: getMountName(d, r), 161 | Connections: 0, 162 | } 163 | 164 | if _, ok := d.mounts[v.Mount]; !ok { //This mountpoint doesn't allready exist -> create it 165 | m := &GlusterMountpoint{ 166 | Path: filepath.Join(d.root, v.Mount), 167 | Connections: 0, 168 | } 169 | 170 | _, err := os.Lstat(m.Path) //Create folder if not exist. This will also failed if already exist 171 | if os.IsNotExist(err) { 172 | if err = os.MkdirAll(m.Path, 0700); err != nil { 173 | return err 174 | } 175 | } else if err != nil { 176 | return err 177 | } 178 | isempty, err := isEmpty(m.Path) 179 | if err != nil { 180 | return err 181 | } 182 | if !isempty { 183 | return fmt.Errorf("%v already exist and is not empty !", m.Path) 184 | } 185 | d.mounts[v.Mount] = m 186 | } 187 | 188 | d.volumes[r.Name] = v 189 | log.Debugf("Volume Created: %v", v) 190 | if err := d.SaveConfig(); err != nil { 191 | return err 192 | } 193 | return nil 194 | } 195 | 196 | //List volumes handled by these driver 197 | func (d *GlusterDriver) List() (*volume.ListResponse, error) { 198 | return common.List(d) 199 | } 200 | 201 | //Get get info on the requested volume 202 | func (d *GlusterDriver) Get(r *volume.GetRequest) (*volume.GetResponse, error) { 203 | v, m, err := common.Get(d, r.Name) 204 | if err != nil { 205 | return nil, err 206 | } 207 | return &volume.GetResponse{Volume: &volume.Volume{Name: r.Name, Status: v.GetStatus(), Mountpoint: m.GetPath()}}, nil 208 | } 209 | 210 | //Remove remove the requested volume 211 | func (d *GlusterDriver) Remove(r *volume.RemoveRequest) error { 212 | return common.Remove(d, r.Name) 213 | } 214 | 215 | //Path get path of the requested volume 216 | func (d *GlusterDriver) Path(r *volume.PathRequest) (*volume.PathResponse, error) { 217 | _, m, err := common.Get(d, r.Name) 218 | if err != nil { 219 | return nil, err 220 | } 221 | return &volume.PathResponse{Mountpoint: m.GetPath()}, nil 222 | } 223 | 224 | //Mount mount the requested volume 225 | func (d *GlusterDriver) Mount(r *volume.MountRequest) (*volume.MountResponse, error) { 226 | log.Debugf("Entering Mount: %v", r) 227 | 228 | v, m, err := common.MountExist(d, r.Name) 229 | if err != nil { 230 | return nil, err 231 | } 232 | if m != nil && m.GetConnections() > 0 { 233 | return &volume.MountResponse{Mountpoint: m.GetPath()}, nil 234 | } 235 | 236 | d.GetLock().Lock() 237 | defer d.GetLock().Unlock() 238 | 239 | cmd := fmt.Sprintf("glusterfs %s %s", parseVolURI(v.GetRemote()), m.GetPath()) 240 | //cmd := fmt.Sprintf("/usr/bin/mount -t glusterfs %s %s", v.VolumeURI, m.Path) 241 | //TODO fuseOpts /usr/bin/mount -t glusterfs v.VolumeURI -o fuseOpts v.Mountpoint 242 | if err := d.RunCmd(cmd); err != nil { 243 | return nil, err 244 | } 245 | //time.Sleep(3 * time.Second) 246 | common.AddN(1, v, m) 247 | return &volume.MountResponse{Mountpoint: m.GetPath()}, d.SaveConfig() 248 | } 249 | 250 | //Unmount unmount the requested volume 251 | func (d *GlusterDriver) Unmount(r *volume.UnmountRequest) error { 252 | return common.Unmount(d, r.Name) 253 | } 254 | 255 | //Capabilities Send capabilities of the local driver 256 | func (d *GlusterDriver) Capabilities() *volume.CapabilitiesResponse { 257 | return common.Capabilities() 258 | } 259 | -------------------------------------------------------------------------------- /gluster/driver/driver_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestInit(t *testing.T) { 8 | d := Init("/tmp/test-root", false) 9 | if d == nil { 10 | t.Error("Expected to be not null, got ", d) 11 | } 12 | /* 13 | if _, err := os.Stat(cfgFolder + "gluster-persistence.json"); err != nil { 14 | t.Error("Expected file to exist, got ", err) 15 | } 16 | */ 17 | } 18 | -------------------------------------------------------------------------------- /gluster/driver/tools.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "net/url" 9 | "os" 10 | "os/exec" 11 | "regexp" 12 | "strings" 13 | 14 | log "github.com/Sirupsen/logrus" 15 | "github.com/docker/go-plugins-helpers/volume" 16 | ) 17 | 18 | const ( 19 | validHostnameRegex = `(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])` 20 | ) 21 | 22 | //GlusterPersistence represent struct of persistence file 23 | type GlusterPersistence struct { 24 | Version int `json:"version"` 25 | Volumes map[string]*GlusterVolume `json:"volumes"` 26 | Mounts map[string]*GlusterMountpoint `json:"mounts"` 27 | } 28 | 29 | //SaveConfig stroe config/state in file //TODO put inside common 30 | func (d *GlusterDriver) SaveConfig() error { 31 | fi, err := os.Lstat(CfgFolder) 32 | if os.IsNotExist(err) { 33 | if err = os.MkdirAll(CfgFolder, 0700); err != nil { 34 | return fmt.Errorf("SaveConfig: %s", err) 35 | } 36 | } else if err != nil { 37 | return fmt.Errorf("SaveConfig: %s", err) 38 | } 39 | if fi != nil && !fi.IsDir() { 40 | return fmt.Errorf("SaveConfig: %v already exist and it's not a directory", d.root) 41 | } 42 | b, err := json.Marshal(GlusterPersistence{Version: CfgVersion, Volumes: d.volumes, Mounts: d.mounts}) 43 | if err != nil { 44 | log.Warn("Unable to encode persistence struct, %v", err) 45 | } 46 | //log.Debug("Writing persistence struct, %v", b, d.volumes) 47 | err = ioutil.WriteFile(CfgFolder+"/persistence.json", b, 0600) 48 | if err != nil { 49 | log.Warn("Unable to write persistence struct, %v", err) 50 | return fmt.Errorf("SaveConfig: %s", err) 51 | } 52 | return nil 53 | } 54 | 55 | //RunCmd run deamon in context of this gvfs drive with custome env 56 | func (d *GlusterDriver) RunCmd(cmd string) error { 57 | log.Debugf(cmd) 58 | out, err := exec.Command("sh", "-c", cmd).CombinedOutput() 59 | if err != nil { 60 | log.Debugf("Error: %v", err) 61 | } 62 | log.Debugf("Output: %v", out) 63 | return err 64 | } 65 | 66 | func isValidURI(volURI string) bool { 67 | re := regexp.MustCompile(validHostnameRegex + ":.+") 68 | return re.MatchString(volURI) 69 | } 70 | 71 | func parseVolURI(volURI string) string { 72 | volParts := strings.Split(volURI, ":") 73 | volServers := strings.Split(volParts[0], ",") 74 | return fmt.Sprintf("--volfile-id='%s' -s '%s'", volParts[1], strings.Join(volServers, "' -s '")) 75 | } 76 | 77 | func getMountName(d *GlusterDriver, r *volume.CreateRequest) string { 78 | if d.mountUniqName { 79 | return url.PathEscape(r.Options["voluri"]) 80 | } 81 | return url.PathEscape(r.Name) 82 | } 83 | 84 | //based on: http://stackoverflow.com/questions/30697324/how-to-check-if-directory-on-path-is-empty 85 | func isEmpty(name string) (bool, error) { 86 | f, err := os.Open(name) 87 | if err != nil { 88 | return false, err 89 | } 90 | defer f.Close() 91 | 92 | _, err = f.Readdirnames(1) // Or f.Readdir(1) 93 | if err == io.EOF { 94 | return true, nil 95 | } 96 | return false, err // Either not empty or error, suits both cases 97 | } 98 | -------------------------------------------------------------------------------- /gluster/driver/tools_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/docker/go-plugins-helpers/volume" 7 | ) 8 | 9 | func TestIsValidURI(t *testing.T) { 10 | tt := []struct { 11 | value string 12 | result bool 13 | }{ 14 | {"test", false}, 15 | {"test:volume", true}, 16 | {"test;volume", false}, 17 | {"test,volume", false}, 18 | {"test,test2:volume", true}, 19 | {"192.168.1.1:volume", true}, 20 | {"192.168.1.:volume", false}, 21 | {"192.168.1.1,10.8.0.1:volume", true}, 22 | {"192.168.1.1,test2:volume", true}, 23 | } 24 | 25 | for _, test := range tt { 26 | r := isValidURI(test.value) 27 | if test.result != r { 28 | t.Errorf("Expected to be '%v' , got '%v'", test.result, r) 29 | } 30 | } 31 | } 32 | func TestParseVolURI(t *testing.T) { 33 | tt := []struct { 34 | value string 35 | result string 36 | }{ 37 | {"test:volume", "--volfile-id='volume' -s 'test'"}, 38 | {"test,test2:volume", "--volfile-id='volume' -s 'test' -s 'test2'"}, 39 | {"192.168.1.1:volume", "--volfile-id='volume' -s '192.168.1.1'"}, 40 | {"192.168.1.1,10.8.0.1:volume", "--volfile-id='volume' -s '192.168.1.1' -s '10.8.0.1'"}, 41 | {"192.168.1.1,test2:volume", "--volfile-id='volume' -s '192.168.1.1' -s 'test2'"}, 42 | } 43 | 44 | for _, test := range tt { 45 | r := parseVolURI(test.value) 46 | if test.result != r { 47 | t.Errorf("Expected to be '%v' , got '%v'", test.result, r) 48 | } 49 | } 50 | } 51 | 52 | func TestMountName(t *testing.T) { 53 | name := getMountName(&GlusterDriver{ 54 | mountUniqName: false, 55 | }, &volume.CreateRequest{ 56 | Name: "test", 57 | Options: map[string]string{ 58 | "voluri": "gluster-node:volname", 59 | }, 60 | }) 61 | 62 | if name != "test" { 63 | t.Error("Expected to be test, got ", name) 64 | } 65 | 66 | nameuniq := getMountName(&GlusterDriver{ 67 | mountUniqName: true, 68 | }, &volume.CreateRequest{ 69 | Name: "test", 70 | Options: map[string]string{ 71 | "voluri": "gluster-node:volname", 72 | }, 73 | }) 74 | 75 | if nameuniq != "gluster-node:volname" { 76 | t.Error("Expected to be gluster-node:volname, got ", name) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /gluster/gluster.go: -------------------------------------------------------------------------------- 1 | package gluster 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | 8 | log "github.com/Sirupsen/logrus" 9 | "github.com/docker/go-plugins-helpers/volume" 10 | "github.com/sapk/docker-volume-gluster/gluster/driver" 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | const ( 15 | //VerboseFlag flag to set more verbose level 16 | VerboseFlag = "verbose" 17 | //MountUniqNameFlag flag to set mount point based on definition and not name of volume to not have multile mount of same distant volume 18 | MountUniqNameFlag = "mount-uniq" 19 | //BasedirFlag flag to set the basedir of mounted volumes 20 | BasedirFlag = "basedir" 21 | longHelp = ` 22 | docker-volume-gluster (GlusterFS Volume Driver Plugin) 23 | Provides docker volume support for GlusterFS. 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 = "gluster" 39 | //BaseDir of mounted volumes 40 | BaseDir = "" 41 | fuseOpts = "" 42 | mountUniqName = false 43 | rootCmd = &cobra.Command{ 44 | Use: "docker-volume-gluster", 45 | Short: "GlusterFS - Docker volume driver plugin", 46 | Long: longHelp, 47 | PersistentPreRun: setupLogger, 48 | } 49 | daemonCmd = &cobra.Command{ 50 | Use: "daemon", 51 | Short: "Run listening volume drive deamon to listen for mount request", 52 | Run: DaemonStart, 53 | } 54 | versionCmd = &cobra.Command{ 55 | Use: "version", 56 | Short: "Display current version and build date", 57 | Run: func(cmd *cobra.Command, args []string) { 58 | fmt.Printf("\nVersion: %s - Branch: %s - Commit: %s - BuildTime: %s\n\n", Version, Branch, Commit, BuildTime) 59 | }, 60 | } 61 | ) 62 | 63 | //Init init the program 64 | func Init() { 65 | setupFlags() 66 | rootCmd.Long = fmt.Sprintf(longHelp, Version, Branch, Commit, BuildTime) 67 | rootCmd.AddCommand(versionCmd, daemonCmd) 68 | if err := rootCmd.Execute(); err != nil { 69 | log.Fatal(err) 70 | } 71 | } 72 | 73 | //DaemonStart Start the deamon 74 | func DaemonStart(cmd *cobra.Command, args []string) { 75 | d := driver.Init(BaseDir, mountUniqName) 76 | log.Debug(d) 77 | h := volume.NewHandler(d) 78 | log.Debug(h) 79 | err := h.ServeUnix(PluginAlias, 0) 80 | if err != nil { 81 | log.Debug(err) 82 | } 83 | } 84 | 85 | func setupFlags() { 86 | rootCmd.PersistentFlags().BoolP(VerboseFlag, "v", os.Getenv("DEBUG") == "1", "Turns on verbose logging") 87 | rootCmd.PersistentFlags().StringVarP(&BaseDir, BasedirFlag, "b", filepath.Join(volume.DefaultDockerRootDirectory, PluginAlias), "Mounted volume base directory") 88 | 89 | daemonCmd.Flags().BoolVar(&mountUniqName, MountUniqNameFlag, os.Getenv("MOUNT_UNIQ") == "1", "Set mountpoint based on definition and not the name of volume") 90 | } 91 | 92 | func setupLogger(cmd *cobra.Command, args []string) { 93 | if verbose, _ := cmd.Flags().GetBool(VerboseFlag); verbose { 94 | log.SetLevel(log.DebugLevel) 95 | log.Debugf("Debug mode on") 96 | } else { 97 | log.SetLevel(log.InfoLevel) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /gluster/integration/docker/gluster-cluster/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | node-1: 5 | image: sapk/glusterfs 6 | # devices: 7 | # - /dev/fuse:/dev/fuse 8 | cap_add: 9 | - SYS_ADMIN 10 | volumes: 11 | - ./glusterd.vol:/etc/glusterfs/glusterd.vol:ro 12 | - state-node-1:/var/lib/glusterd 13 | # - ./data/node-1/brick:/brick 14 | - brick-node-1:/brick 15 | node-2: 16 | image: sapk/glusterfs 17 | # devices: 18 | # - /dev/fuse:/dev/fuse 19 | cap_add: 20 | - SYS_ADMIN 21 | volumes: 22 | - ./glusterd.vol:/etc/glusterfs/glusterd.vol:ro 23 | - state-node-2:/var/lib/glusterd 24 | # - ./data/node-2/brick:/brick 25 | - brick-node-2:/brick 26 | node-3: 27 | image: sapk/glusterfs 28 | # devices: 29 | # - /dev/fuse:/dev/fuse 30 | cap_add: 31 | - SYS_ADMIN 32 | volumes: 33 | - ./glusterd.vol:/etc/glusterfs/glusterd.vol:ro 34 | - state-node-3:/var/lib/glusterd 35 | # - ./data/node-3/brick:/brick 36 | - brick-node-3:/brick 37 | 38 | 39 | volumes: 40 | state-node-1: 41 | state-node-2: 42 | state-node-3: 43 | brick-node-1: 44 | brick-node-2: 45 | brick-node-3: 46 | -------------------------------------------------------------------------------- /gluster/integration/docker/gluster-cluster/glusterd.vol: -------------------------------------------------------------------------------- 1 | volume management 2 | type mgmt/glusterd 3 | option working-directory /var/lib/glusterd 4 | option transport-type socket,rdma 5 | option transport.socket.keepalive-time 10 6 | option transport.socket.keepalive-interval 2 7 | option transport.socket.read-fail-log off 8 | option ping-timeout 0 9 | option event-threads 1 10 | # option transport.address-family inet6 11 | # option base-port 49152 12 | end-volume 13 | -------------------------------------------------------------------------------- /gluster/integration/integration_test.go: -------------------------------------------------------------------------------- 1 | package integration 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "runtime" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | log "github.com/Sirupsen/logrus" 15 | "github.com/docker/go-plugins-helpers/volume" 16 | "github.com/sapk/docker-volume-gluster/gluster" 17 | "github.com/sapk/docker-volume-gluster/gluster/driver" 18 | ) 19 | 20 | const timeInterval = 2 * time.Second 21 | 22 | func TestMain(m *testing.M) { 23 | //TODO check system for gluster, docker and docker-compose (install container version if needed) 24 | //TODO need root to start plugin 25 | 26 | //Setup 27 | setupGlusterCluster() 28 | 29 | //Do tests 30 | retVal := m.Run() 31 | 32 | //Clean up 33 | cleanGlusterCluster() 34 | 35 | os.Exit(retVal) 36 | } 37 | 38 | func setupPlugin() { 39 | gluster.PluginAlias = "gluster-local-integration" 40 | gluster.BaseDir = filepath.Join(volume.DefaultDockerRootDirectory, gluster.PluginAlias) 41 | driver.CfgFolder = "/etc/docker-volumes/" + gluster.PluginAlias 42 | log.Print(cmd("rm", "-rf", driver.CfgFolder)) 43 | log.SetLevel(log.DebugLevel) 44 | 45 | gluster.DaemonStart(nil, []string{}) 46 | time.Sleep(timeInterval) 47 | //log.Print(cmd("docker", "plugin", "ls")) 48 | log.Print(cmd("docker", "info", "-f", "{{.Plugins.Volume}}")) 49 | } 50 | 51 | func setupGlusterCluster() { 52 | pwd := currentPWD() 53 | log.Print(cmd("docker-compose", "-f", pwd+"/docker/gluster-cluster/docker-compose.yml", "up", "-d")) 54 | time.Sleep(timeInterval) 55 | nodes := []string{"node-1", "node-2", "node-3"} 56 | 57 | for _, n := range nodes { 58 | time.Sleep(timeInterval) 59 | log.Print(cmd("docker-compose", "-f", pwd+"/docker/gluster-cluster/docker-compose.yml", "exec", "-T", n, "mkdir", "-p", "/brick")) 60 | } 61 | 62 | time.Sleep(timeInterval) 63 | IPs := getGlusterClusterContainersIPs() 64 | for _, ip := range IPs[1:] { 65 | time.Sleep(timeInterval) 66 | log.Print(cmd("docker-compose", "-f", pwd+"/docker/gluster-cluster/docker-compose.yml", "exec", "-T", "node-1", "gluster", "peer", "probe", ip)) 67 | } 68 | 69 | time.Sleep(timeInterval) 70 | log.Print(cmd("docker-compose", "-f", pwd+"/docker/gluster-cluster/docker-compose.yml", "exec", "-T", "node-1", "gluster", "pool", "list")) 71 | log.Print(cmd("docker-compose", "-f", pwd+"/docker/gluster-cluster/docker-compose.yml", "exec", "-T", "node-1", "gluster", "peer", "status")) 72 | time.Sleep(timeInterval) 73 | 74 | log.Print(cmd("docker-compose", "-f", pwd+"/docker/gluster-cluster/docker-compose.yml", "exec", "-T", "node-1", "gluster", "volume", "create", "test-replica", "replica", "3", IPs[0]+":/brick/replica", IPs[1]+":/brick/replica", IPs[2]+":/brick/replica")) 75 | time.Sleep(timeInterval) 76 | log.Print(cmd("docker-compose", "-f", pwd+"/docker/gluster-cluster/docker-compose.yml", "exec", "-T", "node-1", "gluster", "volume", "create", "test-distributed", IPs[0]+":/brick/distributed", IPs[1]+":/brick/distributed", IPs[2]+":/brick/distributed")) 77 | 78 | time.Sleep(timeInterval) 79 | log.Print(cmd("docker-compose", "-f", pwd+"/docker/gluster-cluster/docker-compose.yml", "exec", "-T", "node-1", "gluster", "volume", "start", "test-replica")) 80 | time.Sleep(timeInterval) 81 | log.Print(cmd("docker-compose", "-f", pwd+"/docker/gluster-cluster/docker-compose.yml", "exec", "-T", "node-1", "gluster", "volume", "start", "test-distributed")) 82 | time.Sleep(timeInterval) 83 | } 84 | 85 | //gluster pool list 86 | func cleanGlusterCluster() { 87 | pwd := currentPWD() 88 | log.Print(cmd("docker-compose", "-f", pwd+"/docker/gluster-cluster/docker-compose.yml", "down")) 89 | log.Print(cmd("docker", "volume", "rm", "-f", "distributed", "replica", "distributed-double-server", "replica-double-server")) 90 | log.Print(cmd("docker", "volume", "prune", "-f")) 91 | //TODO log.Print(cmd("docker", "system", "prune", "-af")) 92 | } 93 | 94 | func cmd(cmd string, arg ...string) (string, error) { 95 | fmt.Println("Executing: " + cmd + " " + strings.Join(arg, " ")) 96 | c := exec.Command(cmd, arg...) 97 | var out bytes.Buffer 98 | c.Stdout = &out 99 | c.Stderr = &out 100 | err := c.Run() 101 | return out.String(), err 102 | } 103 | 104 | func currentPWD() string { 105 | _, filename, _, _ := runtime.Caller(0) 106 | return filepath.Dir(filename) 107 | } 108 | 109 | func getGlusterClusterContainers() []string { 110 | pwd := currentPWD() 111 | list, _ := cmd("docker-compose", "-f", pwd+"/docker/gluster-cluster/docker-compose.yml", "ps", "-q") 112 | l := strings.Split(list, "\n") 113 | return l[:len(l)-1] 114 | } 115 | 116 | func getContainerIP(cid string) string { 117 | ips, _ := cmd("docker", "inspect", "--format", "'{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'", cid) 118 | return strings.Trim(strings.Split(ips, "\n")[0], "'") 119 | } 120 | 121 | func getGlusterClusterContainersIPs() []string { 122 | containers := getGlusterClusterContainers() 123 | IPs := make([]string, len(containers)) 124 | for i := range containers { 125 | IPs[i] = getContainerIP(containers[i]) 126 | } 127 | return IPs 128 | } 129 | 130 | func TestIntegration(t *testing.T) { 131 | //Startplugin with empty config 132 | go setupPlugin() 133 | time.Sleep(3 * timeInterval) 134 | 135 | IPs := getGlusterClusterContainersIPs() 136 | 137 | log.Print(cmd("docker", "volume", "create", "--driver", gluster.PluginAlias, "--opt", "voluri=\""+IPs[0]+":test-replica\"", "replica")) 138 | time.Sleep(timeInterval) 139 | log.Print(cmd("docker", "volume", "create", "--driver", gluster.PluginAlias, "--opt", "voluri=\""+IPs[0]+":test-distributed\"", "distributed")) 140 | time.Sleep(timeInterval) 141 | log.Print(cmd("docker", "volume", "create", "--driver", gluster.PluginAlias, "--opt", "voluri=\""+IPs[0]+","+IPs[1]+":test-replica\"", "replica-double-server")) 142 | time.Sleep(timeInterval) 143 | log.Print(cmd("docker", "volume", "create", "--driver", gluster.PluginAlias, "--opt", "voluri=\""+IPs[0]+","+IPs[1]+":test-distributed\"", "distributed-double-server")) 144 | time.Sleep(timeInterval) 145 | log.Print(cmd("docker", "volume", "ls")) 146 | time.Sleep(3 * timeInterval) 147 | //TODO docker volume create --driver sapk/plugin-gluster --opt voluri=":" --name test 148 | 149 | out, err := cmd("docker", "run", "--rm", "-t", "-v", "replica:/mnt", "alpine", "/bin/ls", "/mnt") 150 | log.Println(out) 151 | if err != nil { 152 | t.Errorf("Failed to list mounted volume : %v", err) 153 | } 154 | out, err = cmd("docker", "run", "--rm", "-t", "-v", "replica:/mnt", "alpine", "/bin/cp", "/etc/hostname", "/mnt/container") 155 | log.Println(out) 156 | if err != nil { 157 | t.Errorf("Failed to write inside mounted volume : %v", err) 158 | } 159 | out, err = cmd("docker", "run", "--rm", "-t", "-v", "replica:/mnt", "alpine", "/bin/cat", "/mnt/container") 160 | log.Println(out) 161 | if err != nil { 162 | t.Errorf("Failed to read from mounted volume : %v", err) 163 | } 164 | time.Sleep(3 * timeInterval) 165 | 166 | out, err = cmd("docker", "run", "--rm", "-t", "-v", "distributed:/mnt", "alpine", "/bin/ls", "/mnt") 167 | log.Println(out) 168 | if err != nil { 169 | t.Errorf("Failed to list mounted volume : %v", err) 170 | } 171 | out, err = cmd("docker", "run", "--rm", "-t", "-v", "distributed:/mnt", "alpine", "/bin/cp", "/etc/hostname", "/mnt/container") 172 | log.Println(out) 173 | if err != nil { 174 | t.Errorf("Failed to write inside mounted volume : %v", err) 175 | } 176 | out, err = cmd("docker", "run", "--rm", "-t", "-v", "distributed:/mnt", "alpine", "/bin/cat", "/mnt/container") 177 | log.Println(out) 178 | if err != nil { 179 | t.Errorf("Failed to read from mounted volume : %v", err) 180 | } 181 | time.Sleep(3 * timeInterval) 182 | 183 | out, err = cmd("docker", "run", "--rm", "-t", "-v", "replica-double-server:/mnt", "alpine", "/bin/ls", "/mnt") 184 | log.Println(out) 185 | if err != nil { 186 | t.Errorf("Failed to list mounted volume (with fallback) : %v", err) 187 | } 188 | out, err = cmd("docker", "run", "--rm", "-t", "-v", "replica-double-server:/mnt", "alpine", "/bin/cat", "/mnt/container") 189 | log.Println(out) 190 | if err != nil { 191 | t.Errorf("Failed to read from mounted volume (with fallback) : %v", err) 192 | } 193 | out, err = cmd("docker", "run", "--rm", "-t", "-v", "distributed-double-server:/mnt", "alpine", "/bin/ls", "/mnt") 194 | log.Println(out) 195 | if err != nil { 196 | t.Errorf("Failed to list mounted volume (with fallback) : %v", err) 197 | } 198 | out, err = cmd("docker", "run", "--rm", "-t", "-v", "distributed-double-server:/mnt", "alpine", "/bin/cat", "/mnt/container") 199 | log.Println(out) 200 | if err != nil { 201 | t.Errorf("Failed to read from mounted volume (with fallback) : %v", err) 202 | } 203 | //TODO check that container is same as before 204 | 205 | } 206 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/sapk/docker-volume-gluster/gluster" 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 | gluster.Version = Version 20 | gluster.Commit = Commit 21 | gluster.Branch = Branch 22 | gluster.BuildTime = BuildTime 23 | gluster.Init() 24 | } 25 | -------------------------------------------------------------------------------- /support/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang AS build-env 2 | 3 | COPY . /go/src/app 4 | WORKDIR /go/src/app 5 | 6 | RUN make build 7 | 8 | FROM ubuntu 9 | MAINTAINER Antoine GIRARD 10 | 11 | RUN apt-get update \ 12 | && apt-get install -y glusterfs-client \ 13 | && mkdir -p /var/lib/docker-volumes/gluster /etc/docker-volumes/gluster \ 14 | && apt-get autoclean -y && apt-get clean -y \ 15 | && rm -rf /var/lib/apt/lists/* 16 | 17 | COPY --from=build-env /go/src/app/docker-volume-gluster /usr/bin/docker-volume-gluster 18 | 19 | RUN /usr/bin/docker-volume-gluster version 20 | 21 | ENTRYPOINT [ "/usr/bin/docker-volume-gluster" ] 22 | CMD [ "daemon" ] 23 | --------------------------------------------------------------------------------