├── .gitignore ├── do_client_factory.go ├── mount_util.go ├── main.go ├── do_facade.go ├── driver.go ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /docker-volume-plugin-dostorage 2 | -------------------------------------------------------------------------------- /do_client_factory.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/digitalocean/godo" 5 | "golang.org/x/oauth2" 6 | ) 7 | 8 | func NewDoAPIClient(accessToken string) *godo.Client { 9 | accessTokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: accessToken}) 10 | 11 | oauthClient := oauth2.NewClient(oauth2.NoContext, accessTokenSource) 12 | client := godo.NewClient(oauthClient) 13 | 14 | return client 15 | } 16 | -------------------------------------------------------------------------------- /mount_util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | ) 6 | 7 | const mountDevicePrefix = "/dev/disk/by-id/scsi-0DO_Volume_" 8 | 9 | type MountUtil struct { 10 | } 11 | 12 | func NewMountUtil() *MountUtil { 13 | return &MountUtil{} 14 | } 15 | 16 | func (m MountUtil) MountVolume(volumeName string, mountpoint string) error { 17 | cmd := exec.Command("mount", mountDevicePrefix+volumeName, mountpoint) 18 | return cmd.Run() 19 | } 20 | 21 | func (m MountUtil) UnmountVolume(volumeName string, mountpoint string) error { 22 | cmd := exec.Command("umount", mountpoint) 23 | return cmd.Run() 24 | } 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log/syslog" 6 | "os" 7 | 8 | "github.com/Sirupsen/logrus" 9 | logrus_syslog "github.com/Sirupsen/logrus/hooks/syslog" 10 | "github.com/digitalocean/go-metadata" 11 | "github.com/docker/go-plugins-helpers/volume" 12 | flag "github.com/ogier/pflag" 13 | ) 14 | 15 | const ( 16 | DefaultBaseMetadataPath = "/etc/docker/plugins/dostorage/volumes" 17 | DefaultBaseMountPath = "/mnt/dostorage" 18 | DefaultUnixSocketGroup = "docker" 19 | ) 20 | 21 | var ( 22 | // will be set if built using govvv 23 | GitSummary string 24 | DriverVersion = GitSummary 25 | ) 26 | 27 | type CommandLineArgs struct { 28 | accessToken *string 29 | metadataPath *string 30 | mountPath *string 31 | unixSocketGroup *string 32 | version *bool 33 | } 34 | 35 | func main() { 36 | configureLogging() 37 | 38 | args := parseCommandLineArgs() 39 | 40 | doMetadataClient := metadata.NewClient() 41 | doAPIClient := NewDoAPIClient(*args.accessToken) 42 | doFacade := NewDoFacade(doMetadataClient, doAPIClient) 43 | 44 | mountUtil := NewMountUtil() 45 | 46 | driver, derr := NewDriver(doFacade, mountUtil, *args.metadataPath, *args.mountPath) 47 | if derr != nil { 48 | logrus.Fatalf("failed to create the driver: %v", derr) 49 | os.Exit(1) 50 | } 51 | 52 | handler := volume.NewHandler(driver) 53 | 54 | serr := handler.ServeUnix(*args.unixSocketGroup, DriverName) 55 | if serr != nil { 56 | logrus.Fatalf("failed to bind to the Unix socket: %v", serr) 57 | os.Exit(1) 58 | } 59 | 60 | for { 61 | // block while requests are served in a separate routine 62 | } 63 | } 64 | 65 | func configureLogging() { 66 | syslogHook, herr := logrus_syslog.NewSyslogHook("", "", syslog.LOG_INFO, DriverName) 67 | if herr == nil { 68 | logrus.AddHook(syslogHook) 69 | } else { 70 | logrus.Warn("it was not possible to activate logging to the local syslog") 71 | } 72 | } 73 | 74 | func parseCommandLineArgs() *CommandLineArgs { 75 | args := &CommandLineArgs{} 76 | 77 | args.accessToken = flag.StringP("access-token", "t", "", "the DigitalOcean API access token") 78 | args.metadataPath = flag.String("metadata-path", DefaultBaseMetadataPath, "the path under which to store volume metadata") 79 | args.mountPath = flag.StringP("mount-path", "m", DefaultBaseMountPath, "the path under which to create the volume mount folders") 80 | args.unixSocketGroup = flag.StringP("unix-socket-group", "g", DefaultUnixSocketGroup, "the group to assign to the Unix socket file") 81 | args.version = flag.Bool("version", false, "outputs the driver version and exits") 82 | flag.Parse() 83 | 84 | if *args.version { 85 | fmt.Printf("%v\n", DriverVersion) 86 | os.Exit(0) 87 | } 88 | 89 | if *args.accessToken == "" { 90 | flag.Usage() 91 | os.Exit(1) 92 | } 93 | 94 | return args 95 | } 96 | -------------------------------------------------------------------------------- /do_facade.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/Sirupsen/logrus" 8 | "github.com/digitalocean/go-metadata" 9 | "github.com/digitalocean/godo" 10 | ) 11 | 12 | const ( 13 | StorageActionRetryCount = 3 14 | StorageActionRetryInterval = 1000 * time.Millisecond 15 | StorageActionCompletionPollCount = 60 16 | StorageActionCompletionPollInterval = 500 * time.Millisecond 17 | MaxResultsPerPage = 200 18 | ) 19 | 20 | type DoFacade struct { 21 | metadataClient *metadata.Client 22 | apiClient *godo.Client 23 | } 24 | 25 | func NewDoFacade(metadataClient *metadata.Client, apiClient *godo.Client) *DoFacade { 26 | return &DoFacade{ 27 | metadataClient: metadataClient, 28 | apiClient: apiClient, 29 | } 30 | } 31 | 32 | func (s DoFacade) GetLocalRegion() (string, error) { 33 | return s.metadataClient.Region() 34 | } 35 | 36 | func (s DoFacade) GetLocalDropletID() (int, error) { 37 | return s.metadataClient.DropletID() 38 | } 39 | 40 | func (s DoFacade) GetVolume(volumeID string) (*godo.Volume, error) { 41 | doVolume, _, err := s.apiClient.Storage.GetVolume(volumeID) 42 | return doVolume, err 43 | } 44 | 45 | func (s DoFacade) GetVolumeByRegionAndName(region string, name string) *godo.Volume { 46 | doVolumes, _, err := s.apiClient.Storage.ListVolumes(&godo.ListOptions{Page: 1, PerPage: MaxResultsPerPage}) 47 | 48 | if err != nil { 49 | logrus.Errorf("failed to get the volume by region and name: %v", err) 50 | return nil 51 | } 52 | 53 | for i := range doVolumes { 54 | if doVolumes[i].Region.Slug == region && doVolumes[i].Name == name { 55 | return &doVolumes[i] 56 | } 57 | } 58 | return nil 59 | } 60 | 61 | func (s DoFacade) IsVolumeAttachedToDroplet(volumeID string, dropletID int) bool { 62 | doVolume, _, err := s.apiClient.Storage.GetVolume(volumeID) 63 | 64 | if err != nil { 65 | logrus.Errorf("failed to get the volume: %v", err) 66 | return false 67 | } 68 | 69 | for _, attachedDropletID := range doVolume.DropletIDs { 70 | if attachedDropletID == dropletID { 71 | return true 72 | } 73 | } 74 | return false 75 | } 76 | 77 | func (s DoFacade) DetachVolumeFromAllDroplets(volumeID string) error { 78 | logrus.Infof("detaching the volume '%v' from all droplets", volumeID) 79 | 80 | attachedDropletIDs := s.getAttachedDroplets(volumeID) 81 | for _, attachedDropletID := range attachedDropletIDs { 82 | derr := s.DetachVolumeFromDroplet(volumeID, attachedDropletID) 83 | if derr != nil { 84 | return derr 85 | } 86 | } 87 | return nil 88 | } 89 | 90 | func (s DoFacade) DetachVolumeFromDroplet(volumeID string, dropletID int) error { 91 | logrus.Infof("detaching the volume from the droplet %v", dropletID) 92 | 93 | var lastErr error 94 | 95 | for i := 1; i <= StorageActionRetryCount; i++ { 96 | action, _, derr := s.apiClient.StorageActions.DetachByDropletID(volumeID, dropletID) 97 | if derr != nil { 98 | logrus.Errorf("failed to detach the volume: %v", lastErr) 99 | time.Sleep(StorageActionRetryInterval) 100 | lastErr = derr 101 | } else { 102 | lastErr = s.waitForVolumeActionToComplete(volumeID, action.ID) 103 | break 104 | } 105 | } 106 | 107 | return lastErr 108 | } 109 | 110 | func (s DoFacade) AttachVolumeToDroplet(volumeID string, dropletID int) error { 111 | logrus.Infof("detaching the volume '%v' from the droplet %v", dropletID) 112 | 113 | var lastErr error 114 | 115 | for i := 1; i <= StorageActionRetryCount; i++ { 116 | action, _, aerr := s.apiClient.StorageActions.Attach(volumeID, dropletID) 117 | if aerr != nil { 118 | logrus.Errorf("failed to attach the volume: %v", aerr) 119 | time.Sleep(StorageActionRetryInterval) 120 | lastErr = aerr 121 | } else { 122 | lastErr = s.waitForVolumeActionToComplete(volumeID, action.ID) 123 | break 124 | } 125 | } 126 | 127 | return lastErr 128 | } 129 | 130 | func (s DoFacade) getAttachedDroplets(volumeID string) []int { 131 | doVolume, _, err := s.apiClient.Storage.GetVolume(volumeID) 132 | if err != nil { 133 | logrus.Errorf("Error getting the volume: %v", err.Error()) 134 | return []int{} 135 | } 136 | return doVolume.DropletIDs 137 | } 138 | 139 | func (s DoFacade) waitForVolumeActionToComplete(volumeID string, actionID int) error { 140 | logrus.Infof("waiting for the storage action %v to complete", actionID) 141 | 142 | lastStatus := "n/a" 143 | 144 | for i := 1; i <= StorageActionCompletionPollCount; i++ { 145 | action, _, aerr := s.apiClient.StorageActions.Get(volumeID, actionID) 146 | if aerr == nil { 147 | lastStatus = action.Status 148 | if action.Status == "completed" || action.Status == "errored" { 149 | break 150 | } 151 | } else { 152 | logrus.Errorf("failed to query the storage action: %v", aerr) 153 | } 154 | time.Sleep(StorageActionCompletionPollInterval) 155 | } 156 | 157 | if lastStatus == "completed" { 158 | logrus.Info("the action completed") 159 | return nil 160 | } 161 | 162 | logrus.Errorf("the action did not complete but ended with status '%v'", lastStatus) 163 | return fmt.Errorf("the action did not complete but ended with status '%v'", lastStatus) 164 | } 165 | -------------------------------------------------------------------------------- /driver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "sync" 9 | 10 | "github.com/Sirupsen/logrus" 11 | "github.com/docker/go-plugins-helpers/volume" 12 | ) 13 | 14 | const ( 15 | DriverName = "dostorage" 16 | MetadataDirMode = 0700 17 | MetadataFileMode = 0600 18 | MountDirMode = os.ModeDir 19 | ) 20 | 21 | type Driver struct { 22 | region string 23 | dropletID int 24 | volumes map[string]*VolumeState 25 | baseMetadataPath string 26 | baseMountPath string 27 | doFacade *DoFacade 28 | mountUtil *MountUtil 29 | m *sync.Mutex 30 | } 31 | 32 | type VolumeState struct { 33 | doVolumeID string 34 | mountpoint string 35 | referenceCount int 36 | } 37 | 38 | func NewDriver(doFacade *DoFacade, mountUtil *MountUtil, baseMetadataPath string, baseMountPath string) (*Driver, error) { 39 | logrus.Info("creating a new driver instance") 40 | 41 | region, rerr := doFacade.GetLocalRegion() 42 | if rerr != nil { 43 | return nil, rerr 44 | } 45 | 46 | dropletID, derr := doFacade.GetLocalDropletID() 47 | if derr != nil { 48 | return nil, derr 49 | } 50 | 51 | merr := os.MkdirAll(baseMetadataPath, MetadataDirMode) 52 | if merr != nil { 53 | return nil, merr 54 | } 55 | 56 | terr := os.MkdirAll(baseMountPath, MountDirMode) 57 | if terr != nil { 58 | return nil, terr 59 | } 60 | 61 | logrus.Infof("droplet metadata: region='%v', dropletID=%v", region, dropletID) 62 | 63 | driver := &Driver{ 64 | region: region, 65 | dropletID: dropletID, 66 | volumes: make(map[string]*VolumeState), 67 | baseMetadataPath: baseMetadataPath, 68 | baseMountPath: baseMountPath, 69 | doFacade: doFacade, 70 | mountUtil: mountUtil, 71 | m: &sync.Mutex{}, 72 | } 73 | 74 | ierr := driver.initVolumesFromMetadata() 75 | if ierr != nil { 76 | return nil, ierr 77 | } 78 | 79 | return driver, nil 80 | } 81 | 82 | func (d Driver) Create(r volume.Request) volume.Response { 83 | logrus.Infof("[Create]: %+v", r) 84 | 85 | d.m.Lock() 86 | defer d.m.Unlock() 87 | 88 | volumeState, ierr := d.initVolume(r.Name) 89 | if ierr != nil { 90 | return volume.Response{Err: ierr.Error()} 91 | } 92 | 93 | metadataFilePath := filepath.Join(d.baseMetadataPath, r.Name) 94 | 95 | metadataFile, ferr := os.Create(metadataFilePath) 96 | if ferr != nil { 97 | logrus.Errorf("failed to create metadata file '%v' for volume '%v'", metadataFilePath, r.Name) 98 | return volume.Response{Err: ferr.Error()} 99 | } 100 | 101 | cerr := metadataFile.Chmod(MetadataFileMode) 102 | if cerr != nil { 103 | os.Remove(metadataFilePath) 104 | logrus.Errorf("failed to change the mode for the metadata file '%v' for volume '%v'", metadataFilePath, r.Name) 105 | return volume.Response{Err: cerr.Error()} 106 | } 107 | 108 | d.volumes[r.Name] = volumeState 109 | 110 | return volume.Response{} 111 | } 112 | 113 | func (d Driver) List(r volume.Request) volume.Response { 114 | logrus.Infof("[List]: %+v", r) 115 | 116 | volumes := []*volume.Volume{} 117 | 118 | for name, state := range d.volumes { 119 | volumes = append(volumes, &volume.Volume{ 120 | Name: name, 121 | Mountpoint: state.mountpoint, 122 | }) 123 | } 124 | 125 | return volume.Response{Volumes: volumes} 126 | } 127 | 128 | func (d Driver) Get(r volume.Request) volume.Response { 129 | logrus.Infof("[Get]: %+v", r) 130 | 131 | if state, ok := d.volumes[r.Name]; ok { 132 | status := make(map[string]interface{}) 133 | doVolume, verr := d.doFacade.GetVolume(state.doVolumeID) 134 | if verr == nil { 135 | status["VolumeID"] = state.doVolumeID 136 | status["ReferenceCount"] = state.referenceCount 137 | status["AttachedDropletIDs"] = doVolume.DropletIDs 138 | } else { 139 | logrus.Errorf("failed to get the volume with ID '%v': %v", state.doVolumeID, verr) 140 | status["Err"] = fmt.Sprintf("failed to get the volume with ID '%v': %v", state.doVolumeID, verr) 141 | } 142 | 143 | return volume.Response{ 144 | Volume: &volume.Volume{ 145 | Name: r.Name, 146 | Mountpoint: state.mountpoint, 147 | Status: status, 148 | }, 149 | } 150 | } 151 | 152 | logrus.Infof("volume named '%v' not found", r.Name) 153 | return volume.Response{Err: fmt.Sprintf("volume named '%v' not found", r.Name)} 154 | } 155 | 156 | func (d Driver) Remove(r volume.Request) volume.Response { 157 | logrus.Infof("[Remove]: %+v", r) 158 | 159 | d.m.Lock() 160 | defer d.m.Unlock() 161 | 162 | if _, ok := d.volumes[r.Name]; ok { 163 | metadataFilePath := filepath.Join(d.baseMetadataPath, r.Name) 164 | 165 | rerr := os.Remove(metadataFilePath) 166 | if rerr != nil { 167 | logrus.Errorf("failed to delete metadata file '%v' for volume '%v", metadataFilePath, r.Name) 168 | return volume.Response{Err: fmt.Sprintf("failed to delete metadata file '%v' for volume '%v", metadataFilePath, r.Name)} 169 | } 170 | 171 | delete(d.volumes, r.Name) 172 | 173 | return volume.Response{} 174 | } 175 | 176 | logrus.Errorf("volume named '%v' not found", r.Name) 177 | return volume.Response{Err: fmt.Sprintf("volume named '%v' not found", r.Name)} 178 | } 179 | 180 | func (d Driver) Path(r volume.Request) volume.Response { 181 | logrus.Infof("[Path]: %+v", r) 182 | 183 | if state, ok := d.volumes[r.Name]; ok { 184 | return volume.Response{ 185 | Mountpoint: state.mountpoint, 186 | } 187 | } 188 | 189 | logrus.Errorf("volume named '%v' not found", r.Name) 190 | return volume.Response{Err: fmt.Sprintf("volume named '%v' not found", r.Name)} 191 | } 192 | 193 | func (d Driver) Mount(r volume.MountRequest) volume.Response { 194 | logrus.Infof("[Mount]: %+v", r) 195 | 196 | d.m.Lock() 197 | defer d.m.Unlock() 198 | 199 | if state, ok := d.volumes[r.Name]; ok { 200 | state.referenceCount++ 201 | 202 | if state.referenceCount == 1 { 203 | logrus.Info("mounting the volume upon detecting the first reference") 204 | 205 | if !d.doFacade.IsVolumeAttachedToDroplet(state.doVolumeID, d.dropletID) { 206 | logrus.Info("attaching the volume to this droplet") 207 | 208 | d.doFacade.DetachVolumeFromAllDroplets(state.doVolumeID) 209 | aerr := d.doFacade.AttachVolumeToDroplet(state.doVolumeID, d.dropletID) 210 | if aerr != nil { 211 | logrus.Errorf("failed to attach the volume to this droplet: %v", aerr) 212 | return volume.Response{Err: fmt.Sprintf("failed to attach the volume to this droplet: %v", aerr)} 213 | } 214 | } 215 | 216 | merr := d.mountUtil.MountVolume(r.Name, state.mountpoint) 217 | if merr != nil { 218 | logrus.Errorf("failed to mount the volume: %v", merr) 219 | return volume.Response{Err: fmt.Sprintf("failed to mount the volume: %v", merr)} 220 | } 221 | } 222 | 223 | return volume.Response{ 224 | Mountpoint: state.mountpoint, 225 | } 226 | } 227 | 228 | logrus.Errorf("volume named '%v' not found", r.Name) 229 | return volume.Response{Err: fmt.Sprintf("volume named '%v' not found", r.Name)} 230 | } 231 | 232 | func (d Driver) Unmount(r volume.UnmountRequest) volume.Response { 233 | logrus.Infof("[Unmount]: %+v", r) 234 | 235 | d.m.Lock() 236 | defer d.m.Unlock() 237 | 238 | if state, ok := d.volumes[r.Name]; ok { 239 | state.referenceCount-- 240 | 241 | if state.referenceCount == 0 { 242 | logrus.Info("unmounting the volume since it is not referenced anymore") 243 | 244 | merr := d.mountUtil.UnmountVolume(r.Name, state.mountpoint) 245 | if merr != nil { 246 | logrus.Errorf("failed to unmount the volume: %v", merr) 247 | return volume.Response{Err: fmt.Sprintf("failed to unmount the volume: %v", merr)} 248 | } 249 | } 250 | } 251 | 252 | return volume.Response{} 253 | } 254 | 255 | func (d Driver) Capabilities(r volume.Request) volume.Response { 256 | logrus.Infof("[Capabilities]: %+v", r) 257 | 258 | return volume.Response{ 259 | Capabilities: volume.Capability{Scope: "local"}, 260 | } 261 | } 262 | 263 | func (d Driver) initVolumesFromMetadata() error { 264 | metadataFiles, ferr := ioutil.ReadDir(d.baseMetadataPath) 265 | if ferr != nil { 266 | return ferr 267 | } 268 | 269 | for _, metadataFile := range metadataFiles { 270 | volumeName := metadataFile.Name() 271 | metadataFilePath := filepath.Join(d.baseMetadataPath, volumeName) 272 | 273 | logrus.Infof("Initializing volume '%v' from metadata file '%v'", volumeName, metadataFilePath) 274 | 275 | volumeState, ierr := d.initVolume(volumeName) 276 | if ierr != nil { 277 | return ierr 278 | } 279 | 280 | d.volumes[volumeName] = volumeState 281 | } 282 | 283 | return nil 284 | } 285 | 286 | func (d Driver) initVolume(name string) (*VolumeState, error) { 287 | doVolume := d.doFacade.GetVolumeByRegionAndName(d.region, name) 288 | if doVolume == nil { 289 | logrus.Errorf("DigitalOcean volume not found for region '%v' and name '%v'", d.region, name) 290 | return nil, fmt.Errorf("DigitalOcean volume not found for region '%v' and name '%v'", d.region, name) 291 | } 292 | 293 | volumePath := filepath.Join(d.baseMountPath, name) 294 | 295 | merr := os.MkdirAll(volumePath, MountDirMode) 296 | if merr != nil { 297 | logrus.Errorf("failed to create the volume mount path '%v'", volumePath) 298 | return nil, fmt.Errorf("failed to create the volume mount path '%v'", volumePath) 299 | } 300 | 301 | d.mountUtil.UnmountVolume(name, volumePath) 302 | 303 | volumeState := &VolumeState{ 304 | doVolumeID: doVolume.ID, 305 | mountpoint: volumePath, 306 | referenceCount: 0, 307 | } 308 | 309 | return volumeState, nil 310 | } 311 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Docker Volume Driver for DigitalOcean 2 | ===================================== 3 | 4 | This repo hosts the Docker Volume Driver for DigitalOcean. The driver is based on the [Docker Volume Plugin framework](https://docs.docker.com/engine/extend/plugins_volume/) and it integrates DigitalOcean's [block storage solution](https://www.digitalocean.com/products/storage/) into the Docker ecosystem by automatically attaching a given block storage volume to a DigitalOcean droplet and making the contents of the volume available to Docker containers running on that droplet. 5 | 6 | 7 | ## Download 8 | 9 | The driver is written in Go and it consists of a single static binary which can be downloaded from the [releases](https://github.com/omallo/docker-volume-plugin-dostorage/releases) page. Appropriate binaries are made available for different Linux platforms and architectures. 10 | 11 | 12 | ## Installation 13 | 14 | For installing the driver on a DigitalOcean droplet, you will need the following before proceeding with the subsequent steps: 15 | - You need to have SSH access to your droplet. The subsequent commands should all be executed on the droplet's command line. 16 | - You need to have an [API access token](https://cloud.digitalocean.com/settings/api/tokens) which is used by the driver to access the DigitalOcean REST API. 17 | 18 | First, you have to download the driver's binary to the droplet and make it executable (make sure you download the binary for the appropriate release version and Linux platform/architecture): 19 | ```sh 20 | sudo curl \ 21 | -sSL \ 22 | -o /usr/bin/docker-volume-plugin-dostorage \ 23 | https://github.com/omallo/docker-volume-plugin-dostorage/releases/download/v0.4.0/docker-volume-plugin-dostorage_linux_amd64 24 | 25 | sudo chmod +x /usr/bin/docker-volume-plugin-dostorage 26 | ``` 27 | 28 | Once downloaded, the driver can be started in the background as follows by providing your DigitalOcean API access token: 29 | ```sh 30 | sudo docker-volume-plugin-dostorage --access-token= & 31 | ``` 32 | 33 | Other command line arguments supported by the driver can be shown by invoking the driver without any argument: 34 | ```sh 35 | sudo docker-volume-plugin-dostorage 36 | 37 | Usage of docker-volume-plugin-dostorage: 38 | -t, --access-token string 39 | the DigitalOcean API access token 40 | --metadata-path string 41 | the path under which to store volume metadata (default "/etc/docker/plugins/dostorage/volumes") 42 | -m, --mount-path string 43 | the path under which to create the volume mount folders (default "/mnt/dostorage") 44 | -g, --unix-socket-group string 45 | the group to assign to the Unix socket file (default "docker") 46 | --version 47 | outputs the driver version and exits 48 | ``` 49 | 50 | Docker plugins should usually be started before the Docker engine so it is advisable to restart the Docker engine after installing the driver. Depending on your Linux distribution, this can be done using either the `service` command 51 | ```sh 52 | sudo service docker restart 53 | ``` 54 | or the `systemctl` command 55 | ```sh 56 | sudo systemctl restart docker 57 | ``` 58 | 59 | You are now ready to use the driver for your Docker containers! 60 | 61 | 62 | ## Basic Usage 63 | 64 | Before using the driver for your Docker containers, you must create a [DigitalOcean volume](https://cloud.digitalocean.com/droplets/volumes). For the subsequent steps, we assume a DigitalOcean volume named `myvol-01`. As of now, the driver does not support volumes with multiple partitions so it is assumed that the volume consists of a single partition which you might have created e.g. as follows: 65 | ```sh 66 | sudo mkfs.ext4 -F /dev/disk/by-id/scsi-0DO_Volume_myvol-01 67 | ``` 68 | An in-depth description on how to create and format DigitalOcean volumes can be found [here](https://www.digitalocean.com/community/tutorials/how-to-use-block-storage-on-digitalocean). Please note that a DigitalOcean volume must be created and formatted manually before it can be integrated into Docker using the driver. 69 | 70 | Once you have created and formatted your DigitalOcean volume, you can create a Docker volume using the same name (assuming a DigitalOcean volume named `myvol-01`): 71 | ```sh 72 | docker volume create --driver dostorage --name myvol-01 73 | ``` 74 | 75 | Once the Docker volume was created, you can use it for your containers. E.g. you can list the contents of your DigitalOcean volume by mapping it to the container path `/mydata` as follows: 76 | ```sh 77 | docker run --rm --volume myvol-01:/mydata busybox ls -la /mydata 78 | ``` 79 | 80 | You can also start an interactive shell on your container and access the contents of your DigitalOcean volume from within your container: 81 | ```sh 82 | docker run -it --volume myvol-01:/mydata busybox sh 83 | 84 | # the following commands are executed within the container's shell 85 | ls -la /mydata 86 | echo "hello world" >/mydata/greeting.txt 87 | cat /mydata/greeting.txt 88 | exit 89 | ``` 90 | Since all the changes made within the cotnainer's `/mydata` path are performed on the DigitalOcean volume storage device, you will not loose the changes even if you later attach the DigitalOcean volume to a different droplet. 91 | 92 | The current status of the Docker volume can be inspected using the following command: 93 | ```sh 94 | docker volume inspect myvol-01 95 | ``` 96 | 97 | The inspection command will return a result similar to the following: 98 | ```json 99 | [ 100 | { 101 | "Name": "myvol-01", 102 | "Driver": "dostorage", 103 | "Mountpoint": "/mnt/dostorage/myvol-01", 104 | "Status": { 105 | "AttachedDropletIDs": [ 106 | 2.5355869e+07 107 | ], 108 | "ReferenceCount": 0, 109 | "VolumeID": "0b3aef8c-7767-11e6-a7c4-000f53315860" 110 | }, 111 | "Labels": {}, 112 | "Scope": "local" 113 | } 114 | ] 115 | ``` 116 | 117 | Apart from the standard inspection information like the local mountpoint path, the result contains a `Status` field with the following information (the status field is only supported with Docker version >=1.12.0): 118 | - `VolumeID`: The ID of the DigitalOcean volume. 119 | - `AttachedDropletIDs`: The IDs of the droplets to which the DigitalOcean volume is currently attached (at most 1). 120 | - `ReferenceCount`: The number of running Docker containers which are using the volume. 121 | 122 | 123 | ## Docker Swarm Usage 124 | 125 | If you use Docker in [swarm mode](https://docs.docker.com/engine/swarm/) with a cluster of droplets, you can use the driver in very much the same way as with a single droplet. The following things should be considered when using a DigitalOcean volume in a Docker cluster: 126 | - The Docker volume must be created on every Docker host separately (using `docker volume create` as described above). 127 | - The driver takes care of attaching a DigitalOcean volume to the appropriate droplet when you start a container which uses that volume on the droplet (and possibly detaching it from any other droplet). 128 | - A DigitalOcean volume can only be attached to a single droplet at the same time. For that reason, you must not run Docker containers concurrently on different hosts which use the same DigitalOcean volume. 129 | 130 | 131 | ## Logging 132 | 133 | The driver logs to the STDOUT as well as to the local `syslog` instance (if supported). Syslog logging uses the `dostorage` tag. 134 | 135 | 136 | ## Systemd Integration 137 | 138 | It is advisable to use `systemd` to manage the startup and shutdown of the driver. Details on how to configure `systemd` for a Docker plugin, can be found [here](https://docs.docker.com/engine/extend/plugin_api/). The following are some *sample* `systemd` configuration files you can use as a starting point: 139 | - The following `dostorage.service` unit file can be used to automate the execution of the driver: 140 | ``` 141 | [Unit] 142 | Description=Docker Volume Driver for DigitalOcean 143 | Before=docker.service 144 | After=network.target dostorage.socket 145 | Requires=dostorage.socket docker.service 146 | 147 | [Service] 148 | ExecStart=/usr/bin/docker-volume-plugin-dostorage --access-token= 149 | 150 | [Install] 151 | WantedBy=multi-user.target 152 | ``` 153 | - The following `dostorage.socket` unit file can be used to make use of socket activation for executing the driver lazily: 154 | ``` 155 | [Unit] 156 | Description=Socket for Docker Volume Driver for DigitalOcean 157 | 158 | [Socket] 159 | ListenStream=/var/run/docker/plugins/dostorage.sock 160 | SocketUser=root 161 | SocketGroup=docker 162 | SocketMode=0660 163 | 164 | [Install] 165 | WantedBy=sockets.target 166 | ``` 167 | 168 | The `systemd` configuration files can be copied to `/etc/systemd/system` or a similar location, depending on your Linux distribution. You can then activate the services either by directly executing the driver 169 | ```sh 170 | # execute the driver directly 171 | sudo systemctl start dostorage.service 172 | 173 | # enable automated startup on reboot 174 | sudo systemctl enable dostorage.service 175 | ``` 176 | or using socket activation 177 | ```sh 178 | # create the Unix socket which is used to execute the driver on demand only 179 | sudo systemctl start dostorage.socket 180 | 181 | # enable automated startup on reboot 182 | sudo systemctl enable dostorage.socket 183 | ``` 184 | 185 | 186 | ## Known Limitations 187 | 188 | The following are some of the main known limitations of the driver: 189 | - No mount configuration options are currently supported ([issue #1](https://github.com/omallo/docker-volume-plugin-dostorage/issues/1)). 190 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------