Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Amazon S3 volume plugin #39

Merged
merged 16 commits into from
Oct 7, 2020
1 change: 1 addition & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ build() {
fi
}
build glusterfs-volume-plugin
build s3fs-volume-plugin
build cifs-volume-plugin
build nfs-volume-plugin
build centos-mounted-volume-plugin
6 changes: 4 additions & 2 deletions glusterfs-volume-plugin/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
FROM oraclelinux:7-slim
ENV TINI_VERSION v0.18.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
RUN yum install -q -y oracle-gluster-release-el7 && \
yum install -q -y git glusterfs glusterfs-fuse attr && \
curl --silent -L https://dl.google.com/go/go1.11.5.linux-amd64.tar.gz | tar -C /usr/local -zxf -
curl --silent -L https://dl.google.com/go/go1.15.2.linux-amd64.tar.gz | tar -C /usr/local -zxf -
RUN /usr/local/go/bin/go get github.com/trajano/docker-volume-plugins/glusterfs-volume-plugin && \
mv $HOME/go/bin/glusterfs-volume-plugin / && \
rm -rf $HOME/go /usr/local/go && \
yum remove -q -y git && \
yum autoremove -q -y && \
yum clean all && \
rm -rf /var/cache/yum /var/log/anaconda /var/cache/yum /etc/mtab && \
rm /var/log/lastlog /var/log/tallylog
4 changes: 3 additions & 1 deletion glusterfs-volume-plugin/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"description": "GlusterFS plugin for Docker",
"documentation": "https://github.com/trajano/docker-volume-plugins/",
"entrypoint": [
"/tini",
"--",
"/glusterfs-volume-plugin"
],
"env": [
Expand Down Expand Up @@ -33,4 +35,4 @@
}
]
}
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "docker-volume-plugins",
"version": "2.0.3",
"version": "2.0.4",
"description": "Docker Managed Volume Plugins\r =============================",
"main": "index.js",
"scripts": {
Expand Down
14 changes: 14 additions & 0 deletions s3fs-volume-plugin/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM oraclelinux:7-slim
ENV TINI_VERSION v0.18.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
RUN yum install -q -y oracle-epel-release-el7
RUN yum install -q -y git fuse s3fs-fuse attr && \
curl --silent -L https://dl.google.com/go/go1.15.2.linux-amd64.tar.gz | tar -C /usr/local -zxf -
RUN /usr/local/go/bin/go get github.com/trajano/docker-volume-plugins/s3fs-volume-plugin && \
mv $HOME/go/bin/s3fs-volume-plugin / && \
rm -rf $HOME/go /usr/local/go && \
yum remove -q -y git && \
yum clean all && \
rm -rf /var/cache/yum /var/log/anaconda /var/cache/yum /etc/mtab && \
rm /var/log/lastlog /var/log/tallylog
93 changes: 93 additions & 0 deletions s3fs-volume-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
S3Fs Volume Plugin
=======================

This is a managed Docker volume plugin to allow Docker containers to access S3Fs volumes. The S3Fs client does not need to be installed on the host and everything is managed within the plugin.

### Caveats:

- Requires Docker 18.03-1 at minimum.
- This is a managed plugin only, no legacy support.
- In order to properly support versions use `--alias` when installing the plugin.
- This only supports one S3Fs cluster per instance use `--alias` to define separate instances
- The value of `AWSACCESSKEYID/AWSSECRETACCESSKEY` is initially blank it needs `docker plugin s3fs set AWSACCESSKEYID=key;docker plugin s3fs set AWSSECRETACCESSKEY=secret` if it is set then it will be used for all buckets and low level options will not be allowed. Primarily this is to control what the deployed stacks can perform.
- **There is no robust error handling. So garbage in -> garbage out**

## Operating modes

There are three operating modes listed in order of preference. Each are mutually exclusive and will result in an error when performing a `docker volume create` if more than one operating mode is configured.

### Just the name

This is the *recommended* approach for production systems as it will prevent stacks from specifying any random server. It also prevents the stack configuration file from containing environment specific key/secrets and instead defers that knowledge to the plugin only which is set on the node level. This relies on `AWSACCESSKEYID/AWSSECRETACCESSKEY` being configured and will use the name as the volume mount set by [`docker plugin set`](https://docs.docker.com/engine/reference/commandline/plugin_set/). This can be done in an automated fashion as:

docker plugin install --alias PLUGINALIAS \
trajano/s3fs-volume-plugin \
--grant-all-permissions --disable
docker plugin set PLUGINALIAS AWSACCESSKEYID=key
docker plugin set PLUGINALIAS AWSSECRETACCESSKEY=secret
docker plugin enable PLUGINALIAS

If there is a need to have a different set of key/secrets, a separate plugin alias should be created with a different set of key/secrets.

Example in docker-compose.yml:

volumes:
sample:
driver: s3fs
name: "bucket/subdir"

The `volumes.x.name` specifies the bucket and optionally a subdirectory mount. The value of `name` will be used as the `-o bucket=` and `-o servicepath=` in s3fs fuse mount. Note that `volumes.x.name` must not start with `/`.

### Specify the s3fs driver opts

This uses the `driver_opts.s3fsopts` to define a comma separated list s3fs options. The rules for specifying the volume is the same as the previous section.

Example in docker-compose.yml assuming the alias was set as `s3fs`:

volumes:
sample:
driver: s3fs
driver_opts:
s3fsopts: nomultipart,use_path_request_style
name: "bucket/subdir"

The `volumes.x.name` specifies the bucket and optionally a subdirectory mount. The value of `name` will be used as the `-o bucket=` and `-o servicepath=`. Note that `volumes.x.name` must not start with `/`. The values above correspond to the following mounting command:

s3fs -o nomultipart,use_path_request_style,bucket=bucket,servicepath=subdir [generated_mount_point]

### Specify the options

This passes the `driver_opts.s3fsopts` to the `s3fs` command followed by the generated mount point. This is the most flexible method and gives full range to the options of the S3Fs FUSE client. Example in docker-compose.yml assuming the alias was set as `s3fs`:

volumes:
sample:
driver: s3fs
driver_opts:
s3fsopts: "nomultipart,use_path_request_style,bucket=bucket,servicepath=subdir"
name: "whatever"

The value of `name` will not be used for mounting; the value of `driver_opts.s3fsopts` is expected to have all the volume connection information.

## Testing outside the swarm

This is an example of mounting and testing a store outside the swarm. It is assuming the server is called `store1` and the volume name is `trajano`.

docker plugin install trajano/s3fs-volume-plugin --grant-all-permissions --disable
docker plugin set AWSACCESSKEYID=key
docker plugin set AWSSECRETACCESSKEY=secret
docker plugin set DEFAULT_S3FSOPTS="nomultipart,use_path_request_style"
docker plugin enable trajano/s3fs-volume-plugin
docker volume create -d trajano/s3fs-volume-plugin mybucket
docker run -it -v mybucket:/mnt alpine

## Testing with Oracle Cloud Object storage

Sample usage Oracle Object Storage in S3 compatibilty mode, replace tenant_id and region_id with a proper value:

docker plugin install trajano/s3fs-volume-plugin --grant-all-permissions --disable
docker plugin set AWSACCESSKEYID=key
docker plugin set AWSSECRETACCESSKEY=secret
docker plugin set DEFAULT_S3FSOPTS="nomultipart,use_path_request_style,url=https://[tenant_id].compat.objectstorage.[region-id].oraclecloud.com/"
docker plugin enable trajano/s3fs-volume-plugin
docker volume create -d trajano/s3fs-volume-plugin mybucket
docker run -it -v mybucket:/mnt alpine
52 changes: 52 additions & 0 deletions s3fs-volume-plugin/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"description": "S3FS plugin for Docker",
"documentation": "https://github.com/trajano/docker-volume-plugins/",
"entrypoint": [
"/tini",
"--",
"/s3fs-volume-plugin"
],
"env": [
{
"name": "AWSACCESSKEYID",
"settable": [
"value"
],
"value": ""
},
{
"name": "AWSSECRETACCESSKEY",
"settable": [
"value"
],
"value": ""
},
{
"name": "DEFAULT_S3FSOPTS",
"settable": [
"value"
],
"value": ""
}
],
"network": {
"type": "host"
},
"propagatedMount": "/var/lib/docker-volumes",
"interface": {
"types": [
"docker.volumedriver/1.0"
],
"socket": "s3fs.sock"
},
"linux": {
"capabilities": [
"CAP_SYS_ADMIN"
],
"devices": [
{
"path": "/dev/fuse"
}
]
}
}
16 changes: 16 additions & 0 deletions s3fs-volume-plugin/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: '3.7'

volumes:
# docker plugin set trajano/s3fs-volume-plugin:latest AWSACCESSKEYID=key
# docker plugin set trajano/s3fs-volume-plugin:latest AWSSECRETACCESSKEY="secret"
# docker plugin set trajano/s3fs-volume-plugin:latest DEFAULT_S3FSOPTS="nomultipart,use_path_request_style,url=https://[tenant].compat.objectstorage.[region].oraclecloud.com/"
mybucket:
driver: trajano/s3fs-volume-plugin:latest
name: "docker-shared-bucket"

services:
myapp:
image: alpine
command: sleep 1d
volumes:
- mybucket:/mnt
69 changes: 69 additions & 0 deletions s3fs-volume-plugin/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package main

import (
"log"
"os"
"strings"

"github.com/docker/go-plugins-helpers/volume"
"github.com/trajano/docker-volume-plugins/mounted-volume"
)

type s3fsDriver struct {
defaultS3fsopts string
mountedvolume.Driver
}

func (p *s3fsDriver) Validate(req *volume.CreateRequest) error {

return nil
}

func (p *s3fsDriver) MountOptions(req *volume.CreateRequest) []string {

s3fsopts, s3fsoptsInOpts := req.Options["s3fsopts"]

var s3fsoptsArray []string
if s3fsoptsInOpts {
s3fsoptsArray = append(s3fsoptsArray, strings.Split(s3fsopts, ",")...)
} else {
s3fsoptsArray = append(s3fsoptsArray, strings.Split(p.defaultS3fsopts, ",")...)
}
s3fsoptsArray = AppendBucketOptionsByVolumeName(s3fsoptsArray, req.Name)

return []string{"-o", strings.Join(s3fsoptsArray, ",")}
}

func (p *s3fsDriver) PreMount(req *volume.MountRequest) error {
return nil
}

func (p *s3fsDriver) PostMount(req *volume.MountRequest) {
}

// AppendBucketOptionsByVolumeName appends the command line arguments into the current argument list given the volume name
func AppendBucketOptionsByVolumeName(args []string, volumeName string) []string {
parts := strings.SplitN(volumeName, "/", 2)
ret := append(args, "bucket="+parts[0])
if len(parts) == 2 {
ret = append(ret, "servicepath=/"+parts[1])
}
return ret
}

func buildDriver() *s3fsDriver {
defaultsopts := os.Getenv("DEFAULT_S3FSOPTS")
d := &s3fsDriver{
Driver: *mountedvolume.NewDriver("s3fs", false, "s3fs", "local"),
defaultS3fsopts: defaultsopts,
}
d.Init(d)
return d
}

func main() {
log.SetFlags(0)
d := buildDriver()
defer d.Close()
d.ServeUnix()
}
37 changes: 37 additions & 0 deletions s3fs-volume-plugin/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

import (
"fmt"
"reflect"
"testing"
)

func TestVolumeCalculation(t *testing.T) {

var calculated = AppendBucketOptionsByVolumeName([]string{"mount"}, "mybucket")
var expected = []string{"mount", "bucket=mybucket"}
if !reflect.DeepEqual(calculated, expected) {
fmt.Errorf("%v didn't match expected", calculated)
t.Fail()
}
}

func TestVolumeCalculationOneLevel(t *testing.T) {

var calculated = AppendBucketOptionsByVolumeName([]string{"mount"}, "mybucket/levelone")
var expected = []string{"mount", "bucket=mybucket", "servicepath=/levelone"}
if !reflect.DeepEqual(calculated, expected) {
fmt.Errorf("%v didn't match expected", calculated)
t.Fail()
}
}

func TestVolumeCalculationTwoLevels(t *testing.T) {

var calculated = AppendBucketOptionsByVolumeName([]string{"mount"}, "mybucket/levelone/level2")
var expected = []string{"mount", "bucket=mybucket", "servicepath=/levelone/level2"}
if !reflect.DeepEqual(calculated, expected) {
fmt.Errorf("%v didn't match expected", calculated)
t.Fail()
}
}