require 'spec_helper' require 'fakefs/spec_helpers' # fake the docker-api module Docker class Image end class Container end end module Beaker platforms = [ "ubuntu-14.04-x86_64", "cumulus-2.2-x86_64", "fedora-22-x86_64", "centos-7-x86_64", "sles-12-x86_64", "archlinux-2017.12.27-x86_64" ] describe Docker do let(:hosts) { the_hosts = make_hosts the_hosts[2]['dockeropts'] = { 'Labels' => { 'one' => 3, 'two' => 4, }, } the_hosts } let(:logger) do logger = double('logger') allow( logger ).to receive(:debug) allow( logger ).to receive(:info) allow( logger ).to receive(:warn) allow( logger ).to receive(:error) allow( logger ).to receive(:notify) logger end let(:options) {{ :logger => logger, :forward_ssh_agent => true, :provision => true, :dockeropts => { 'Labels' => { 'one' => 1, 'two' => 2, }, }, }} let(:image) do image = double('Docker::Image') allow( image ).to receive(:id).and_return("zyxwvu") allow( image ).to receive(:tag) image end let(:container) do container = double('Docker::Container') allow( container ).to receive(:id).and_return('abcdef') allow( container ).to receive(:start) allow( container ).to receive(:info).and_return( *(0..2).map { |index| { 'Names' => ["/spec-container-#{index}"] } } ) allow( container ).to receive(:json).and_return({ 'NetworkSettings' => { 'IPAddress' => '192.0.2.1', 'Ports' => { '22/tcp' => [ { 'HostIp' => '127.0.1.1', 'HostPort' => 8022, }, ], }, 'Gateway' => '192.0.2.254' } }) allow( container ).to receive(:kill) allow( container ).to receive(:delete) allow( container ).to receive(:exec) container end let (:docker) { ::Beaker::Docker.new( hosts, options ) } let(:docker_options) { nil } let (:version) { {"ApiVersion"=>"1.18", "Arch"=>"amd64", "GitCommit"=>"4749651", "GoVersion"=>"go1.4.2", "KernelVersion"=>"3.16.0-37-generic", "Os"=>"linux", "Version"=>"1.6.0"} } before :each do # Stub out all of the docker-api gem. we should never really call it # from these tests allow_any_instance_of( ::Beaker::Docker ).to receive(:require).with('docker') allow( ::Docker ).to receive(:options).and_return(docker_options) allow( ::Docker ).to receive(:options=) allow( ::Docker ).to receive(:logger=) allow( ::Docker ).to receive(:version).and_return(version) allow( ::Docker::Image ).to receive(:build).and_return(image) allow( ::Docker::Container ).to receive(:create).and_return(container) allow_any_instance_of( ::Docker::Container ).to receive(:start) end describe '#initialize, failure to validate' do before :each do require 'excon' allow( ::Docker ).to receive(:validate_version!).and_raise(Excon::Errors::SocketError.new( StandardError.new('oops') )) end it 'should fail when docker not present' do expect { docker }.to raise_error(RuntimeError, /Docker instance not connectable./) expect { docker }.to raise_error(RuntimeError, /Check your DOCKER_HOST variable has been set/) expect { docker }.to raise_error(RuntimeError, /If you are on OSX or Windows, you might not have Docker Machine setup correctly/) expect { docker }.to raise_error(RuntimeError, /Error was: oops/) end end describe '#initialize' do before :each do allow( ::Docker ).to receive(:validate_version!) end it 'should require the docker gem' do expect_any_instance_of( ::Beaker::Docker ).to receive(:require).with('docker').once docker end it 'should fail when the gem is absent' do allow_any_instance_of( ::Beaker::Docker ).to receive(:require).with('docker').and_raise(LoadError) expect { docker }.to raise_error(LoadError) end it 'should set Docker options' do expect( ::Docker ).to receive(:options=).with({:write_timeout => 300, :read_timeout => 300}).once docker end context 'when Docker options are already set' do let(:docker_options) {{:write_timeout => 600, :foo => :bar}} it 'should not override Docker options' do expect( ::Docker ).to receive(:options=).with({:write_timeout => 600, :read_timeout => 300, :foo => :bar}).once docker end end it 'should check the Docker gem can work with the api' do expect( ::Docker ).to receive(:validate_version!).once docker end it 'should hook the Beaker logger into the Docker one' do expect( ::Docker ).to receive(:logger=).with(logger) docker end end describe '#install_ssh_components' do let(:test_container) { double('container') } let(:host) {hosts[0]} before :each do allow( ::Docker ).to receive(:validate_version!) allow( docker ).to receive(:dockerfile_for) end platforms.each do |platform| it 'should call exec at least twice' do host['platform'] = platform expect(test_container).to receive(:exec).at_least(:twice) docker.install_ssh_components(test_container, host) end end it 'should accept alpine as valid platform' do host['platform'] = 'alpine-3.8-x86_64' expect(test_container).to receive(:exec).at_least(:twice) docker.install_ssh_components(test_container, host) end it 'should raise an error with an unsupported platform' do host['platform'] = 'boogeyman-2000-x86_64' expect{docker.install_ssh_components(test_container, host)}.to raise_error(RuntimeError, /boogeyman/) end end describe '#provision' do before :each do allow( ::Docker ).to receive(:validate_version!) allow( docker ).to receive(:dockerfile_for) end context 'when the host has "tag" defined' do before :each do hosts.each do |host| host['tag'] = 'my_tag' end end it 'will tag the image with the value of the tag' do expect( image ).to receive(:tag).with({:repo => 'my_tag'}).exactly(3).times docker.provision end end context 'when the host has "use_image_entry_point" set to true on the host' do before :each do hosts.each do |host| host['use_image_entry_point'] = true end end it 'should not call #dockerfile_for but run methods necessary for ssh installation' do expect( docker ).not_to receive(:dockerfile_for) expect( docker ).to receive(:install_ssh_components).exactly(3).times #once per host expect( docker ).to receive(:fix_ssh).exactly(3).times #once per host docker.provision end end context 'when the host has a "dockerfile" for the host' do before :each do allow( docker ).to receive(:buildargs_for).and_return('buildargs') hosts.each do |host| host['dockerfile'] = 'mydockerfile' end end it 'should not call #dockerfile_for but run methods necessary for ssh installation' do allow( File ).to receive(:exist?).with('mydockerfile').and_return(true) allow( ::Docker::Image ).to receive(:build_from_dir).with("/", hash_including(:rm => true, :buildargs => 'buildargs')).and_return(image) expect( docker ).not_to receive(:dockerfile_for) expect( docker ).to receive(:install_ssh_components).exactly(3).times #once per host expect( docker ).to receive(:fix_ssh).exactly(3).times #once per host docker.provision end end it 'should call dockerfile_for with all the hosts' do hosts.each do |host| expect( docker ).not_to receive(:install_ssh_components) expect( docker ).not_to receive(:fix_ssh) expect( docker ).to receive(:dockerfile_for).with(host).and_return('') end docker.provision end it 'should pass the Dockerfile on to Docker::Image.create' do allow( docker ).to receive(:dockerfile_for).and_return('special testing value') expect( ::Docker::Image ).to receive(:build).with('special testing value', { :rm => true, :buildargs => '{}' }) docker.provision end it 'should pass the buildargs from ENV DOCKER_BUILDARGS on to Docker::Image.create' do allow( docker ).to receive(:dockerfile_for).and_return('special testing value') ENV['DOCKER_BUILDARGS'] = 'HTTP_PROXY=http://1.1.1.1:3128' expect( ::Docker::Image ).to receive(:build).with('special testing value', { :rm => true, :buildargs => "{\"HTTP_PROXY\":\"http://1.1.1.1:3128\"}" }) docker.provision end it 'should create a container based on the Image (identified by image.id)' do hosts.each do |host| expect( ::Docker::Container ).to receive(:create).with({ 'Image' => image.id, 'Hostname' => host.name, 'HostConfig' => { 'PortBindings' => { '22/tcp' => [{ 'HostPort' => /\b\d{4}\b/, 'HostIp' => '0.0.0.0'}] }, 'PublishAllPorts' => true, 'Privileged' => true, 'RestartPolicy' => { 'Name' => 'always' } }, 'Labels' => { 'one' => 1, 'two' => 2, }, }).with(hash_excluding('name')) end docker.provision end it 'should pass the multiple buildargs from ENV DOCKER_BUILDARGS on to Docker::Image.create' do allow( docker ).to receive(:dockerfile_for).and_return('special testing value') ENV['DOCKER_BUILDARGS'] = 'HTTP_PROXY=http://1.1.1.1:3128 HTTPS_PROXY=https://1.1.1.1:3129' expect( ::Docker::Image ).to receive(:build).with('special testing value', { :rm => true, :buildargs => "{\"HTTP_PROXY\":\"http://1.1.1.1:3128\",\"HTTPS_PROXY\":\"https://1.1.1.1:3129\"}" }) docker.provision end it 'should create a container based on the Image (identified by image.id)' do hosts.each do |host| expect( ::Docker::Container ).to receive(:create).with({ 'Image' => image.id, 'Hostname' => host.name, 'HostConfig' => { 'PortBindings' => { '22/tcp' => [{ 'HostPort' => /\b\d{4}\b/, 'HostIp' => '0.0.0.0'}] }, 'PublishAllPorts' => true, 'Privileged' => true, 'RestartPolicy' => { 'Name' => 'always' } }, 'Labels' => { 'one' => 1, 'two' => 2, }, }).with(hash_excluding('name')) end docker.provision end it 'should create a named container based on the Image (identified by image.id)' do hosts.each_with_index do |host, index| container_name = "spec-container-#{index}" host['docker_container_name'] = container_name allow(::Docker::Container).to receive(:all).and_return([]) expect( ::Docker::Container ).to receive(:create).with({ 'Image' => image.id, 'Hostname' => host.name, 'name' => container_name, 'HostConfig' => { 'PortBindings' => { '22/tcp' => [{ 'HostPort' => /\b\d{4}\b/, 'HostIp' => '0.0.0.0'}] }, 'PublishAllPorts' => true, 'Privileged' => true, 'RestartPolicy' => { 'Name' => 'always' } }, 'Labels' => { 'one' => (index == 2 ? 3 : 1), 'two' => (index == 2 ? 4 : 2), }, }) end docker.provision end it 'should create a container with volumes bound' do hosts.each_with_index do |host, index| host['mount_folders'] = { 'mount1' => { 'host_path' => '/source_folder', 'container_path' => '/mount_point', }, 'mount2' => { 'host_path' => '/another_folder', 'container_path' => '/another_mount', 'opts' => 'ro', }, 'mount3' => { 'host_path' => '/different_folder', 'container_path' => '/different_mount', 'opts' => 'rw', }, 'mount4' => { 'host_path' => './', 'container_path' => '/relative_mount', }, 'mount5' => { 'host_path' => 'local_folder', 'container_path' => '/another_relative_mount', } } expect( ::Docker::Container ).to receive(:create).with({ 'Image' => image.id, 'Hostname' => host.name, 'HostConfig' => { 'Binds' => [ '/source_folder:/mount_point', '/another_folder:/another_mount:ro', '/different_folder:/different_mount:rw', "#{File.expand_path('./')}:/relative_mount", "#{File.expand_path('local_folder')}:/another_relative_mount", ], 'PortBindings' => { '22/tcp' => [{ 'HostPort' => /\b\d{4}\b/, 'HostIp' => '0.0.0.0'}] }, 'PublishAllPorts' => true, 'Privileged' => true, 'RestartPolicy' => { 'Name' => 'always' } }, 'Labels' => { 'one' => (index == 2 ? 3 : 1), 'two' => (index == 2 ? 4 : 2), }, }) end docker.provision end it 'should create a container with capabilities added' do hosts.each_with_index do |host, index| host['docker_cap_add'] = ['NET_ADMIN', 'SYS_ADMIN'] expect( ::Docker::Container ).to receive(:create).with({ 'Image' => image.id, 'Hostname' => host.name, 'HostConfig' => { 'PortBindings' => { '22/tcp' => [{ 'HostPort' => /\b\d{4}\b/, 'HostIp' => '0.0.0.0'}] }, 'PublishAllPorts' => true, 'Privileged' => true, 'RestartPolicy' => { 'Name' => 'always' }, 'CapAdd' => ['NET_ADMIN', 'SYS_ADMIN'] }, 'Labels' => { 'one' => (index == 2 ? 3 : 1), 'two' => (index == 2 ? 4 : 2), }, }) end docker.provision end it 'should start the container' do expect( container ).to receive(:start) docker.provision end context "connecting to ssh" do before { @docker_host = ENV['DOCKER_HOST'] } after { ENV['DOCKER_HOST'] = @docker_host } it 'should expose port 22 to beaker' do ENV['DOCKER_HOST'] = nil docker.provision expect( hosts[0]['ip'] ).to be === '127.0.1.1' expect( hosts[0]['port'] ).to be === 8022 end it 'should expose port 22 to beaker when using DOCKER_HOST' do ENV['DOCKER_HOST'] = "tcp://192.0.2.2:2375" docker.provision expect( hosts[0]['ip'] ).to be === '192.0.2.2' expect( hosts[0]['port'] ).to be === 8022 end it 'should have ssh agent forwarding enabled' do ENV['DOCKER_HOST'] = nil docker.provision expect( hosts[0]['ip'] ).to be === '127.0.1.1' expect( hosts[0]['port'] ).to be === 8022 expect( hosts[0]['ssh'][:password] ).to be === 'root' expect( hosts[0]['ssh'][:port] ).to be === 8022 expect( hosts[0]['ssh'][:forward_agent] ).to be === true end it 'should connect to gateway ip' do FakeFS do File.open('/.dockerenv', 'w') { } docker.provision expect( hosts[0]['ip'] ).to be === '192.0.2.254' expect( hosts[0]['port'] ).to be === 8022 end end end it "should generate a new /etc/hosts file referencing each host" do ENV['DOCKER_HOST'] = nil docker.provision hosts.each do |host| expect( docker ).to receive( :get_domain_name ).with( host ).and_return( 'labs.lan' ) expect( docker ).to receive( :set_etc_hosts ).with( host, "127.0.0.1\tlocalhost localhost.localdomain\n192.0.2.1\tvm1.labs.lan vm1\n192.0.2.1\tvm2.labs.lan vm2\n192.0.2.1\tvm3.labs.lan vm3\n" ).once end docker.hack_etc_hosts( hosts, options ) end it 'should record the image and container for later' do docker.provision expect( hosts[0]['docker_image_id'] ).to be === image.id expect( hosts[0]['docker_container_id'] ).to be === container.id end context 'provision=false' do let(:options) {{ :logger => logger, :forward_ssh_agent => true, :provision => false }} it 'should fix ssh' do hosts.each_with_index do |host, index| container_name = "spec-container-#{index}" host['docker_container_name'] = container_name expect( ::Docker::Container ).to receive(:all).and_return([container]) expect(docker).to receive(:fix_ssh).exactly(1).times end docker.provision end it 'should not create a container if a named one already exists' do hosts.each_with_index do |host, index| container_name = "spec-container-#{index}" host['docker_container_name'] = container_name expect( ::Docker::Container ).to receive(:all).and_return([container]) expect( ::Docker::Container ).not_to receive(:create) end docker.provision end end end describe '#cleanup' do before :each do # get into a state where there's something to clean allow( ::Docker ).to receive(:validate_version!) allow( ::Docker::Container ).to receive(:all).and_return([container]) allow( ::Docker::Image ).to receive(:remove).with(image.id) allow( docker ).to receive(:dockerfile_for) docker.provision end it 'should stop the containers' do allow( docker ).to receive( :sleep ).and_return(true) expect( container ).to receive(:kill) docker.cleanup end it 'should delete the containers' do allow( docker ).to receive( :sleep ).and_return(true) expect( container ).to receive(:delete) docker.cleanup end it 'should delete the images' do allow( docker ).to receive( :sleep ).and_return(true) expect( ::Docker::Image ).to receive(:remove).with(image.id) docker.cleanup end it 'should not delete the image if docker_preserve_image is set to true' do allow( docker ).to receive( :sleep ).and_return(true) hosts.each do |host| host['docker_preserve_image']=true end expect( ::Docker::Image ).to_not receive(:remove) docker.cleanup end it 'should delete the image if docker_preserve_image is set to false' do allow( docker ).to receive( :sleep ).and_return(true) hosts.each do |host| host['docker_preserve_image']=false end expect( ::Docker::Image ).to receive(:remove).with(image.id) docker.cleanup end end describe '#dockerfile_for' do FakeFS.deactivate! before :each do allow( ::Docker ).to receive(:validate_version!) end it 'should raise on an unsupported platform' do expect { docker.send(:dockerfile_for, {'platform' => 'a_sidewalk', 'image' => 'foobar' }) }.to raise_error(/platform a_sidewalk not yet supported/) end it 'should set "ENV container docker"' do FakeFS.deactivate! platforms.each do |platform| dockerfile = docker.send(:dockerfile_for, { 'platform' => platform, 'image' => 'foobar', }) expect( dockerfile ).to be =~ /ENV container docker/ end end it 'should add docker_image_commands as RUN statements' do FakeFS.deactivate! platforms.each do |platform| dockerfile = docker.send(:dockerfile_for, { 'platform' => platform, 'image' => 'foobar', 'docker_image_commands' => [ 'special one', 'special two', 'special three', ] }) expect( dockerfile ).to be =~ /RUN special one\nRUN special two\nRUN special three/ end end it 'should add docker_image_entrypoint' do FakeFS.deactivate! platforms.each do |platform| dockerfile = docker.send(:dockerfile_for, { 'platform' => platform, 'image' => 'foobar', 'docker_image_entrypoint' => '/bin/bash' }) expect( dockerfile ).to be =~ %r{ENTRYPOINT /bin/bash} end end it 'should use zypper on sles' do FakeFS.deactivate! dockerfile = docker.send(:dockerfile_for, { 'platform' => 'sles-12-x86_64', 'image' => 'foobar', }) expect( dockerfile ).to be =~ /RUN zypper -n in openssh/ end (22..29).to_a.each do | fedora_release | it "should use dnf on fedora #{fedora_release}" do FakeFS.deactivate! dockerfile = docker.send(:dockerfile_for, { 'platform' => "fedora-#{fedora_release}-x86_64", 'image' => 'foobar', }) expect( dockerfile ).to be =~ /RUN dnf install -y sudo/ end end it 'should use pacman on archlinux' do FakeFS.deactivate! dockerfile = docker.send(:dockerfile_for, { 'platform' => 'archlinux-current-x86_64', 'image' => 'foobar', }) expect( dockerfile ).to be =~ /RUN pacman -S --noconfirm openssh/ end end end end