diff --git a/build.sh b/build.sh index 6eafb32..326c34e 100755 --- a/build.sh +++ b/build.sh @@ -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 diff --git a/glusterfs-volume-plugin/Dockerfile b/glusterfs-volume-plugin/Dockerfile index 1e3734d..3fab445 100644 --- a/glusterfs-volume-plugin/Dockerfile +++ b/glusterfs-volume-plugin/Dockerfile @@ -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 diff --git a/glusterfs-volume-plugin/config.json b/glusterfs-volume-plugin/config.json index 4c96a21..bddb3f6 100644 --- a/glusterfs-volume-plugin/config.json +++ b/glusterfs-volume-plugin/config.json @@ -2,6 +2,8 @@ "description": "GlusterFS plugin for Docker", "documentation": "https://github.com/trajano/docker-volume-plugins/", "entrypoint": [ + "/tini", + "--", "/glusterfs-volume-plugin" ], "env": [ @@ -33,4 +35,4 @@ } ] } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 55cba09..baa1907 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/s3fs-volume-plugin/Dockerfile b/s3fs-volume-plugin/Dockerfile new file mode 100644 index 0000000..2c8aa37 --- /dev/null +++ b/s3fs-volume-plugin/Dockerfile @@ -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 diff --git a/s3fs-volume-plugin/README.md b/s3fs-volume-plugin/README.md new file mode 100644 index 0000000..2682820 --- /dev/null +++ b/s3fs-volume-plugin/README.md @@ -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 diff --git a/s3fs-volume-plugin/config.json b/s3fs-volume-plugin/config.json new file mode 100644 index 0000000..dc40a42 --- /dev/null +++ b/s3fs-volume-plugin/config.json @@ -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" + } + ] + } +} \ No newline at end of file diff --git a/s3fs-volume-plugin/docker-compose.yml b/s3fs-volume-plugin/docker-compose.yml new file mode 100644 index 0000000..a436fa8 --- /dev/null +++ b/s3fs-volume-plugin/docker-compose.yml @@ -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 diff --git a/s3fs-volume-plugin/main.go b/s3fs-volume-plugin/main.go new file mode 100644 index 0000000..daad419 --- /dev/null +++ b/s3fs-volume-plugin/main.go @@ -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() +} diff --git a/s3fs-volume-plugin/main_test.go b/s3fs-volume-plugin/main_test.go new file mode 100644 index 0000000..4e97e87 --- /dev/null +++ b/s3fs-volume-plugin/main_test.go @@ -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() + } +}