From 686e2ae621f20a255a561fd990bdd29f388af4e4 Mon Sep 17 00:00:00 2001 From: davejrt <2067825+davejrt@users.noreply.github.com> Date: Mon, 18 Feb 2019 22:42:22 +1100 Subject: [PATCH] adding in type/provider for docker stack (#433) --- README.md | 18 ++++- lib/puppet/provider/docker_stack/ruby.rb | 90 ++++++++++++++++++++++++ lib/puppet/type/docker_stack.rb | 33 +++++++++ manifests/stack.pp | 3 + spec/acceptance/stack_spec.rb | 22 +++--- spec/unit/docker_stack_spec.rb | 36 ++++++++++ 6 files changed, 188 insertions(+), 14 deletions(-) create mode 100644 lib/puppet/provider/docker_stack/ruby.rb create mode 100644 lib/puppet/type/docker_stack.rb create mode 100644 spec/unit/docker_stack_spec.rb diff --git a/README.md b/README.md index 4d0ef7e6..58649269 100755 --- a/README.md +++ b/README.md @@ -626,6 +626,8 @@ Please note you should supply your master docker-compose file as the first eleme If you are using a v3.2 compose file or above on a Docker Swarm cluster, use the `docker::stack` class. Include the file resource before you run the stack command. +NOTE: this define will be deprecated in a future release in favor of the [docker type](#types) + To deploy the stack, add the following code to the manifest file: ```puppet @@ -927,7 +929,6 @@ docker::plugin {'foo/fooplugin:latest' ensure => 'absent', force_remove => true, } -thub.com ``` ## Reference @@ -974,6 +975,7 @@ thub.com * docker_compose: A type that represents a docker compose file. * docker_network: A type that represents a docker network. * docker_volume: A type that represents a docker volume. +* docker_stack: A type that repsents a docker stack. ### Parameters @@ -1057,6 +1059,20 @@ Additional options for the volume driver. The location that the volume is mounted to. +The following parameters are available in the `docker_stack` type: + +#### 'bundle_file' + +A path to a Distributed Application Bundle file. + +#### 'compose_files' + +An array containing the docker compose file paths. + +#### `up_args` + +Arguments to be passed directly to docker stack deploy. + #### Docker class parameters #### `version` diff --git a/lib/puppet/provider/docker_stack/ruby.rb b/lib/puppet/provider/docker_stack/ruby.rb new file mode 100644 index 00000000..2e99dbd9 --- /dev/null +++ b/lib/puppet/provider/docker_stack/ruby.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'deep_merge' + +Puppet::Type.type(:docker_stack).provide(:ruby) do + desc 'Support for Puppet running Docker Stacks' + + mk_resource_methods + commands docker: 'docker' + + def exists? + Puppet.info("Checking for stack #{name}") + stack_services = {} + stack_containers = [] + resource[:compose_files].each do |file| + compose_file = YAML.safe_load(File.read(file), [], [], true) + # rubocop:disable Style/StringLiterals + containers = docker([ + 'ps', + '--format', + "{{.Label \"com.docker.swarm.service.name\"}}-{{.Image}}", + '--filter', + "label=com.docker.stack.namespace=#{name}", + ]).split("\n").each do |c| + c.slice!("#{name}_") + end + stack_containers.push(*containers) + stack_containers.uniq! + # rubocop:enable Style/StringLiterals + case compose_file['version'] + when %r{^3(\.[0-7])?$} + stack_services.merge!(compose_file['services']) + else + raise(Puppet::Error, "Unsupported docker compose file syntax version \"#{compose_file['version']}\"!") + end + end + + if stack_services.count != stack_containers.count + return false + end + counts = Hash[*stack_services.each.map { |key, array| + image = (array['image']) ? array['image'] : get_image(key, stack_services) + Puppet.info("Checking for compose service #{key} #{image}") + ["#{key}-#{image}", stack_containers.count("#{key}-#{image}")] + }.flatten] + # No containers found for the project + if counts.empty? || + # Containers described in the compose file are not running + counts.any? { |_k, v| v.zero? } + false + else + true + end + end + + def get_image(service_name, stack_services) + image = stack_services[service_name]['image'] + unless image + if stack_services[service_name]['extends'] + image = get_image(stack_services[service_name]['extends'], stack_services) + elsif stack_services[service_name]['build'] + image = "#{name}_#{service_name}" + end + end + image + end + + def create + Puppet.info("Running stack #{name}") + args = ['stack', 'deploy', compose_files, name].insert(1, bundle_file).insert(4, resource[:up_args]).compact + docker(args) + end + + def destroy + Puppet.info("Removing docker stack #{name}") + rm_args = ['stack', 'rm', name] + docker(rm_args) + end + + def bundle_file + return unless resource[:bundle_file].nil? + resource[:bundle_file].map { |x| ['-c', x] }.flatten + end + + def compose_files + resource[:compose_files].map { |x| ['-c', x] }.flatten + end + + private +end diff --git a/lib/puppet/type/docker_stack.rb b/lib/puppet/type/docker_stack.rb new file mode 100644 index 00000000..461907f1 --- /dev/null +++ b/lib/puppet/type/docker_stack.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +Puppet::Type.newtype(:docker_stack) do + @doc = 'A type representing a Docker Stack' + + ensurable + + newparam(:bundle_file) do + desc 'Path to a Distributed Application Bundle file.' + validate do |value| + raise _('bundle files should be a string') unless value.is_a? String + end + end + + newparam(:compose_files, array_matching: :all) do + desc 'An array of Docker Compose Files paths.' + validate do |value| + raise _('compose files should be an array') unless value.is_a? Array + end + end + + newparam(:up_args) do + desc 'Arguments to be passed directly to docker stack deploy.' + validate do |value| + raise _('up_args should be a String') unless value.is_a? String + end + end + + newparam(:name) do + isnamevar + desc 'The name of the stack' + end +end diff --git a/manifests/stack.pp b/manifests/stack.pp index 97388b9d..63844cf3 100644 --- a/manifests/stack.pp +++ b/manifests/stack.pp @@ -47,6 +47,9 @@ include docker::params + deprecation('docker::stack','The docker stack define type will be deprecated in a future release. Please migrate to the docker_stack type/provider.') + + $docker_command = "${docker::params::docker_command} stack" if $::osfamily == 'windows' { diff --git a/spec/acceptance/stack_spec.rb b/spec/acceptance/stack_spec.rb index 02f2b9bf..c7568bcc 100644 --- a/spec/acceptance/stack_spec.rb +++ b/spec/acceptance/stack_spec.rb @@ -5,6 +5,7 @@ tmp_path = 'C:/cygwin64/tmp' test_container = 'nanoserver-sac2016' wait_for_container_seconds = 120 + else docker_args = '' tmp_path = '/tmp' @@ -31,8 +32,7 @@ class { 'docker': #{docker_args} } context 'Creating stack' do let(:install) {" - docker::stack { 'web': - stack_name => 'web', + docker_stack { 'web': compose_files => ['#{tmp_path}/docker-stack.yml'], ensure => present, }" @@ -60,21 +60,20 @@ class { 'docker': #{docker_args} } context 'Destroying stack' do let(:install) {" - docker::stack { 'web': - stack_name => 'web', + docker_stack { 'web': compose_files => ['#{tmp_path}/docker-stack.yml'], ensure => present, }" } let(:destroy) {" - docker::stack { 'web': - stack_name => 'web', + docker_stack { 'web': compose_files => ['#{tmp_path}/docker-stack.yml'], ensure => absent, }" } it 'should run successfully' do apply_manifest(destroy, :catch_failures=>true) + sleep 10 end it 'should be idempotent' do @@ -94,8 +93,7 @@ class { 'docker': #{docker_args} } before(:all) do @install_code = <<-code - docker::stack { 'web': - stack_name => 'web', + docker_stack { 'web': compose_files => ['#{tmp_path}/docker-stack.yml', '#{tmp_path}/docker-stack-override.yml'], ensure => present, } @@ -113,8 +111,7 @@ class { 'docker': #{docker_args} } context 'Destroying project with multiple compose files' do before(:all) do @install_code = <<-code - docker::stack { 'web': - stack_name => 'web', + docker_stack { 'web': compose_files => ['#{tmp_path}/docker-stack.yml', '#{tmp_path}/docker-stack-override.yml'], ensure => present, } @@ -123,15 +120,14 @@ class { 'docker': #{docker_args} } apply_manifest(@install_code, :catch_failures=>true) @destroy_code = <<-code - docker::stack { 'web': - stack_name => 'web', + docker_stack { 'web': compose_files => ['#{tmp_path}/docker-stack.yml', '#{tmp_path}/docker-stack-override.yml'], ensure => absent, } code apply_manifest(@destroy_code, :catch_failures=>true) - sleep 10 # wait for containers to stop + sleep 10# wait for containers to stop end it 'should be idempotent' do diff --git a/spec/unit/docker_stack_spec.rb b/spec/unit/docker_stack_spec.rb new file mode 100644 index 00000000..2a5958fe --- /dev/null +++ b/spec/unit/docker_stack_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +stack = Puppet::Type.type(:docker_stack) + +describe stack do + + let :params do + [ + :name, + :provider, + :up_args, + ] + end + + let :properties do + [ + :ensure, + ] + end + + it 'should have expected properties' do + properties.each do |property| + expect(stack.properties.map(&:name)).to be_include(property) + end + end + + it 'should have expected parameters' do + params.each do |param| + expect(stack.parameters).to be_include(param) + end + end + + it 'should require up_args to be a string' do + expect(stack).to require_string_for('up_args') + end +end