diff --git a/README.md b/README.md index 3173bf35..2f1367e6 100755 --- a/README.md +++ b/README.md @@ -403,6 +403,42 @@ docker::networks::networks: A defined network can be used on a `docker::run` resource with the `net` parameter. +### Volumes + +Docker 1.9.x added support for Volumes. These are *NOT* to be confused with the legacy volumes, now known as `bind mounts`. To expose the `docker_volume` type, which is used to manage volumes, add the following code to the manifest file: + +```puppet +docker_volume { 'my-volume': + ensure => present, +} +``` + +The name value and the `ensure` parameter are required. If you do not include the `driver` value, the default `local` is used. + +Some of the key advantages for using `volumes` over `bind mounts` are: +* Easier to back up or migrate than `bind mounts` (legacy volumes). +* Managed with Docker CLI or API (Puppet type uses the CLI commands). +* Work on both Windows and Linux. +* More easily shared between containers. +* Allows for the store volumes on remote hosts or cloud providers. +* Encrypt contents of volumes. +* Add other functionality +* New volume's contents can be pre-populated by a container. + +When using the `volumes` array with `docker::run`, the command on the backend will know if it needs to use `bind mounts` or `volumes` based off the data passed to the `-v` option. + +Running `docker::run` with native volumes: + +```puppet +docker::run { 'helloworld': + image => 'ubuntu:precise', + command => '/bin/sh -c "while true; do echo hello world; sleep 1; done"', + volumes => ['my-volume:/var/log'], +} +``` + +For more information on volumes see the [Docker Volumes](https://docs.docker.com/engine/admin/volumes/volumes) documentation + ### Compose Docker Compose describes a set of containers in YAML format and runs a command to build and run those containers. Included in the docker module is the `docker_compose` type. This enables Puppet to run Compose and remediate any issues to ensure reality matches the model in your Compose file. diff --git a/lib/puppet/provider/docker_volume/ruby.rb b/lib/puppet/provider/docker_volume/ruby.rb new file mode 100644 index 00000000..2f89c8f7 --- /dev/null +++ b/lib/puppet/provider/docker_volume/ruby.rb @@ -0,0 +1,67 @@ +require 'json' + +Puppet::Type.type(:docker_volume).provide(:ruby) do + desc 'Support for Docker Volumes' + + mk_resource_methods + commands :docker => 'docker' + + def volume_conf + flags = ['volume', 'create'] + multi_flags = lambda { |values, format| + filtered = [values].flatten.compact + filtered.map { |val| sprintf(format, val) } + } + + [ + ['--driver=%s', :driver], + ['--opt=%s', :options] + ].each do |(format, key)| + values = resource[key] + new_flags = multi_flags.call(values, format) + flags.concat(new_flags) + end + flags << resource[:name] + end + + def self.instances + output = docker(['volume', 'ls']) + lines = output.split("\n") + lines.shift # remove header row + lines.collect do |line| + driver, name = line.split(' ') + inspect = docker(['volume', 'inspect', name]) + obj = JSON.parse(inspect).first + new({ + :name => name, + :mountpoint => obj['Mountpoint'], + :options => obj['Options'], + :ensure => :present, + :driver => driver + }) + end + end + + def self.prefetch(resources) + instances.each do |prov| + if resource = resources[prov.name] # rubocop:disable Lint/AssignmentInCondition + resource.provider = prov + end + end + end + + def exists? + Puppet.info("Checking if docker volume #{name} exists") + @property_hash[:ensure] == :present + end + + def create + Puppet.info("Creating docker volume #{name}") + docker(volume_conf) + end + + def destroy + Puppet.info("Removing docker volume #{name}") + docker(['volume', 'rm', name]) + end +end \ No newline at end of file diff --git a/lib/puppet/type/docker_volume.rb b/lib/puppet/type/docker_volume.rb new file mode 100644 index 00000000..613c3910 --- /dev/null +++ b/lib/puppet/type/docker_volume.rb @@ -0,0 +1,24 @@ +Puppet::Type.newtype(:docker_volume) do + @doc = 'A type representing a Docker volume' + ensurable + + newparam(:name) do + isnamevar + desc 'The name of the volume' + end + + newproperty(:driver) do + desc 'The volume driver used by the volume' + end + + newproperty(:options) do + desc 'Additional options for the volume driver' + end + + newproperty(:mountpoint) do + desc 'The location that the volume is mounted to' + validate do |value| + fail "#{self.name.to_s} is read-only and is only available via puppet resource." + end + end +end \ No newline at end of file diff --git a/manifests/volumes.pp b/manifests/volumes.pp new file mode 100644 index 00000000..3426d1d7 --- /dev/null +++ b/manifests/volumes.pp @@ -0,0 +1,4 @@ +# docker::volumes +class docker::volumes($volumes) { + create_resources(docker_volumes, $volumes) +} diff --git a/spec/acceptance/volume_spec.rb b/spec/acceptance/volume_spec.rb new file mode 100644 index 00000000..88a7033c --- /dev/null +++ b/spec/acceptance/volume_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper_acceptance' + +describe 'docker network' do + command = 'docker' + + before(:all) do + install_code = "class { 'docker': }" + apply_manifest(install_code, :catch_failures => true) + end + + describe command("#{command} volume --help") do + its(:exit_status) { should eq 0 } + end + + context 'with a local volume described in Puppet' do + before(:all) do + @name = 'test-volume' + @pp = <<-code + docker_volume { '#{@name}': + ensure => present, + } + code + apply_manifest(@pp, :catch_failures => true) + end + + it 'should be idempotent' do + apply_manifest(@pp, :catch_changes => true) + end + + it 'should have created a volume' do + shell("#{command} volume inspect #{@name}", :acceptable_exit_codes => [0]) + end + + after(:all) do + shell("#{command} volume rm #{@name}") + end + end +end \ No newline at end of file diff --git a/spec/unit/docker_volume_spec.rb b/spec/unit/docker_volume_spec.rb new file mode 100644 index 00000000..8df44af0 --- /dev/null +++ b/spec/unit/docker_volume_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +volume = Puppet::Type.type(:docker_volume) + +describe volume do + let :params do + [ + :name, + :provider, + ] + end + + let :properties do + [ + :driver, + :options, + :mountpoint, + ] + end + + it 'should have expected properties' do + properties.each do |property| + expect(volume.properties.map(&:name)).to be_include(property) + end + end + + it 'should have expected parameters' do + params.each do |param| + expect(volume.parameters).to be_include(param) + end + end +end \ No newline at end of file