Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ CMD ["/go/bin/docker-volume-linode"]

FROM alpine
COPY --from=builder /go/bin/docker-volume-linode .
RUN apk update && apk add ca-certificates e2fsprogs
RUN apk update && apk add ca-certificates e2fsprogs xfsprogs btrfs-progs util-linux
CMD ["./docker-volume-linode"]
8 changes: 3 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ DOCKER_PASSWORD ?= xxxxx

# Test Arguments
TEST_TOKEN ?= xyz
TEST_REGION ?= xyz
TEST_LABEL ?= xyz

GOPATH=$(shell go env GOPATH)
Expand Down Expand Up @@ -82,16 +81,15 @@ test-use-volume:
docker run --rm -i -v test-volume-default-size:/mnt busybox test -f /mnt/abc.txt || false

test-pre-check:
@if [ "${TEST_TOKEN}" = "xyz" ] || [ "${TEST_REGION}" = "xyz" ] || [ "${TEST_LABEL}" = "xyz" ] ; then \
@if [ "${TEST_TOKEN}" = "xyz" ] || [ "${TEST_LABEL}" = "xyz" ] ; then \
echo -en "#############################\nYou must set TEST_* Variables\n#############################\n"; exit 1; fi

test-setup:
@docker plugin set $(PLUGIN_NAME) LINODE_TOKEN=${TEST_TOKEN} LINODE_REGION=${TEST_REGION} LINODE_LABEL=${TEST_LABEL}
docker plugin enable $(PLUGIN_NAME)
@docker plugin set $(PLUGIN_NAME) LINODE_TOKEN=${TEST_TOKEN} LINODE_LABEL=${TEST_LABEL}
docker plugin enable $(PLUGIN_NAME)

check:
# Tools

GO111MODULE=off go get -u github.com/tsenart/deadcode
GO111MODULE=off go get -u github.com/kisielk/errcheck
GO111MODULE=off go get -u golang.org/x/lint/golint
Expand Down
30 changes: 21 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,14 @@ The plugin can also be configured (or reconfigured) in multiple steps.
docker plugin install --alias linode linode/docker-volume-linode
docker plugin disable linode
docker plugin set linode linode-token=<linode token>
docker plugin set linode linode-region=<linode region>
docker plugin set linode linode-label=<linode label>
docker plugin enable linode
```

- \<linode token\>: You will need a Linode APIv4 Personal Access Token. Get one here: <https://developers.linode.com/api/v4#section/Personal-Access-Token>. The API Token must have Read/Write permission for Volumes and Linodes.
- \<linode regions\>: `us-east`, `us-central`, `us-west`, `eu-west`, `eu-central`, `ap-south`, `ap-northeast`. [Some Linode regions do not have Block Storage Volume support](https://www.linode.com/community/questions/344/when-will-block-storage-be-available-in-my-datacenter), such as: `us-southeast` and `ap-northeast-1a`.
- \<linode label\>: The label given to the host Linode Control Panel.
- For a complete list of regions: https://api.linode.com/v4/regions
- For all options see "Driver Options" section
- \<linode label\>: The label given to the host Linode Control Panel. Defaults to the system hostname.
[Some Linode regions do not have Block Storage Volume support](https://www.linode.com/community/questions/344/when-will-block-storage-be-available-in-my-datacenter), such as: `us-southeast` and `ap-northeast-1a`. For a complete list of regions: https://api.linode.com/v4/regions
- For all options see [Driver Options](#Driver-Options) section

### Docker Swarm

Expand All @@ -56,9 +54,17 @@ $ docker volume create -d linode my-test-volume
my-test-volume
```

If a named volume already exists on the Linode account and it is in the same region of the Linode, it will be reattached if possible. A Linode Volume can be attached to a single Linode at a time.

#### Create Options

This driver offers `size` as [driver specific option](https://docs.docker.com/engine/reference/commandline/volume_create/#driver-specific-options). The `size` option specifies the size (in GB) of the volume to be created. Volumes must be at least 10GB in size, so the default is 10GB.
The driver offers [driver specific volume create options](https://docs.docker.com/engine/reference/commandline/volume_create/#driver-specific-options):

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `size` | int | `10` | the size (in GB) of the volume to be created. Volumes must be at least 10GB in size, so the default is 10GB.
| `filesystem` | string | `ext4` | the filesystem argument for `mkfs` when formating the new (raw) volume (xfs, btrfs, ext4)
| `delete-on-remove` | bool | `false`| if the Linode volume should be deleted when removed

```sh
$ docker volume create -o size=50 -d linode my-test-volume-50
Expand All @@ -71,6 +77,13 @@ Volumes can also be created and attached from `docker run`:
docker run -it --rm --mount volume-driver=linode,source=test-vol,destination=/test,volume-opt=size=25 alpine
```

Multiple create options can be supplied:

```sh
docker run -it --rm --mount volume-driver=linode,source=test-vol,destination=/test,volume-opt=size=25,volume-opt=filesystem=btrfs,volume-opt=delete-on-remove=true alpine
```


### List Volumes

```sh
Expand Down Expand Up @@ -103,7 +116,6 @@ my-test-volume-50
| --- | --- |
| linode-token | **Required** The Linode APIv4 [Personal Access Token](https://cloud.linode.com/profile/tokens)
| linode-label | The Linode Label to attach block storage volumes to (defaults to the system hostname) |
| linode-region | The Linode region to create volumes in (inferred if using linode-label, defaults to us-west) |
| socket-file | Sets the socket file/address (defaults to /run/docker/plugins/linode.sock) |
| socket-gid | Sets the socket GID (defaults to 0) |
| mount-root | Sets the root directory for volume mounts (default /mnt) |
Expand All @@ -119,7 +131,7 @@ Options can be set once for all future uses with [`docker plugin set`](https://d
### Run the driver

```sh
docker-volume-linode --linode-token=<token from linode console> --linode-region=<linode region> --linode-label=<linode label>
docker-volume-linode --linode-token=<token from linode console> --linode-label=<linode label>
```

### Debugging
Expand All @@ -135,7 +147,7 @@ docker plugin set docker-volume-linode LOG_LEVEL=debug
#### Enable Debug Level in manual installation

```sh
docker-volume-linode --linode-token=<...> --linode-region=<...> --linode-label=<...> --log-level=debug
docker-volume-linode --linode-token=<...> --linode-label=<...> --log-level=debug
```

## Development
Expand Down
82 changes: 65 additions & 17 deletions driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http"
"os"
"strconv"
"strings"
"sync"

"github.com/docker/docker/api/types/filters"
Expand All @@ -29,15 +30,21 @@ type linodeVolumeDriver struct {
linodeAPIPtr *linodego.Client
}

// Constructor
func newLinodeVolumeDriver(region string, linodeLabel string, linodeToken string) linodeVolumeDriver {
const (
fsTagPrefix = "docker-volume-filesystem-"
)

// Constructor
func newLinodeVolumeDriver(linodeLabel string, linodeToken string) linodeVolumeDriver {
driver := linodeVolumeDriver{
linodeToken: linodeToken,
region: region,
linodeLabel: linodeLabel,
mutex: &sync.Mutex{},
}
if _, err := driver.linodeAPI(); err != nil {
log.Fatalf("Could not initialize Linode API: %s", err)
}

return driver
}

Expand All @@ -50,7 +57,20 @@ func (driver *linodeVolumeDriver) linodeAPI() (*linodego.Client, error) {
return driver.linodeAPIPtr, nil
}

tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: *linodeTokenParamPtr})
driver.linodeAPIPtr = setupLinodeAPI(*linodeTokenParamPtr)

if driver.instanceID == 0 {
if err := driver.determineLinodeID(); err != nil {
driver.linodeAPIPtr = nil
return nil, err
}
}

return driver.linodeAPIPtr, nil
}

func setupLinodeAPI(token string) *linodego.Client {
tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
oauth2Client := &http.Client{
Transport: &oauth2.Transport{
Source: tokenSource,
Expand All @@ -60,14 +80,15 @@ func (driver *linodeVolumeDriver) linodeAPI() (*linodego.Client, error) {
api := linodego.NewClient(oauth2Client)
ua := fmt.Sprintf("docker-volume-linode/%s linodego/%s", VERSION, linodego.Version)
api.SetUserAgent(ua)
return &api
}

driver.linodeAPIPtr = &api

func (driver *linodeVolumeDriver) determineLinodeID() error {
if driver.linodeLabel == "" {
var hostnameErr error
driver.linodeLabel, hostnameErr = os.Hostname()
if hostnameErr != nil {
return nil, fmt.Errorf("Could not determine hostname: %s", hostnameErr)
return fmt.Errorf("Could not determine hostname: %s", hostnameErr)
}
}

Expand All @@ -76,17 +97,16 @@ func (driver *linodeVolumeDriver) linodeAPI() (*linodego.Client, error) {
linodes, lErr := driver.linodeAPIPtr.ListInstances(context.Background(), listOpts)

if lErr != nil {
return nil, fmt.Errorf("Could not determine Linode instance ID from Linode label %s due to error: %s", driver.linodeLabel, lErr)
return fmt.Errorf("Could not determine Linode instance ID from Linode label %s due to error: %s", driver.linodeLabel, lErr)
} else if len(linodes) != 1 {
return nil, fmt.Errorf("Could not determine Linode instance ID from Linode label %s", driver.linodeLabel)
return fmt.Errorf("Could not determine Linode instance ID from Linode label %s", driver.linodeLabel)
}

driver.instanceID = linodes[0].ID
if driver.region == "" {
driver.region = linodes[0].Region
}

return driver.linodeAPIPtr, nil
return nil
}

// Get implementation
Expand Down Expand Up @@ -159,6 +179,7 @@ func (driver *linodeVolumeDriver) Create(req *volume.CreateRequest) error {
defer driver.mutex.Unlock()

var size int

if sizeOpt, ok := req.Options["size"]; ok {
s, err := strconv.Atoi(sizeOpt)
if err != nil {
Expand All @@ -173,6 +194,20 @@ func (driver *linodeVolumeDriver) Create(req *volume.CreateRequest) error {
Size: size,
}

if fsOpt, ok := req.Options["filesystem"]; ok {
createOpts.Tags = append(createOpts.Tags, fsTagPrefix+fsOpt)
}

if deleteOpt, ok := req.Options["delete-on-remove"]; ok {
b, err := strconv.ParseBool(deleteOpt)
if err != nil {
return fmt.Errorf("Invalid delete-on-remove argument")
}
if b {
createOpts.Tags = append(createOpts.Tags, "docker-volume-delete-on-remove")
}
}

if _, err := api.CreateVolume(context.Background(), createOpts); err != nil {
return fmt.Errorf("Create(%s) Failed: %s", req.Name, err)
}
Expand Down Expand Up @@ -203,10 +238,16 @@ func (driver *linodeVolumeDriver) Remove(req *volume.RemoveRequest) error {
return err
}

// Send Delete request
if err := api.DeleteVolume(context.Background(), linVol.ID); err != nil {
return err
// Optionally send Delete request
for _, t := range linVol.Tags {
if t == "docker-volume-delete-on-remove" {
if err := api.DeleteVolume(context.Background(), linVol.ID); err != nil {
return err
}
break
}
}

return nil
}

Expand Down Expand Up @@ -262,14 +303,21 @@ func (driver *linodeVolumeDriver) Mount(req *volume.MountRequest) (*volume.Mount
// else... linode already attached to current host

// wait for kernel to have block device available
if err := waitForDeviceFileExists(linVol.FilesystemPath, 180); err != nil {
if err := waitForDeviceFileExists(linVol.FilesystemPath, 300); err != nil {
return nil, err
}

// Format block device if no FS found
if GetFSType(linVol.FilesystemPath) == "" {
log.Infof("Formatting device:%s;", linVol.FilesystemPath)
if err := Format(linVol.FilesystemPath); err != nil {
filesystem := "ext4"
for _, tag := range linVol.Tags {
if strings.HasPrefix(tag, fsTagPrefix) {
filesystem = tag[len(fsTagPrefix):]
break
}
}
if err := Format(linVol.FilesystemPath, filesystem); err != nil {
return nil, err
}
}
Expand Down Expand Up @@ -376,7 +424,7 @@ func attachAndWait(api *linodego.Client, volumeID int, linodeID int) error {
return fmt.Errorf("Error attaching volume(%d) to linode(%d): %s", volumeID, linodeID, err)
}

if _, err := api.WaitForVolumeLinodeID(context.Background(), volumeID, &linodeID, 180); err != nil {
if _, err := api.WaitForVolumeLinodeID(context.Background(), volumeID, &linodeID, 300); err != nil {
return fmt.Errorf("Error waiting for attachment of volume(%d) to linode(%d): %s", volumeID, linodeID, err)
}
return nil
Expand Down
8 changes: 2 additions & 6 deletions fs_utils_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,9 @@ import (
log "github.com/sirupsen/logrus"
)

const (
formatFSType = "ext4"
)

// Format calls mke2fs on path
func Format(path string) error {
cmd := exec.Command("mke2fs", "-t", formatFSType, path)
func Format(path string, formatFSType string) error {
cmd := exec.Command("mkfs", "-t", formatFSType, path)
stdOutAndErr, err := cmd.CombinedOutput()
log.Debugf("Mke2fs Output:\n%s", stdOutAndErr)
return err
Expand Down
20 changes: 4 additions & 16 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,13 @@ var (
socketAddressParamPtr = cfgString("socket-file", DefaultSocketAddress, "Sets the socket file/address.")
mountRootParamPtr = cfgString("mount-root", MountRoot, "Sets the root directory for volume mounts.")
linodeTokenParamPtr = cfgString("linode-token", "", "Required Personal Access Token generated in Linode Console.")
linodeRegionParamPtr = cfgString("linode-region", "", "Required linode region.")
linodeLabelParamPtr = cfgString("linode-label", "", "Sets the Linode instance label.")
logLevelPtr = cfgString("log-level", "info", "Sets log level debug,info,warn,error")
linodeLabelParamPtr = cfgString("linode-label", "", "Sets the Linode Instance Label (defaults to the OS HOSTNAME)")
logLevelPtr = cfgString("log-level", "info", "Sets log level: debug,info,warn,error")
)

func main() {
//
flag.Parse()

//
log.SetOutput(os.Stdout)
level, err := log.ParseLevel(*logLevelPtr)
Expand All @@ -49,26 +47,16 @@ func main() {

// check required parameters (token, region and label)
if *linodeTokenParamPtr == "" {
log.Error("linode-token is required.")
}

if *linodeRegionParamPtr == "" {
log.Error("linode-region is required.")
}

if *linodeLabelParamPtr == "" {
log.Error("linode-label is required.")
log.Fatal("linode-token is required.")
}

MountRoot = *mountRootParamPtr

//
log.Debugf("linode-token: %s", *linodeTokenParamPtr)
log.Debugf("linode-region: %s", *linodeRegionParamPtr)
log.Debugf("linode-label: %s", *linodeLabelParamPtr)

// Driver instance
driver := newLinodeVolumeDriver(*linodeRegionParamPtr, *linodeLabelParamPtr, *linodeTokenParamPtr)
driver := newLinodeVolumeDriver(*linodeLabelParamPtr, *linodeTokenParamPtr)

// Attach Driver to docker
handler := volume.NewHandler(&driver)
Expand Down