├── .dockerignore ├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode ├── configurationCache.log ├── settings.json └── targets.log ├── Dockerfile ├── Dockerfile-build ├── LICENSE ├── Makefile ├── README.md ├── docker-compose.example.yml ├── driver.go ├── driver_test.go ├── glide.yaml ├── go.mod ├── go.sum ├── init ├── systemd.service └── upstart.conf ├── main.go └── scripts ├── docker-build.sh ├── docker-run.sh ├── install.sh ├── integration.sh └── release.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea 2 | bin 3 | init 4 | scripts 5 | local-persist.iml 6 | README.md 7 | coverage.out 8 | vendor 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{json,yml,yaml}] 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [Makefile] 19 | indent_style = tab 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | 11 | test: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.18 20 | 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v2 23 | 24 | - name: Go Mod 25 | run: go mod download && go mod vendor 26 | 27 | - name: Make Binaries 28 | run: make binary-linux-amd64 29 | 30 | - name: Change Permissions 31 | run: chmod +x ./bin/linux/amd64/local-persist 32 | 33 | - name: Start in the Background 34 | run: sudo ./bin/linux/amd64/local-persist & 35 | 36 | - name: Integration Tests 37 | run: ./scripts/integration.sh 38 | 39 | - name: Unit Tests 40 | run: make test 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | bin/ 3 | 4 | *.iml 5 | coverage.out 6 | vendor/ 7 | .vscode 8 | -------------------------------------------------------------------------------- /.vscode/configurationCache.log: -------------------------------------------------------------------------------- 1 | {"buildTargets":["binaries","binary","binary-linux-amd64","clean-bin","coverage","docker-build","docker-run","release","run","test"],"launchTargets":[],"customConfigurationProvider":{"workspaceBrowse":{"browsePath":[],"compilerArgs":[]},"fileIndex":[]}} -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "makefile.extensionOutputFolder": "./.vscode" 3 | } -------------------------------------------------------------------------------- /.vscode/targets.log: -------------------------------------------------------------------------------- 1 | make all --print-data-base --no-builtin-variables --no-builtin-rules --question 2 | make: *** No rule to make target 'all'. Stop. 3 | 4 | # GNU Make 4.3 5 | # Built for aarch64-unknown-linux-gnu 6 | # Copyright (C) 1988-2020 Free Software Foundation, Inc. 7 | # License GPLv3+: GNU GPL version 3 or later 8 | # This is free software: you are free to change and redistribute it. 9 | # There is NO WARRANTY, to the extent permitted by law. 10 | 11 | # Make data base, printed on Mon Aug 1 15:27:24 2022 12 | 13 | # Variables 14 | 15 | # environment 16 | LC_ALL = C 17 | # environment 18 | NO_AT_BRIDGE = 1 19 | # environment 20 | VSCODE_CWD = /home/badger 21 | # environment 22 | TEXTDOMAIN = Linux-PAM 23 | # default 24 | MAKE_COMMAND := make 25 | # environment 26 | VSCODE_HANDLES_SIGPIPE = true 27 | # environment 28 | GOPATH = /home/badger/go 29 | # automatic 30 | @D = $(patsubst %/,%,$(dir $@)) 31 | # environment 32 | VSCODE_HANDLES_UNCAUGHT_ERRORS = true 33 | # default 34 | .VARIABLES := 35 | # makefile (from 'Makefile', line 1) 36 | PWD = $(shell bash -c 'pwd') 37 | # automatic 38 | %D = $(patsubst %/,%,$(dir $%)) 39 | # automatic 40 | ^D = $(patsubst %/,%,$(dir $^)) 41 | # automatic 42 | %F = $(notdir $%) 43 | # environment 44 | LANG = C 45 | # default 46 | .LOADED := 47 | # default 48 | .INCLUDE_DIRS = /usr/local/include /usr/include /usr/include 49 | # makefile 50 | MAKEFLAGS = pqrR 51 | # makefile 52 | CURDIR := /home/badger/git/local-persist 53 | # environment 54 | APPLICATION_INSIGHTS_NO_DIAGNOSTIC_CHANNEL = true 55 | # automatic 56 | *D = $(patsubst %/,%,$(dir $*)) 57 | # environment 58 | MFLAGS = -pqrR 59 | # default 60 | .SHELLFLAGS := -c 61 | # automatic 62 | +D = $(patsubst %/,%,$(dir $+)) 63 | # makefile (from 'Makefile', line 1) 64 | MAKEFILE_LIST := Makefile 65 | # automatic 66 | @F = $(notdir $@) 67 | # automatic 68 | ?D = $(patsubst %/,%,$(dir $?)) 69 | # automatic 70 | *F = $(notdir $*) 71 | # automatic 72 | Plugins can run inside or outside containers. Currently running them outside containers is recommended. 26 | 27 | It doesn't really say *why* one way is recommended over the other, but I provide binaries and instructions to run outside of container, as well as an image and instructions to run it inside a container. 28 | 29 | ### Running Outside a Container 30 | 31 | **Note:** You currently cannot run this plugin natively on macOS or Windows. The current workaround is to [run the plugin in a container](#running-from-within-a-container). 32 | 33 | #### Quick Way 34 | 35 | I provide an `install` script that will download the proper binary, set up an Systemd service to start when Docker does and enable it. 36 | 37 | ```shell 38 | curl -fsSL https://raw.githubusercontent.com/MatchbookLab/local-persist/master/scripts/install.sh | sudo bash 39 | ``` 40 | 41 | This needs be to run on the Docker *host*. i.e. running that on a Mac won't work (and it will print a message saying as much and exit). 42 | 43 | This has been tested on Ubuntu 15.10, and is known *not* to work on CoreOS (yet). If you need to use Upstart instead of Systemd, you can pass the `--upstart` flag to the install script, but it isn't as tested, so it may not work: 44 | 45 | ```shell 46 | curl -fsSL https://raw.githubusercontent.com/MatchbookLab/local-persist/master/scripts/install.sh | sudo bash -s -- --upstart 47 | ``` 48 | 49 | Follow the same process to update to the latest version. 50 | 51 | #### Manual Way 52 | 53 | If you're uncomfortable running a script you downloaded off the internet with `sudo`, you can extract any of the steps out of the [`install.sh`](scripts/install.sh) script and run them manually. However you want to do it, the main steps are: 54 | 55 | 1. Download the appropriate binary from the [Releases page](https://github.com/MatchbookLab/local-persist/releases) for your OS and architecture. 56 | 2. Rename the downloaded file `docker-volume-local-persist` 57 | 3. Place it in `/usr/bin` (you can put it somewhere else, but be sure your Systemd (or similar) config reflects the change). 58 | 4. Make sure the file is executable (`chmod +x /usr/bin/docker-volume-local-persist`) 59 | 5. It's enough to just run it at this point (type `docker-volume-local-persist` and hit enter) to test, etc, and if that's all you're trying to do, you're done. But if you want it to start with Docker, proceed to step 6. 60 | 6. Download [systemd.service](init/systemd.service) 61 | 7. Rename the service file to `docker-volume-local-persist.service` 62 | 8. Move it to `/etc/systemd/system/` 63 | 9. run `sudo systemctl daemon-reload` to reload the config 64 | 10. run `sudo systemctl enable docker-volume-local-persist` to enable the service (it will start after Docker does) 65 | 11. run `sudo systemctl start docker-volume-local-persist` to start it now. Safe to run if it's already started 66 | 67 | 68 | ### Running from Within a Container (aka Running on Mac or Windows) 69 | 70 | macOS and Windows do not support native Docker plugins, so the solution is to run this plugin from another container (you can also do this on Linux if you don't want to install the plugin manually). 71 | 72 | I maintain an [image on Docker Hub](https://hub.docker.com/r/cwspear/docker-local-persist-volume-plugin/) to run this plugin from a container: 73 | 74 | ```shell 75 | docker run -d \ 76 | -v /run/docker/plugins/:/run/docker/plugins/ \ 77 | -v /path/to/store/json/for/restart/:/var/lib/docker/plugin-data/ \ 78 | -v /path/to/where/you/want/data/volume/:/path/to/where/you/want/data/volume/ \ 79 | cwspear/docker-local-persist-volume-plugin 80 | ``` 81 | 82 | The `-v /run/docker/plugins/:/run/docker/plugins/` part will make sure the `sock` file gets created at the right place. You also need to add one or more volumes to places you want to mount your volumes later at. 83 | 84 | For example, if I am going to persist my MySQL data for a container I'm going to build later at `/data/mysql/`, I would add a `-v /data/mysql/:/data/mysql/` to the command above (or even `-v /data/:/data/`). You can add more than one location in this manner. 85 | 86 | Lastly, the `-v /path/to/store/json/for/restart/:/var/lib/docker/plugin-data/` part is so that the plugin can create a `json` file to know what volumes existed in case of a system restart, etc. 87 | 88 | When the container is destroyed, etc, it will look at a file it created in `/var/lib/docker/plugin-data/` to recreate any volumes that had previously existed, so you want that JSON file to persist on the host. 89 | 90 | ## Usage: Creating Volumes 91 | 92 | Then to use, you can create a volume with this plugin (this example will be for a shared folder for images): 93 | 94 | ```shell 95 | docker volume create -d local-persist -o mountpoint=/data/images --name=images 96 | ``` 97 | 98 | Then if you create a container, you can connect it to this Volume: 99 | 100 | ```shell 101 | docker run -d -v images:/path/to/images/on/one/ one 102 | docker run -d -v images:/path/to/images/on/two/ two 103 | # etc 104 | ``` 105 | 106 | Also, see [docker-compose.example.yml](docker-compose.example.yml) for an example to do something like this with Docker Compose (needs Compose 1.6+ which needs Engine 1.10+). 107 | 108 | ## Benefits 109 | 110 | This has a few advantages over the (default) `local` driver that comes with Docker, because our data *will not be deleted* when the Volume is removed. The `local` driver deletes all data when it's removed. With the `local-persist` driver, if you remove the driver, and then recreate it later with the same command above, any volume that was added to that volume will *still be there*. 111 | 112 | You may have noticed that you could do this with data-only containers, too. And that's true, and using that technique has a few advantages, one thing it (specifically as a limitation of `volumes-from`) does *not* allow, is mounting that shared volume to a different path inside your containers. Trying to recreate the above example, each container would have to store images in the same directory in their containers, instead of separate ones which `local-persist` allows. 113 | 114 | Also, using `local-persist` instead of data-only containers, `docker ps -a` won't have extra dead entries, and `docker volume ls` will have more descriptive output (because volumes have names). 115 | -------------------------------------------------------------------------------- /docker-compose.example.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | one: 5 | image: alpine 6 | working_dir: /one/ 7 | command: sleep 600 8 | volumes: 9 | - data:/one/ 10 | 11 | two: 12 | image: alpine 13 | working_dir: /two/ 14 | command: sleep 600 15 | volumes: 16 | - data:/two/ 17 | 18 | volumes: 19 | data: 20 | driver: local-persist 21 | driver_opts: 22 | mountpoint: /data/local-persist/data 23 | -------------------------------------------------------------------------------- /driver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "os" 7 | "strconv" 8 | "io/ioutil" 9 | "path" 10 | "encoding/json" 11 | 12 | "github.com/docker/go-plugins-helpers/volume" 13 | "github.com/docker/engine-api/client" 14 | "github.com/docker/engine-api/types" 15 | "github.com/fatih/color" 16 | "golang.org/x/net/context" 17 | ) 18 | 19 | var ( 20 | // red = color.New(color.FgRed).SprintfFunc() 21 | // green = color.New(color.FgGreen).SprintfFunc() 22 | yellow = color.New(color.FgYellow).SprintfFunc() 23 | cyan = color.New(color.FgCyan).SprintfFunc() 24 | blue = color.New(color.FgBlue).SprintfFunc() 25 | magenta = color.New(color.FgMagenta).SprintfFunc() 26 | white = color.New(color.FgWhite).SprintfFunc() 27 | ) 28 | 29 | const ( 30 | stateDir = "/var/lib/docker/plugin-data/" 31 | stateFile = "local-persist.json" 32 | ) 33 | 34 | type localPersistDriver struct { 35 | volumes map[string]string 36 | mutex *sync.Mutex 37 | debug bool 38 | name string 39 | } 40 | 41 | type saveData struct { 42 | State map[string]string `json:"state"` 43 | } 44 | 45 | func newLocalPersistDriver() localPersistDriver { 46 | fmt.Printf(white("%-18s", "Starting... ")) 47 | 48 | driver := localPersistDriver{ 49 | volumes : map[string]string{}, 50 | mutex : &sync.Mutex{}, 51 | debug : true, 52 | name : "local-persist", 53 | } 54 | 55 | os.Mkdir(stateDir, 0700) 56 | 57 | _, driver.volumes = driver.findExistingVolumesFromStateFile() 58 | fmt.Printf("Found %s volumes on startup\n", yellow(strconv.Itoa(len(driver.volumes)))) 59 | 60 | return driver 61 | } 62 | 63 | func (driver localPersistDriver) Get(req volume.Request) volume.Response { 64 | fmt.Print(white("%-18s", "Get Called... ")) 65 | 66 | if driver.exists(req.Name) { 67 | fmt.Printf("Found %s\n", cyan(req.Name)) 68 | return volume.Response{ 69 | Volume: driver.volume(req.Name), 70 | } 71 | } 72 | 73 | fmt.Printf("Couldn't find %s\n", cyan(req.Name)) 74 | return volume.Response{ 75 | Err: fmt.Sprintf("No volume found with the name %s", cyan(req.Name)), 76 | } 77 | } 78 | 79 | func (driver localPersistDriver) List(req volume.Request) volume.Response { 80 | fmt.Print(white("%-18s", "List Called... ")) 81 | 82 | var volumes []*volume.Volume 83 | for name, _ := range driver.volumes { 84 | volumes = append(volumes, driver.volume(name)) 85 | } 86 | 87 | fmt.Printf("Found %s volumes\n", yellow(strconv.Itoa(len(volumes)))) 88 | 89 | return volume.Response{ 90 | Volumes: volumes, 91 | } 92 | } 93 | 94 | func (driver localPersistDriver) Create(req volume.Request) volume.Response { 95 | fmt.Print(white("%-18s", "Create Called... ")) 96 | 97 | mountpoint := req.Options["mountpoint"] 98 | if mountpoint == "" { 99 | fmt.Printf("No %s option provided\n", blue("mountpoint")) 100 | return volume.Response{ Err: fmt.Sprintf("The `mountpoint` option is required") } 101 | } 102 | 103 | driver.mutex.Lock() 104 | defer driver.mutex.Unlock() 105 | 106 | if driver.exists(req.Name) { 107 | return volume.Response{ Err: fmt.Sprintf("The volume %s already exists", req.Name) } 108 | } 109 | 110 | err := os.MkdirAll(mountpoint, 0755) 111 | fmt.Printf("Ensuring directory %s exists on host...\n", magenta(mountpoint)) 112 | 113 | if err != nil { 114 | fmt.Printf("%17s Could not create directory %s\n", " ", magenta(mountpoint)) 115 | return volume.Response{ Err: err.Error() } 116 | } 117 | 118 | driver.volumes[req.Name] = mountpoint 119 | e := driver.saveState(driver.volumes) 120 | if e != nil { 121 | fmt.Println(e.Error()) 122 | } 123 | 124 | fmt.Printf("%17s Created volume %s with mountpoint %s\n", " ", cyan(req.Name), magenta(mountpoint)) 125 | 126 | return volume.Response{} 127 | } 128 | 129 | func (driver localPersistDriver) Remove(req volume.Request) volume.Response { 130 | fmt.Print(white("%-18s", "Remove Called... ")) 131 | driver.mutex.Lock() 132 | defer driver.mutex.Unlock() 133 | 134 | delete(driver.volumes, req.Name) 135 | 136 | err := driver.saveState(driver.volumes) 137 | if err != nil { 138 | fmt.Println(err.Error()) 139 | } 140 | 141 | fmt.Printf("Removed %s\n", cyan(req.Name)) 142 | 143 | return volume.Response{} 144 | } 145 | 146 | func (driver localPersistDriver) Mount(req volume.MountRequest) volume.Response { 147 | fmt.Print(white("%-18s", "Mount Called... ")) 148 | 149 | fmt.Printf("Mounted %s\n", cyan(req.Name)) 150 | 151 | return driver.Path(volume.Request{Name: req.Name}) 152 | } 153 | 154 | func (driver localPersistDriver) Path(req volume.Request) volume.Response { 155 | fmt.Print(white("%-18s", "Path Called... ")) 156 | 157 | fmt.Printf("Returned path %s\n", magenta(driver.volumes[req.Name])) 158 | 159 | return volume.Response{ Mountpoint: driver.volumes[req.Name] } 160 | } 161 | 162 | func (driver localPersistDriver) Unmount(req volume.UnmountRequest) volume.Response { 163 | fmt.Print(white("%-18s", "Unmount Called... ")) 164 | 165 | fmt.Printf("Unmounted %s\n", cyan(req.Name)) 166 | 167 | return driver.Path(volume.Request{Name: req.Name}) 168 | } 169 | 170 | func (driver localPersistDriver) Capabilities(req volume.Request) volume.Response { 171 | fmt.Print(white("%-18s", "Capabilities Called... ")) 172 | 173 | return volume.Response{ 174 | Capabilities: volume.Capability{ Scope: "local" }, 175 | } 176 | } 177 | 178 | 179 | func (driver localPersistDriver) exists(name string) bool { 180 | return driver.volumes[name] != "" 181 | } 182 | 183 | func (driver localPersistDriver) volume(name string) *volume.Volume { 184 | return &volume.Volume{ 185 | Name: name, 186 | Mountpoint: driver.volumes[name], 187 | } 188 | } 189 | 190 | func (driver localPersistDriver) findExistingVolumesFromDockerDaemon() (error, map[string]string) { 191 | // set up the ability to make API calls to the daemon 192 | defaultHeaders := map[string]string{"User-Agent": "engine-api-cli-1.0"} 193 | // need at least Docker 1.9 (API v1.21) for named Volume support 194 | cli, err := client.NewClient("unix:///var/run/docker.sock", "v1.21", nil, defaultHeaders) 195 | if err != nil { 196 | return err, map[string]string{} 197 | } 198 | 199 | // grab ALL containers... 200 | options := types.ContainerListOptions{All: true} 201 | containers, err := cli.ContainerList(context.Background(), options) 202 | 203 | // ...and check to see if any of them belong to this driver and recreate their references 204 | var volumes = map[string]string{} 205 | for _, container := range containers { 206 | info, err := cli.ContainerInspect(context.Background(), container.ID) 207 | if err != nil { 208 | // something really weird happened here... PANIC 209 | panic(err) 210 | } 211 | 212 | for _, mount := range info.Mounts { 213 | if mount.Driver == driver.name { 214 | // @TODO there could be multiple volumes (mounts) with this { name: source } combo, and while that's okay 215 | // what if they is the same name with a different source? could that happen? if it could, 216 | // it'd be bad, so maybe we want to panic here? 217 | volumes[mount.Name] = mount.Source 218 | } 219 | } 220 | } 221 | 222 | if err != nil || len(volumes) == 0 { 223 | fmt.Print("Attempting to load from file state... ") 224 | 225 | return driver.findExistingVolumesFromStateFile() 226 | } 227 | 228 | return nil, volumes 229 | } 230 | 231 | func (driver localPersistDriver) findExistingVolumesFromStateFile() (error, map[string]string) { 232 | path := path.Join(stateDir, stateFile) 233 | fileData, err := ioutil.ReadFile(path) 234 | if err != nil { 235 | return err, map[string]string{} 236 | } 237 | 238 | var data saveData 239 | e := json.Unmarshal(fileData, &data) 240 | if e != nil { 241 | return e, map[string]string{} 242 | } 243 | 244 | return nil, data.State 245 | } 246 | 247 | func (driver localPersistDriver) saveState(volumes map[string]string) error { 248 | data := saveData{ 249 | State: volumes, 250 | } 251 | 252 | fileData, err := json.Marshal(data) 253 | if err != nil { 254 | return err 255 | } 256 | 257 | path := path.Join(stateDir, stateFile) 258 | return ioutil.WriteFile(path, fileData, 0600) 259 | } 260 | -------------------------------------------------------------------------------- /driver_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import( 4 | "testing" 5 | "os" 6 | 7 | "github.com/docker/go-plugins-helpers/volume" 8 | ) 9 | 10 | var ( 11 | defaultTestName = "test-volume" 12 | defaultTestMountpoint = "/tmp/data/local-persist-test" 13 | ) 14 | 15 | func TestCreate(t *testing.T) { 16 | driver := newLocalPersistDriver() 17 | 18 | defaultCreateHelper(driver, t) 19 | 20 | // test that a directory is created 21 | _, err := os.Stat(defaultTestMountpoint) 22 | if os.IsNotExist(err) { 23 | t.Error("Mountpoint directory was not created:", err.Error()) 24 | } 25 | 26 | // test that volumes has one 27 | if len(driver.volumes) != 1 { 28 | t.Error("Driver should have exactly 1 volume") 29 | } 30 | 31 | defaultCleanupHelper(driver, t) 32 | 33 | // test that options are required 34 | res := driver.Create(volume.Request{ 35 | Name: defaultTestName, 36 | }); 37 | 38 | if res.Err != "The `mountpoint` option is required" { 39 | t.Error("Should error out without mountpoint option") 40 | } 41 | } 42 | 43 | func TestGet(t *testing.T) { 44 | driver := newLocalPersistDriver() 45 | 46 | defaultCreateHelper(driver, t) 47 | 48 | res := driver.Get(volume.Request{ Name: defaultTestName }) 49 | if res.Err != "" { 50 | t.Error("Should have found a volume!") 51 | } 52 | 53 | defaultCleanupHelper(driver, t) 54 | } 55 | 56 | func TestList(t *testing.T) { 57 | driver := newLocalPersistDriver() 58 | 59 | name := defaultTestName + "2" 60 | mountpoint := defaultTestMountpoint + "2" 61 | 62 | defaultCreateHelper(driver, t) 63 | res := driver.List(volume.Request{}) 64 | if len(res.Volumes) != 1 { 65 | t.Error("Should have found 1 volume!") 66 | } 67 | 68 | createHelper(driver, t, name, mountpoint) 69 | res2 := driver.List(volume.Request{}) 70 | if len(res2.Volumes) != 2 { 71 | t.Error("Should have found 1 volume!") 72 | } 73 | 74 | defaultCleanupHelper(driver, t) 75 | cleanupHelper(driver, t, name, mountpoint) 76 | } 77 | 78 | func TestMountUnmountPath(t *testing.T) { 79 | driver := newLocalPersistDriver() 80 | 81 | defaultCreateHelper(driver, t) 82 | 83 | // mount, mount and path should have same output (they all use Path under the hood) 84 | pathRes := driver.Path(volume.Request{ Name: defaultTestName }) 85 | mountRes := driver.Mount(volume.MountRequest{ Name: defaultTestName }) 86 | unmountRes := driver.Unmount(volume.UnmountRequest{ Name: defaultTestName }) 87 | 88 | if !(pathRes.Mountpoint == mountRes.Mountpoint && 89 | mountRes.Mountpoint == unmountRes.Mountpoint && 90 | unmountRes.Mountpoint == defaultTestMountpoint) { 91 | t.Error("Mount, Unmount and Path should all return the same Mountpoint") 92 | } 93 | } 94 | 95 | 96 | func createHelper(driver localPersistDriver, t *testing.T, name string, mountpoint string) { 97 | res := driver.Create(volume.Request{ 98 | Name: name, 99 | Options: map[string]string{ 100 | "mountpoint": mountpoint, 101 | }, 102 | }); 103 | 104 | if res.Err != "" { 105 | t.Error(res.Err) 106 | } 107 | } 108 | 109 | func defaultCreateHelper(driver localPersistDriver, t *testing.T) { 110 | createHelper(driver, t, defaultTestName, defaultTestMountpoint) 111 | } 112 | 113 | func cleanupHelper(driver localPersistDriver, t *testing.T, name string, mountpoint string) { 114 | os.RemoveAll(mountpoint) 115 | 116 | _, err := os.Stat(mountpoint) 117 | if !os.IsNotExist(err) { 118 | t.Error("[Cleanup] Mountpoint still exists:", err.Error()) 119 | } 120 | 121 | 122 | driver.Remove(volume.Request{ Name: name }) 123 | 124 | res := driver.Get(volume.Request{ Name: name }) 125 | if res.Err == "" { 126 | t.Error("[Cleanup] Volume still exists:", res.Err) 127 | } 128 | } 129 | 130 | func defaultCleanupHelper(driver localPersistDriver, t *testing.T) { 131 | cleanupHelper(driver, t, defaultTestName, defaultTestMountpoint) 132 | } 133 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: local-persist 2 | import: 3 | - package: github.com/docker/engine-api 4 | version: ^0.4.0 5 | subpackages: 6 | - client 7 | - types 8 | - package: github.com/docker/go-plugins-helpers 9 | subpackages: 10 | - volume 11 | - package: github.com/fatih/color 12 | version: ^1.1.0 13 | - package: golang.org/x/net 14 | subpackages: 15 | - context 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/MatchbookLab/local-persist 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/Microsoft/go-winio v0.3.6-0.20160621211438-ce2922f643c8 7 | github.com/Sirupsen/logrus v0.7.3 8 | github.com/coreos/go-systemd v0.0.0-20161102170722-64d5cd7cb947 9 | github.com/coreos/pkg v0.0.0-20161026222926-447b7ec906e5 10 | github.com/docker/distribution v2.5.0-rc.1.0.20161102195617-fbe6e8d212ed+incompatible 11 | github.com/docker/engine-api v0.4.0 12 | github.com/docker/go-connections v0.2.2-0.20161031182214-f512407a188e 13 | github.com/docker/go-plugins-helpers v0.0.0-20161031114640-60d242cfd0fb 14 | github.com/docker/go-units v0.3.2-0.20161020213227-8a7beacffa30 15 | github.com/fatih/color v1.1.0 16 | github.com/mattn/go-colorable v0.0.7-0.20161102080925-6e26b354bd2b 17 | github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c 18 | github.com/opencontainers/runc v1.0.0-rc2.0.20161102180326-49ed0a10e4ed 19 | golang.org/x/net v0.0.0-20160322021652-4876518f9e71 20 | golang.org/x/sys v0.0.0-20161023150541-c200b10b5d5e 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Microsoft/go-winio v0.3.6-0.20160621211438-ce2922f643c8 h1:Qp3i6ZUVcAOeTQj0KBsivXCfsnvsBASQYfXXS/N34NY= 2 | github.com/Microsoft/go-winio v0.3.6-0.20160621211438-ce2922f643c8/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= 3 | github.com/Sirupsen/logrus v0.7.3 h1:G04jzNEnKQKTemyvbCV32zFbOu+D++ECKnCFazUjPdc= 4 | github.com/Sirupsen/logrus v0.7.3/go.mod h1:rmk17hk6i8ZSAJkSDa7nOxamrG+SP4P0mm+DAvExv4U= 5 | github.com/coreos/go-systemd v0.0.0-20161102170722-64d5cd7cb947 h1:9zLgi/vHKaTOHO7YtcSvAzSiytU13fhg/w5t8YOM/Ho= 6 | github.com/coreos/go-systemd v0.0.0-20161102170722-64d5cd7cb947/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 7 | github.com/coreos/pkg v0.0.0-20161026222926-447b7ec906e5 h1:aqoxNZ9nyZ9kabG+daqglH9RtWIbpxDjVQMoUsoVHuo= 8 | github.com/coreos/pkg v0.0.0-20161026222926-447b7ec906e5/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 9 | github.com/docker/distribution v2.5.0-rc.1.0.20161102195617-fbe6e8d212ed+incompatible h1:6W0HnJhig6393/Qm1Qv/4mhZl/TdsVuRP24WRVkCi64= 10 | github.com/docker/distribution v2.5.0-rc.1.0.20161102195617-fbe6e8d212ed+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 11 | github.com/docker/engine-api v0.4.0 h1:D0Osr6+45yAlQqLyoczv5qJtAu+P0HB0rLCddck03wY= 12 | github.com/docker/engine-api v0.4.0/go.mod h1:xtQCpzf4YysNZCVFfIGIm7qfLvYbxtLkEVVfKhTVOvw= 13 | github.com/docker/go-connections v0.2.2-0.20161031182214-f512407a188e h1:jcaJgljPmPKABWQTL7kZ4bn75pxBwDrzW9PcLkqkH7o= 14 | github.com/docker/go-connections v0.2.2-0.20161031182214-f512407a188e/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 15 | github.com/docker/go-plugins-helpers v0.0.0-20161031114640-60d242cfd0fb h1:3PbRNm4EK4EszBwI3UR2Wv6IR9tT+PozgOdgJjSq8hs= 16 | github.com/docker/go-plugins-helpers v0.0.0-20161031114640-60d242cfd0fb/go.mod h1:LFyLie6XcDbyKGeVK6bHe+9aJTYCxWLBg5IrJZOaXKA= 17 | github.com/docker/go-units v0.3.2-0.20161020213227-8a7beacffa30 h1:bANAGhKtjG23j8rJr6QEKZJlUaRhiTuq7kCaYV9Ggbg= 18 | github.com/docker/go-units v0.3.2-0.20161020213227-8a7beacffa30/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 19 | github.com/fatih/color v1.1.0 h1:4RQHlUrrLRssqNPpcM+ZLy+alwucmC4mkIGTbiVdCeY= 20 | github.com/fatih/color v1.1.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 21 | github.com/mattn/go-colorable v0.0.7-0.20161102080925-6e26b354bd2b h1:QDkLN5n0HZJHlRE6QtBlrhuHgiQVYBuJ+R7gfyJBQ84= 22 | github.com/mattn/go-colorable v0.0.7-0.20161102080925-6e26b354bd2b/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 23 | github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c h1:3nKFouDdpgGUV/uerJcYWH45ZbJzX0SiVWfTgmUeTzc= 24 | github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 25 | github.com/opencontainers/runc v1.0.0-rc2.0.20161102180326-49ed0a10e4ed h1:CO6K4Ex3bZFJrue+c19dYXE0ZCa9qoux6JSPMZKEeHw= 26 | github.com/opencontainers/runc v1.0.0-rc2.0.20161102180326-49ed0a10e4ed/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= 27 | golang.org/x/net v0.0.0-20160322021652-4876518f9e71 h1:uzpLVINUETeZRto6ZphAoh11snmPXlRiUF+rNKPHlO8= 28 | golang.org/x/net v0.0.0-20160322021652-4876518f9e71/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 29 | golang.org/x/sys v0.0.0-20161023150541-c200b10b5d5e h1:uau++4dmjGy3TuvPvRbhZHY+TQVsImIxtWY5HRh9eho= 30 | golang.org/x/sys v0.0.0-20161023150541-c200b10b5d5e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 31 | -------------------------------------------------------------------------------- /init/systemd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=docker-volume-local-persist 3 | Before=docker.service 4 | Wants=docker.service 5 | 6 | [Service] 7 | TimeoutStartSec=0 8 | ExecStart=/usr/bin/docker-volume-local-persist 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /init/upstart.conf: -------------------------------------------------------------------------------- 1 | # Ubuntu upstart file at /etc/init/docker-volume-local-persist.conf 2 | 3 | description "Local Persist volume driver for Docker" 4 | author "Cameron Spear cam@cameronspear.com" 5 | 6 | start on starting docker 7 | stop on stopped docker 8 | respawn 9 | 10 | script 11 | exec docker-volume-local-persist 12 | end script 13 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/docker/go-plugins-helpers/volume" 7 | ) 8 | 9 | func main() { 10 | driver := newLocalPersistDriver() 11 | 12 | handler := volume.NewHandler(driver) 13 | fmt.Println(handler.ServeUnix("root", driver.name)) 14 | } 15 | -------------------------------------------------------------------------------- /scripts/docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | LOCAL_PERSIST=$(docker build -q -f Dockerfile-build .) 4 | docker run -it -v `pwd`/bin:/go/src/local-persist/bin $LOCAL_PERSIST 5 | -------------------------------------------------------------------------------- /scripts/docker-run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [[ ! -e $DATA_VOLUME ]]; then 4 | echo Missing required environment variable: DATA_VOLUME 5 | exit 1 6 | fi 7 | 8 | CMD="docker run -d -v /run/docker/plugins/:/run/docker/plugins/ -v ${DATA_VOLUME}:${DATA_VOLUME} cwspear/docker-local-persist-volume-plugin" 9 | 10 | echo $CMD 11 | exec $CMD 12 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | VERSION="v1.3.0" 6 | 7 | gitDir=$(realpath `dirname $BASH_SOURCE`/..) 8 | echo $gitDir 9 | 10 | # uname -s, uname -m 11 | # Deb 32: Linux i686 12 | # Ubuntu 64: Linux x86_64 13 | # FreeBSD: FreeBSD amd64 14 | 15 | if [[ "$UID" != 0 ]]; then 16 | echo NOTE: sudo needed to set up and run start service 17 | exit 1 18 | fi 19 | 20 | 21 | if [[ ! "`which git 2> /dev/null`" == "" ]]; then 22 | thisGit=`git -C "${gitDir}" config --get remote.origin.url` 23 | thisGit=${thisGit::-4} 24 | GITHUB_BINARY_BASE="${thisGit}/releases/download" 25 | GITHUB_RAW_BASE="${thisGit/github.com/raw.githubusercontent.com}/releases/download" 26 | fi 27 | 28 | if [[ $thisGit == "" ]]; then 29 | GITHUB_URL_PARTS="MatchbookLab/local-persist" 30 | GITHUB_BINARY_BASE="https://github.com/${GITHUB_URL_PARTS}/releases/download" 31 | GITHUB_RAW_BASE="https://raw.githubusercontent.com/${GITHUB_URL_PARTS}/" 32 | GITHUB_URL_PARTS= 33 | fi 34 | 35 | 36 | function setenv { 37 | OS=$(uname -s | tr "[:upper:]" "[:lower:]") 38 | ARCH=$(uname -m) 39 | 40 | SUPPORTED=false 41 | if [[ $OS == "linux" ]]; then 42 | case $ARCH in 43 | "x86_64") 44 | ARCH="amd64" 45 | SUPPORTED=true 46 | ;; 47 | "aarch64") 48 | ARCH="arm64" 49 | SUPPORTED=true 50 | ;; 51 | "i686") 52 | # ARCH="386" 53 | SUPPORTED=false 54 | ;; 55 | # untested 56 | arm*) 57 | # ARCH="arm" 58 | SUPPORTED=false 59 | ;; 60 | esac 61 | elif [[ $OS == 'freebsd' ]]; then 62 | ARCH=$(uname -m) 63 | SUPPORTED=false 64 | fi 65 | 66 | if [[ $SUPPORTED == false ]]; then 67 | echo $OS $ARCH is not supported 68 | exit 2 69 | fi 70 | } 71 | 72 | function install-binary { 73 | echo Stopping docker-volume-local-persist service if running 74 | echo '' 75 | if [[ $* == *--upstart* ]]; then 76 | (sudo service docker-volume-local-persist stop || true) 77 | else 78 | (sudo systemctl stop docker-volume-local-persist || true) 79 | fi 80 | 81 | BINARY_URL="${GITHUB_BINARY_BASE}/${VERSION}/local-persist-${OS}-${ARCH}" 82 | BINARY_DEST="/usr/bin/docker-volume-local-persist" 83 | 84 | echo Downloading binary: 85 | echo " From: $BINARY_URL" 86 | echo " To: $BINARY_DEST" 87 | 88 | curl -fLsS "$BINARY_URL" > $BINARY_DEST 89 | chmod +x $BINARY_DEST 90 | 91 | echo Binary download 92 | echo '' 93 | } 94 | 95 | # Systemd (default) 96 | function setup-systemd { 97 | SYSTEMD_CONFIG_URL="${GITHUB_RAW_BASE}/${VERSION}/init/systemd.service" 98 | SYSTEMD_CONFIG_DEST="/etc/systemd/system/docker-volume-local-persist.service" 99 | 100 | echo Downloading Systemd service conf: 101 | echo " From: $SYSTEMD_CONFIG_URL" 102 | echo " To: $SYSTEMD_CONFIG_DEST" 103 | 104 | sudo curl -fLsS "$SYSTEMD_CONFIG_URL" > $SYSTEMD_CONFIG_DEST 105 | 106 | echo Systemd conf downloaded 107 | echo '' 108 | } 109 | 110 | function start-systemd { 111 | echo Starting docker-volume-local-persist service... 112 | 113 | sudo systemctl daemon-reload 114 | sudo systemctl enable docker-volume-local-persist 115 | sudo systemctl start docker-volume-local-persist 116 | sudo systemctl status --full --no-pager docker-volume-local-persist 117 | 118 | echo '' 119 | echo Done! If you see this message, that should mean everything is installed and is running. 120 | } 121 | 122 | # Upstart 123 | function setup-upstart { 124 | UPSTART_CONFIG_URL="${GITHUB_RAW_BASE}/${VERSION}/init/upstart.conf" 125 | UPSTART_CONFIG_DEST="/etc/init/docker-volume-local-persist.conf" 126 | 127 | echo Downloading binary: 128 | echo " From: $UPSTART_CONFIG_URL" 129 | echo " To: $UPSTART_CONFIG_DEST" 130 | 131 | sudo curl -fLsS "$UPSTART_CONFIG_URL" > $UPSTART_CONFIG_DEST 132 | 133 | echo Upstart conf downloaded 134 | echo '' 135 | } 136 | 137 | function start-upstart { 138 | echo Reloading Upstart config and starting docker-volume-local-persist service... 139 | 140 | sudo initctl reload-configuration 141 | sudo service docker-volume-local-persist start 142 | sudo service docker-volume-local-persist status 143 | 144 | echo '' 145 | echo Done! If you see this message, that should mean everything is installed and is running. 146 | } 147 | 148 | 149 | setenv 150 | 151 | if [[ $* == *--upstart* ]]; then 152 | install-binary --upstart 153 | setup-upstart 154 | start-upstart 155 | else 156 | install-binary 157 | setup-systemd 158 | start-systemd 159 | fi 160 | -------------------------------------------------------------------------------- /scripts/integration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | function create-volume { 6 | VOLUME=`docker volume create --driver=local-persist --opt mountpoint=/tmp/local-persist-integration/ --name=test-data` 7 | } 8 | 9 | function create-containers { 10 | ONE=`docker run -d -v test-data:/app/data/ alpine sleep 30` 11 | TWO=`docker run -d -v test-data:/src/data/ alpine sleep 30` 12 | } 13 | 14 | function check-containers { 15 | (docker exec $ONE cat /app/data/test.txt | grep 'Cameron Spear') || exit 111 16 | (docker exec $TWO cat /src/data/test.txt | grep 'Cameron Spear') || exit 222 17 | } 18 | 19 | function clean { 20 | docker rm -f $ONE 21 | docker rm -f $TWO 22 | docker volume rm $VOLUME 23 | } 24 | 25 | # setup 26 | create-volume 27 | create-containers 28 | 29 | # copy a test file (note how this subtly breaks integration tests if my name is removed from the LICENSE ;-)) 30 | docker cp LICENSE $ONE:/app/data/test.txt 31 | 32 | # check that the file exists in both 33 | check-containers 34 | 35 | # delete everything (start over point) 36 | clean 37 | 38 | 39 | # do it all again, but this time, DON'T manually copy a file... it should have persisted from before! 40 | create-volume 41 | create-containers 42 | 43 | # if we were just using the `local` driver, this step would fail 44 | check-containers 45 | 46 | clean 47 | 48 | echo -e "\nSuccess!" 49 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | black='\033[0;30m' # Black 4 | red='\033[0;31m' # Red 5 | green='\033[0;32m' # Green 6 | yellow='\033[0;33m' # Yellow 7 | blue='\033[0;34m' # Blue 8 | purple='\033[0;35m' # Purple 9 | cyan='\033[0;36m' # Cyan 10 | white='\033[0;37m' # White 11 | nocolor='\033[0m' # No Color 12 | 13 | 14 | USER=cwspear 15 | REPO=local-persist 16 | 17 | # check to make sure github-release is installed! 18 | github-release --version > /dev/null || exit 19 | 20 | if [[ $RELEASE_NAME == "" ]]; then 21 | echo -e ${cyan}Enter release name:${nocolor} 22 | read RELEASE_NAME 23 | echo '' 24 | fi 25 | 26 | if [[ $RELEASE_DESCRIPTION == "" ]]; then 27 | echo -e ${cyan}Enter release description:${nocolor} 28 | read RELEASE_DESCRIPTION 29 | echo '' 30 | fi 31 | 32 | if [[ $RELEASE_TAG == "" ]]; then 33 | printf "${cyan}Enter release tag:${nocolor} v" 34 | read RELEASE_TAG 35 | echo '' 36 | 37 | RELEASE_TAG="v${RELEASE_TAG}" 38 | fi 39 | 40 | if [[ $PRERELEASE == "" ]]; then 41 | printf "${cyan}Is this a prerelease? [yN]${nocolor} " 42 | read PRERELEASE_REPLY 43 | echo '' 44 | 45 | FIRST=${PRERELEASE_REPLY:0:1} 46 | echo $FIRST 47 | 48 | PRERELEASE=false 49 | [[ $FIRST == 'Y' || $FIRST == 'y' ]] && PRERELEASE=true 50 | fi 51 | 52 | sed -i '' "s|VERSION=\".*\"|VERSION=\"${RELEASE_TAG}\"|" scripts/install.sh 53 | sed -i '' "s|ENV VERSION .*|ENV VERSION ${RELEASE_TAG}|" Dockerfile 54 | 55 | git commit -am "Tagged ${RELEASE_TAG}" 56 | git push 57 | git tag -a $RELEASE_TAG -m "$RELEASE_NAME" 58 | git push --tags 59 | 60 | echo '' 61 | echo Releasing... 62 | echo '' 63 | echo USER=$USER 64 | echo REPO=$REPO 65 | echo RELEASE_NAME="'$RELEASE_NAME'" 66 | echo RELEASE_DESCRIPTION="'$RELEASE_DESCRIPTION'" 67 | echo RELEASE_TAG=$RELEASE_TAG 68 | echo PRERELEASE=$PRERELEASE 69 | echo '' 70 | 71 | if [[ "$PRERELEASE" == true ]]; then 72 | github-release release \ 73 | --user $USER \ 74 | --repo $REPO \ 75 | --tag $RELEASE_TAG \ 76 | --name "$RELEASE_NAME" \ 77 | --description "$RELEASE_DESCRIPTION" \ 78 | --pre-release 79 | else 80 | github-release release \ 81 | --user $USER \ 82 | --repo $REPO \ 83 | --tag $RELEASE_TAG \ 84 | --name "$RELEASE_NAME" \ 85 | --description "$RELEASE_DESCRIPTION" 86 | fi 87 | 88 | echo Uploading binaries... 89 | for FILE in `find bin -type f`; do 90 | NAME=${FILE/bin\//} 91 | NAME=${NAME//\//-} 92 | NAME=`echo $NAME | sed 's/\(.*\)-local-persist/local-persist-\1/'` 93 | 94 | echo Uploading ${NAME}... 95 | 96 | if [[ $NAME != "" ]]; then 97 | github-release upload \ 98 | --user $USER \ 99 | --repo $REPO \ 100 | --tag $RELEASE_TAG \ 101 | --name $NAME \ 102 | --file $FILE 103 | fi 104 | done 105 | --------------------------------------------------------------------------------