diff --git a/lib/puppet/provider/docker_stack/ruby.rb b/lib/puppet/provider/docker_stack/ruby.rb index 2ea08ef..e45297a 100644 --- a/lib/puppet/provider/docker_stack/ruby.rb +++ b/lib/puppet/provider/docker_stack/ruby.rb @@ -1,89 +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) + image = "#{image}:latest" unless image.include?(':') 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 resource[:bundle_file].map { |x| ['-c', x] }.flatten unless resource[:bundle_file].nil? end def compose_files resource[:compose_files].map { |x| ['-c', x] }.flatten end private end diff --git a/spec/acceptance/docker_custom_source_spec.rb b/spec/acceptance/docker_custom_source_spec.rb index 7e655ef..6229ae8 100644 --- a/spec/acceptance/docker_custom_source_spec.rb +++ b/spec/acceptance/docker_custom_source_spec.rb @@ -1,106 +1,106 @@ require 'spec_helper_acceptance' skip = false if fact('osfamily') == 'windows' docker_args = 'docker_ee => true, docker_ee_source_location => "https://download.docker.com/components/engine/windows-server/17.06/docker-17.06.2-ee-14.zip"' - default_image = 'microsoft/nanoserver' - default_image_tag = '10.0.14393.2189' + default_image = 'winamd64/hello-world' + default_image_tag = 'nanoserver-sac2016' #The default args are set because: #restart => 'always' - there is no service created to manage containers #net => 'nat' - docker uses bridged by default when running a container. When installing docker on windows the default network is NAT. default_docker_run_arg = "restart => 'always', net => 'nat'," default_run_command = "ping 127.0.0.1 -t" docker_command = "\"/cygdrive/c/Program Files/Docker/docker\"" skip = false elsif fact('os.name') == 'Ubuntu' && fact('os.release.full') == '14.04' docker_args = "version => '18.06.1~ce~3-0~ubuntu'" skip = true else docker_args = '' skip = true end describe 'the Puppet Docker module' do context 'with download location', :skip => skip do let(:pp) {" class { 'docker': #{docker_args} } "} it 'should run successfully' do apply_manifest(pp, :catch_failures => true) end it 'should run idempotently' do apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' end it 'should be start a docker process' do if fact('osfamily') == 'windows' shell('powershell Get-Process -Name dockerd') do |r| expect(r.stdout).to match(/ProcessName/) end else shell('ps aux | grep docker') do |r| expect(r.stdout).to match(/dockerd -H unix:\/\/\/var\/run\/docker.sock/) end end end it 'should install a working docker client' do shell("#{docker_command} ps", :acceptable_exit_codes => [0] ) end it 'should stop a running container and remove container' do pp=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': require => Class['docker'], } docker::run { 'container_3_6': image => '#{default_image}', command => '#{default_run_command}', require => Docker::Image['#{default_image}'], #{default_docker_run_arg} } EOS pp2=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': require => Class['docker'], } docker::run { 'container_3_6': ensure => 'absent', image => '#{default_image}', require => Docker::Image['#{default_image}'], } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 15 shell("#{docker_command} ps", :acceptable_exit_codes => [0]) apply_manifest(pp2, :catch_failures => true) apply_manifest(pp2, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 15 shell("#{docker_command} inspect container-3-6", :acceptable_exit_codes => [1]) if fact('osfamily') == 'windows' shell('test -f /cygdrive/c/Users/Administrator/AppData/Local/Temp/container-3-6.service', :acceptable_exit_codes => [1]) else shell('test -f /etc/systemd/system/container-3-6.service', :acceptable_exit_codes => [1]) end end end end \ No newline at end of file diff --git a/spec/acceptance/docker_full_spec.rb b/spec/acceptance/docker_full_spec.rb index 8638a9e..79f2e7b 100644 --- a/spec/acceptance/docker_full_spec.rb +++ b/spec/acceptance/docker_full_spec.rb @@ -1,966 +1,966 @@ require 'spec_helper_acceptance' if fact('kernel') == 'windows' docker_args = 'docker_ee => true' - default_image = 'microsoft/nanoserver' - default_image_tag = '10.0.14393.2189' - second_image = 'hello-world' - default_digest = 'sha256:204c41542c0927ac0296802e44c56b886b47e99cf8220fb49d46951bd5fc1742' + default_image = 'winamd64/hello-world' + default_image_tag = 'nanoserver-sac2016' + default_digest = 'sha256:88f6207b9281e60dc4a91008e68b542cc7350d06985c7fc3c5132e6f6b9b3ebe' + second_image = 'winamd64/hola-mundo' default_dockerfile = 'C:/Users/Administrator/AppData/Local/Temp/Dockerfile' dockerfile_test = 'C:/Windows/Dockerfile_test.txt' #The default args are set because: #restart => 'always' - there is no service created to manage containers #net => 'nat' - docker uses bridged by default when running a container. When installing docker on windows the default network is NAT. default_docker_run_arg = "restart => 'always', net => 'nat'," default_run_command = "ping 127.0.0.1 -t" docker_command = "\"/cygdrive/c/Program Files/Docker/docker\"" default_docker_exec_lr_command = 'cmd /c "ping 127.0.0.1 -t > c:\windows\temp\test_file.txt"' default_docker_exec_command = 'cmd /c "echo test > c:\windows\temp\test_file.txt"' docker_mount_path = 'C:/Users/Administrator/AppData/Local/Temp' storage_driver = "windowsfilter" else if fact('os.family') == 'RedHat' docker_args = "repo_opt => '--enablerepo=localmirror-extras'" elsif fact('os.name') == 'Ubuntu' && fact('os.release.full') == '14.04' docker_args = "version => '18.06.1~ce~3-0~ubuntu'" else docker_args = '' end default_image = 'alpine' second_image = 'busybox' default_image_tag = '3.7' default_digest = 'sha256:3dcdb92d7432d56604d4545cbd324b14e647b313626d99b889d0626de158f73a' default_dockerfile = '/root/Dockerfile' dockerfile_test = "#{default_dockerfile}_test.txt" docker_command = "docker" default_docker_run_arg = '' default_run_command = "init" default_docker_exec_lr_command = '/bin/sh -c "touch /root/test_file.txt; while true; do echo hello world; sleep 1; done"' default_docker_exec_command = 'touch /root/test_file.txt' docker_mount_path = "/root" storage_driver = "devicemapper" if fact('os.family') == 'Debian' && fact('os.release.major') =~ (/14.04|^8$/) storage_driver = "aufs" elsif fact('os.family') == 'RedHat' storage_driver = "devicemapper" else storage_driver = "overlay2" end end describe 'the Puppet Docker module' do context 'clean up before each test' do before(:each) do retry_on_error_matching(60, 5, /connection failure running/) do # Stop all container using systemd shell('ls -D -1 /etc/systemd/system/docker-container* | sed \'s/\/etc\/systemd\/system\///g\' | sed \'s/\.service//g\' | while read container; do service $container stop; done') # Delete all running containers shell("#{docker_command} rm -f $(#{docker_command} ps -a -q) || true") # Delete all existing images shell("#{docker_command} rmi -f $(#{docker_command} images -q) || true") # Check to make sure no images are present shell("#{docker_command} images | wc -l") do |r| expect(r.stdout).to match(/^0|1$/) end # Check to make sure no running containers are present shell("#{docker_command} ps | wc -l") do |r| expect(r.stdout).to match(/^0|1$/) end end end describe 'docker class' do context 'without any parameters' do let(:pp) {" class { 'docker': #{docker_args} } "} it 'should run successfully' do apply_manifest(pp, :catch_failures => true) end it 'should run idempotently' do apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' end it 'should be start a docker process' do if fact('osfamily') == 'windows' shell('powershell Get-Process -Name dockerd') do |r| expect(r.stdout).to match(/ProcessName/) end else shell('ps aux | grep docker') do |r| expect(r.stdout).to match(/dockerd -H unix:\/\/\/var\/run\/docker.sock/) end end end it 'should install a working docker client' do shell("#{docker_command} ps", :acceptable_exit_codes => [0] ) end it 'should stop a running container and remove container' do pp=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': require => Class['docker'], } docker::run { 'container_3_6': image => '#{default_image}', command => '#{default_run_command}', require => Docker::Image['#{default_image}'], #{default_docker_run_arg} } EOS pp2=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': require => Class['docker'], } docker::run { 'container_3_6': ensure => 'absent', image => '#{default_image}', require => Docker::Image['#{default_image}'], } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 15 shell("#{docker_command} ps", :acceptable_exit_codes => [0]) apply_manifest(pp2, :catch_failures => true) apply_manifest(pp2, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 15 shell("#{docker_command} inspect container-3-6", :acceptable_exit_codes => [1]) if fact('osfamily') == 'windows' shell('test -f /cygdrive/c/Users/Administrator/AppData/Local/Temp/container-3-6.service', :acceptable_exit_codes => [1]) else shell('test -f /etc/systemd/system/container-3-6.service', :acceptable_exit_codes => [1]) end end end context 'passing a storage driver' do before(:all) do @pp=<<-EOS class {'docker': #{docker_args}, storage_driver => "#{storage_driver}", } EOS apply_manifest(@pp, :catch_failures => true) sleep 15 end it 'should result in the docker daemon being configured with the specified storage driver' do shell("#{docker_command} info -f \"{{ .Driver}}\"") do |r| expect(r.stdout).to match (/#{storage_driver}/) end end end context 'passing a TCP address to bind to' do before(:all) do @pp =<<-EOS class { 'docker': tcp_bind => 'tcp://127.0.0.1:4444', #{docker_args} } EOS apply_manifest(@pp, :catch_failures => true) # A sleep to give docker time to execute properly sleep 4 end it 'should run idempotently' do apply_manifest(@pp, :catch_changes => true) unless fact('selinux') == 'true' end it 'should result in docker listening on the specified address' do if fact('osfamily') == 'windows' shell('netstat -a -b') do |r| expect(r.stdout).to match(/127.0.0.1:4444/) end else shell('netstat -tulpn | grep docker') do |r| expect(r.stdout).to match(/tcp\s+0\s+0\s+127.0.0.1:4444\s+0.0.0.0\:\*\s+LISTEN\s+\d+\/docker/) end end end end context 'bound to a particular unix socket' do before(:each) do @pp =<<-EOS class { 'docker': socket_bind => 'unix:///var/run/docker.sock', #{docker_args} } EOS apply_manifest(@pp, :catch_failures => true) # A sleep to give docker time to execute properly sleep 4 end it 'should run idempotently' do apply_manifest(@pp, :catch_changes => true) unless fact('selinux') == 'true' end it 'should show docker listening on the specified unix socket' do if fact('osfamily') != 'windows' shell('ps aux | grep docker') do |r| expect(r.stdout).to match(/unix:\/\/\/var\/run\/docker.sock/) end end end end context 'uninstall docker' do after(:all) do @pp =<<-EOS class {'docker': #{docker_args}, ensure => 'present' } EOS apply_manifest(@pp, :catch_failures => true) # Wait for reboot if windows sleep 300 if fact('osfamily') == 'windows' end it 'should uninstall successfully' do @pp =<<-EOS class {'docker': #{docker_args}, ensure => 'absent' } EOS apply_manifest(@pp, :catch_failures => true) sleep 4 shell('docker ps', :acceptable_exit_codes => [1, 127]) end end end describe 'docker::image' do it 'should successfully download an image from the Docker Hub' do pp=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': ensure => present, require => Class['docker'], } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 shell("#{docker_command} images") do |r| expect(r.stdout).to match(/#{default_image}/) end end it 'should successfully download an image based on a tag from the Docker Hub' do pp=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': ensure => present, image_tag => '#{default_image_tag}', require => Class['docker'], } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 shell("#{docker_command} images") do |r| expect(r.stdout).to match(/#{default_image}\s+#{default_image_tag}/) end end it 'should successfully download an image based on a digest from the Docker Hub' do pp=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': ensure => present, image_digest => '#{default_digest}', require => Class['docker'], } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 shell("#{docker_command} images --digests") do |r| expect(r.stdout).to match(/#{default_image}.*#{default_digest}/) end end it 'should create a new image based on a Dockerfile' do if fact('osfamily') == 'windows' run_cmd = 'RUN echo test > C:\\Windows\\Temp\\Dockerfile_test.txt' else run_cmd = "RUN echo test > #{dockerfile_test}" end pp=<<-EOS class { 'docker': #{docker_args} } docker::image { 'alpine_with_file': docker_file => "#{default_dockerfile}", require => Class['docker'], } file { '#{default_dockerfile}': ensure => present, content => "FROM #{default_image}\n#{run_cmd}", before => Docker::Image['alpine_with_file'], } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 if fact('osfamily') == 'windows' shell("#{docker_command} run alpine_with_file cmd /c dir Windows\\\\Temp") do |r| expect(r.stdout).to match(/_test.txt/) end else shell("#{docker_command} run alpine_with_file ls #{dockerfile_test}") do |r| expect(r.stdout).to match(/#{dockerfile_test}/) end end end it 'should create a new image based on a tar', :win_broken => true do pp=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': require => Class['docker'], ensure => present, } docker::run { 'container_2_4': image => '#{default_image}', command => '/bin/sh -c "touch /root/test_file_for_tar_test.txt; while true; do echo hello world; sleep 1; done"', require => Docker::Image['alpine'], } EOS pp2=<<-EOS class { 'docker': #{docker_args} } docker::image { 'alpine_from_commit': docker_tar => "/root/rootfs.tar" } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 # Commit currently running container as an image container_id = shell("#{docker_command} ps | awk 'FNR == 2 {print $1}'") shell("#{docker_command} commit #{container_id.stdout.strip} alpine_from_commit") # Stop all container using systemd shell('ls -D -1 /etc/systemd/system/docker-container* | sed \'s/\/etc\/systemd\/system\///g\' | sed \'s/\.service//g\' | while read container; do service $container stop; done') # Stop all running containers shell("#{docker_command} rm -f $(docker ps -a -q) || true") # Make sure no other containers are running shell("#{docker_command} ps | wc -l") do |r| expect(r.stdout).to match(/^1$/) end # Export new to a tar file shell("#{docker_command} save alpine_from_commit > /root/rootfs.tar") # Remove all images shell("#{docker_command} rmi $(docker images -q) || true") # Make sure no other images are present shell("#{docker_command} images | wc -l") do |r| expect(r.stdout).to match(/^1$/) end apply_manifest(pp2, :catch_failures => true) apply_manifest(pp2, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 shell("#{docker_command} run alpine_from_commit ls /root") do |r| expect(r.stdout).to match(/test_file_for_tar_test.txt/) end end it 'should successfully delete the image' do pp1=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': ensure => present, require => Class['docker'], } EOS apply_manifest(pp1, :catch_failures => true) pp2=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': ensure => absent, } EOS apply_manifest(pp2, :catch_failures => true) apply_manifest(pp2, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 shell("#{docker_command} images") do |r| expect(r.stdout).to_not match(/#{default_image}/) end end end describe "docker::run" do it 'should start a container with a configurable command' do pp=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': require => Class['docker'], } docker::run { 'container_3_1': image => '#{default_image}', command => '#{default_docker_exec_lr_command}', require => Docker::Image['#{default_image}'], #{default_docker_run_arg} } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 container_id = shell("#{docker_command} ps | awk 'FNR == 2 {print $1}'") if fact('osfamily') == 'windows' shell("#{docker_command} exec #{container_id.stdout.strip} cmd /c dir Windows\\\\Temp") do |r| expect(r.stdout).to match(/test_file.txt/) end else shell("#{docker_command} exec #{container_id.stdout.strip} ls /root") do |r| expect(r.stdout).to match(/test_file.txt/) end end container_name = shell("#{docker_command} ps | awk 'FNR == 2 {print $NF}'") expect("#{container_name.stdout.strip}").to match(/(container-3-1|container_3_1)/) end it 'should start a container with port configuration' do pp=<<-EOS class { 'docker': #{docker_args}} docker::image { '#{default_image}': require => Class['docker'], } docker::run { 'container_3_2': image => '#{default_image}', command => '#{default_run_command}', ports => ['4444'], expose => ['5555'], require => Docker::Image['#{default_image}'], #{default_docker_run_arg} } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 shell("#{docker_command} ps") do |r| expect(r.stdout).to match(/"#{default_run_command}".+5555\/tcp\, 0\.0\.0.0\:\d+\-\>4444\/tcp/) end end it 'should start a container with the hostname set' do pp=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': require => Class['docker'], } docker::run { 'container_3_3': image => '#{default_image}', command => '#{default_run_command}', hostname => 'testdomain.com', require => Docker::Image['#{default_image}'], #{default_docker_run_arg} } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 container_id = shell("#{docker_command} ps | awk 'FNR == 2 {print $1}'") shell("#{docker_command} exec #{container_id.stdout.strip} hostname") do |r| expect(r.stdout).to match(/testdomain.com/) end end it 'should start a container while mounting local volumes' do pp=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': require => Class['docker'], } docker::run { 'container_3_4': image => '#{default_image}', command => '#{default_run_command}', volumes => ["#{docker_mount_path}:#{docker_mount_path}/mnt:rw"], require => Docker::Image['#{default_image}'], #{default_docker_run_arg} } file { '#{docker_mount_path}/test_mount.txt': ensure => present, before => Docker::Run['container_3_4'], } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 container_id = shell("#{docker_command} ps | awk 'FNR == 2 {print $1}'") if fact('osfamily') == 'windows' shell("#{docker_command} exec #{container_id.stdout.strip} cmd /c dir Users\\\\Administrator\\\\AppData\\\\Local\\\\Temp\\\\mnt") do |r| expect(r.stdout).to match(/test_mount.txt/) end else shell("#{docker_command} exec #{container_id.stdout.strip} ls /root/mnt") do |r| expect(r.stdout).to match(/test_mount.txt/) end end end #cpuset is not supported on Docker Windows #STDERR: C:/Program Files/Docker/docker.exe: Error response from daemon: invalid option: Windows does not support CpusetCpus. it 'should start a container with cpuset paramater set', :win_broken => true do pp=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': require => Class['docker'], } docker::run { 'container_3_5_5': image => '#{default_image}', command => '#{default_run_command}', cpuset => ['0'], require => Docker::Image['#{default_image}'], #{default_docker_run_arg} } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 shell('#{docker_command} inspect container_3_5_5') do |r| expect(r.stdout).to match(/"CpusetCpus"\: "0"/) end end #leagacy container linking was not implemented on Windows. --link is a legacy Docker feature: https://docs.docker.com/network/links/ it 'should start multiple linked containers', :win_broken => true do pp=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': require => Class['docker'], } docker::run { 'container_3_5_1': image => '#{default_image}', command => '#{default_run_command}', require => Docker::Image['#{default_image}'], #{default_docker_run_arg} } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 container_1 = shell("#{docker_command} ps | awk 'FNR == 2 {print $NF}'") pp2=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': require => Class['docker'], } docker::run { 'container_3_5_2': image => '#{default_image}', command => '#{default_run_command}', depends => ['#{container_1.stdout.strip}'], links => "#{container_1.stdout.strip}:the_link", require => Docker::Image['#{default_image}'], #{default_docker_run_arg} } EOS apply_manifest(pp2, :catch_failures => true) apply_manifest(pp2, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 container_2 = shell("#{docker_command} ps | awk 'FNR == 2 {print $NF}'") container_id = shell("#{docker_command} ps | awk 'FNR == 2 {print $1}'") shell("#{docker_command} inspect -f \"{{ .HostConfig.Links }}\" #{container_id.stdout.strip}") do |r| expect(r.stdout).to match("/#{container_1.stdout.strip}:/#{container_2.stdout.strip}/the_link") end end it 'should stop a running container' do pp=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': require => Class['docker'], } docker::run { 'container_3_6': image => '#{default_image}', command => '#{default_run_command}', require => Docker::Image['#{default_image}'], #{default_docker_run_arg} } EOS pp2=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': require => Class['docker'], } docker::run { 'container_3_6': image => '#{default_image}', running => false, require => Docker::Image['#{default_image}'], #{default_docker_run_arg} } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 shell("#{docker_command} ps | wc -l") do |r| expect(r.stdout).to match(/^2$/) end apply_manifest(pp2, :catch_failures => true) apply_manifest(pp2, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 shell("#{docker_command} ps | wc -l") do |r| expect(r.stdout).to match(/^1$/) end end it 'should stop a running container and remove container' do pp=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': require => Class['docker'], } docker::run { 'container_3_6_1': image => '#{default_image}', command => '#{default_run_command}', require => Docker::Image['#{default_image}'], #{default_docker_run_arg} } EOS pp2=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': require => Class['docker'], } docker::run { 'container_3_6_1': ensure => 'absent', image => '#{default_image}', require => Docker::Image['#{default_image}'], } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 15 shell("#{docker_command} inspect container_3_6_1", :acceptable_exit_codes => [0]) apply_manifest(pp2, :catch_failures => true) apply_manifest(pp2, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 15 shell("#{docker_command} inspect container_3_6_1", :acceptable_exit_codes => [1]) end it 'should allow dependency for ordering of independent run and image' do pp=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': } docker::run { 'container_3_7_1': image => '#{default_image}', command => '#{default_run_command}', #{default_docker_run_arg} } docker::image { '#{second_image}': require => Docker::Run['container_3_7_1'], } docker::run { 'container_3_7_2': image => '#{second_image}', command => '#{default_run_command}', #{default_docker_run_arg} } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' end it 'should restart a unhealthy container' do pp5=<<-EOS class { 'docker': #{docker_args} } docker::run { 'container_3_7_3': image => '#{default_image}', command => '#{default_run_command}', health_check_cmd => 'echo', restart_on_unhealthy => true, #{default_docker_run_arg} } EOS pp_delete=<<-EOS class { 'docker': #{docker_args} } docker::run { 'container_3_7_3': image => '#{default_image}', ensure => absent, } EOS if fact('osfamily') == 'windows' apply_manifest(pp5, :catch_failures => true) elsif fact('os.release.major') =~ (/14.04|8/) apply_manifest(pp5, :catch_failures => true) do |r| expect(r.stdout).to match(/container_3_7_3/) end else apply_manifest(pp5, :catch_failures => true) do |r| expect(r.stdout).to match(/docker-container_3_7_3-systemd-reload/) end end apply_manifest(pp_delete, :catch_failures => true) end end end describe "docker::exec" do it 'should run a command inside an already running container' do pp=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': require => Class['docker'], } docker::run { 'container_4_1': image => '#{default_image}', command => '#{default_run_command}', require => Docker::Image['#{default_image}'], #{default_docker_run_arg} } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 15 container_1 = shell("#{docker_command} ps | awk 'FNR == 2 {print $NF}'") pp2=<<-EOS class { 'docker': #{docker_args} } docker::exec { 'test_command': container => '#{container_1.stdout.strip}', command => '#{default_docker_exec_command}', tty => true, } EOS pp_delete=<<-EOS docker::run { 'container_4_1': image => '#{default_image}', ensure => absent, } EOS apply_manifest(pp2, :catch_failures => true) # A sleep to give docker time to execute properly sleep 4 container_id = shell("#{docker_command} ps | awk 'FNR == 2 {print $1}'") if fact('osfamily') == 'windows' shell("#{docker_command} exec #{container_id.stdout.strip} cmd /c dir Windows\\\\Temp") do |r| expect(r.stdout).to match(/test_file.txt/) end else shell("#{docker_command} exec #{container_id.stdout.strip} ls /root") do |r| expect(r.stdout).to match(/test_file.txt/) end end apply_manifest(pp_delete, :catch_failures => true) end it 'should only run if notified when refreshonly is true' do container_name = 'container_4_2' pp=<<-EOS class { 'docker': #{docker_args} } docker::image { '#{default_image}': } docker::run { '#{container_name}': image => '#{default_image}', command => '#{default_run_command}', #{default_docker_run_arg} } docker::exec { 'test_command': container => '#{container_name}', command => '#{default_docker_exec_command}', refreshonly => true, } EOS apply_manifest(pp, :catch_failures => true) apply_manifest(pp, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 if fact('osfamily') == 'windows' shell("#{docker_command} exec #{container_name} cmd /c dir Windows\\\\Temp") do |r| expect(r.stdout).to_not match(/test_file.txt/) end else shell("#{docker_command} exec #{container_name} ls /root") do |r| expect(r.stdout).to_not match(/test_file.txt/) end end pp_extra=<<-EOS file { '#{default_dockerfile}_dummy_file': ensure => 'present', notify => Docker::Exec['test_command'], } EOS pp_delete=<<-EOS docker::run { '#{container_name}': image => '#{default_image}', ensure => absent, } EOS pp2 = pp + pp_extra apply_manifest(pp2, :catch_failures => true) apply_manifest(pp2, :catch_changes => true) unless fact('selinux') == 'true' # A sleep to give docker time to execute properly sleep 4 if fact('osfamily') == 'windows' shell("#{docker_command} exec #{container_name} cmd /c dir Windows\\\\Temp") do |r| expect(r.stdout).to match(/test_file.txt/) end else shell("#{docker_command} exec #{container_name} ls /root") do |r| expect(r.stdout).to match(/test_file.txt/) end end apply_manifest(pp_delete, :catch_failures => true) end end end diff --git a/spec/acceptance/stack_spec.rb b/spec/acceptance/stack_spec.rb index e968c48..10ccda1 100644 --- a/spec/acceptance/stack_spec.rb +++ b/spec/acceptance/stack_spec.rb @@ -1,155 +1,158 @@ require 'spec_helper_acceptance' if fact('osfamily') == 'windows' docker_args = 'docker_ee => true' tmp_path = 'C:/cygwin64/tmp' test_container = 'nanoserver-sac2016' wait_for_container_seconds = 120 else if fact('os.name') == 'Ubuntu' && fact('os.release.full') == '14.04' docker_args = "version => '18.06.1~ce~3-0~ubuntu'" else docker_args = '' end tmp_path = '/tmp' test_container = 'alpine' wait_for_container_seconds = 10 end describe 'docker stack' do before(:all) do retry_on_error_matching(60, 5, /connection failure running/) do @install_code = <<-code class { 'docker': #{docker_args} } docker::swarm {'cluster_manager': init => true, ensure => 'present', advertise_addr => $facts['networking']['ip'], listen_addr => $facts['networking']['ip'], require => Class['docker'], } code apply_manifest(@install_code, :catch_failures=>true) end end context 'Creating stack' do let(:install) {" docker_stack { 'web': compose_files => ['#{tmp_path}/docker-stack.yml'], ensure => present, }" } it 'should deploy stack' do apply_manifest(install, :catch_failures=>true) sleep wait_for_container_seconds end it 'should be idempotent' do apply_manifest(install, :catch_changes=>true) end it 'should find a stack' do shell('docker stack ls') do |r| expect(r.stdout).to match(/web/) end end - it 'should find a docker container' do - shell("docker ps | grep web_compose_test", :acceptable_exit_codes => [0]) - end + it 'should not find a docker container' do + shell("docker ps -a -q -f \"name=web_compose_test\"", :acceptable_exit_codes => [0]) + end end context 'Destroying stack' do let(:install) {" docker_stack { 'web': compose_files => ['#{tmp_path}/docker-stack.yml'], ensure => present, }" } let(:destroy) {" 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 retry_on_error_matching(10, 3, /Removing network web_default/) do apply_manifest(destroy, :catch_changes=>true) end end it 'should not find a docker stack' do - sleep 5 - shell('docker stack ls') do |r| - expect(r.stdout).to_not match(/web/) - end + shell('docker stack ls') do |r| + expect(r.stdout).to_not match(/web/) + end end end context 'creating stack with multi compose files' do before(:all) do @install_code = <<-code docker_stack { 'web': compose_files => ['#{tmp_path}/docker-stack.yml', '#{tmp_path}/docker-stack-override.yml'], ensure => present, } code apply_manifest(@install_code, :catch_failures=>true) end it "should find container with web_compose_test tag" do sleep wait_for_container_seconds shell("docker ps | grep web_compose_test", :acceptable_exit_codes => [0]) end end context 'Destroying project with multiple compose files' do before(:all) do @install_code = <<-code docker_stack { 'web': compose_files => ['#{tmp_path}/docker-stack.yml', '#{tmp_path}/docker-stack-override.yml'], ensure => present, } code apply_manifest(@install_code, :catch_failures=>true) @destroy_code = <<-code 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 + retry_on_error_matching(10, 3, /Removing network web_default/) do + apply_manifest(@destroy_code, :catch_failures=>true) + end + sleep 15 # Wait for containers to stop and be destroyed end it 'should be idempotent' do retry_on_error_matching(10, 3, /Removing network web_default/) do apply_manifest(@destroy_code, :catch_changes=>true) end end it 'should not find a docker stack' do shell('docker stack ls') do |r| expect(r.stdout).to_not match(/web/) end end it 'should not find a docker container' do - shell("docker ps | grep web_compose_test", :acceptable_exit_codes => [1]) + shell("docker ps", :acceptable_exit_codes => [0]) do |r| + expect(r.stdout).not_to match(/web_compose_test/) + end end end end diff --git a/spec/spec_helper_acceptance.rb b/spec/spec_helper_acceptance.rb index 075dc68..b98127c 100644 --- a/spec/spec_helper_acceptance.rb +++ b/spec/spec_helper_acceptance.rb @@ -1,171 +1,171 @@ # frozen_string_literal: true require 'beaker-rspec/spec_helper' require 'beaker-rspec/helpers/serverspec' require 'beaker/puppet_install_helper' require 'rspec/retry' begin require 'pry' rescue LoadError # rubocop:disable Lint/HandleExceptions for optional loading end # This method allows a block to be passed in and if an exception is raised # that matches the 'error_matcher' matcher, the block will wait a set number # of seconds before retrying. # Params: # - max_retry_count - Max number of retries # - retry_wait_interval_secs - Number of seconds to wait before retry # - error_matcher - Matcher which the exception raised must match to allow retry # Example Usage: # retry_on_error_matching(3, 5, /OpenGPG Error/) do # apply_manifest(pp, :catch_failures => true) # end def retry_on_error_matching(max_retry_count = 3, retry_wait_interval_secs = 5, error_matcher = nil) try = 0 begin try += 1 yield rescue StandardError => e raise unless try < max_retry_count && (error_matcher.nil? || e.message =~ error_matcher) sleep retry_wait_interval_secs retry end end run_puppet_install_helper unless ENV['BEAKER_provision'] == 'no' RSpec.configure do |c| # Add exclusive filter for Windows untill all the windows functionality is implemented c.filter_run_excluding win_broken: true # Project root proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..')) # Readable test descriptions c.formatter = :documentation # show retry status in spec process c.verbose_retry = true # show exception that triggers a retry if verbose_retry is set to true c.display_try_failure_messages = true # Configure all nodes in nodeset c.before :suite do # Install module and dependencies hosts.each do |host| next unless not_controller(host) copy_module_to(host, source: proj_root, module_name: 'docker') # Due to RE-6764, running yum update renders the machine unable to install # other software. Thus this workaround. if fact_on(host, 'operatingsystem') == 'RedHat' on(host, 'mv /etc/yum.repos.d/redhat.repo /etc/yum.repos.d/internal-mirror.repo') on(host, 'rpm -ivh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm') end on(host, 'yum update -y -q') if fact_on(host, 'osfamily') == 'RedHat' on host, puppet('module', 'install', 'puppetlabs-stdlib', '--version', '4.24.0'), acceptable_exit_codes: [0, 1] on host, puppet('module', 'install', 'puppetlabs-apt', '--version', '4.4.1'), acceptable_exit_codes: [0, 1] on host, puppet('module', 'install', 'puppetlabs-translate', '--version', '1.0.0'), acceptable_exit_codes: [0, 1] on host, puppet('module', 'install', 'puppetlabs-powershell', '--version', '2.1.5'), acceptable_exit_codes: [0, 1] on host, puppet('module', 'install', 'puppetlabs-reboot', '--version', '2.0.0'), acceptable_exit_codes: [0, 1] # net-tools required for netstat utility being used by some tests if fact_on(host, 'osfamily') == 'RedHat' && fact_on(host, 'operatingsystemmajrelease') == '7' on(host, 'yum install -y net-tools device-mapper') end if fact_on(host, 'osfamily') == 'Debian' on(host, 'apt-get install net-tools') end docker_compose_content_v3 = <<-EOS version: "3.4" x-images: &default-image alpine:3.8 services: compose_test: image: *default-image command: /bin/sh -c "while true; do echo hello world; sleep 1; done" EOS docker_compose_override_v3 = <<-EOS version: "3.4" x-images: &default-image debian:stable-slim services: compose_test: image: *default-image command: /bin/sh -c "while true; do echo hello world; sleep 1; done" EOS docker_stack_override_v3 = <<-EOS version: "3.4" x-images: &default-image debian:stable-slim services: compose_test: image: *default-image command: /bin/sh -c "while true; do echo hello world; sleep 1; done" EOS docker_compose_content_v3_windows = <<-EOS version: "3" services: compose_test: - image: hello-world:nanoserver + image: winamd64/hello-world command: cmd.exe /C "ping 8.8.8.8 -t" networks: default: external: name: nat EOS docker_compose_override_v3_windows = <<-EOS version: "3" services: compose_test: - image: hello-world:nanoserver-sac2016 + image: winamd64/hello-world:nanoserver-sac2016 command: cmd.exe /C "ping 8.8.8.8 -t" networks: default: external: name: nat EOS docker_stack_content_windows = <<-EOS version: "3" services: compose_test: - image: hello-world:nanoserver + image: winamd64/hello-world command: cmd.exe /C "ping 8.8.8.8 -t" EOS docker_stack_override_windows = <<-EOS version: "3" services: compose_test: - image: hello-world:nanoserver-sac2016 + image: winamd64/hello-world:nanoserver-sac2016 EOS if fact_on(host, 'osfamily') == 'windows' create_remote_file(host, '/tmp/docker-compose-v3.yml', docker_compose_content_v3_windows) create_remote_file(host, '/tmp/docker-stack.yml', docker_stack_content_windows) create_remote_file(host, '/tmp/docker-compose-override-v3.yml', docker_compose_override_v3_windows) create_remote_file(host, '/tmp/docker-stack-override.yml', docker_stack_override_windows) else create_remote_file(host, '/tmp/docker-compose-v3.yml', docker_compose_content_v3) create_remote_file(host, '/tmp/docker-stack.yml', docker_compose_content_v3) create_remote_file(host, '/tmp/docker-compose-override-v3.yml', docker_compose_override_v3) create_remote_file(host, '/tmp/docker-stack-override.yml', docker_stack_override_v3) end next unless fact_on(host, 'osfamily') == 'windows' win_host = only_host_with_role(hosts, 'default') retry_on_error_matching(60, 5, %r{connection failure running}) do @windows_ip = win_host.ip end apply_manifest_on(host, "class { 'docker': docker_ee => true, extra_parameters => '\"insecure-registries\": [ \"#{@windows_ip}:5000\" ]' }") docker_path = '/cygdrive/c/Program Files/Docker' host.add_env_var('PATH', docker_path) host.add_env_var('TEMP', 'C:\Users\Administrator\AppData\Local\Temp') puts 'Waiting for box to come online' sleep 300 end end end