require 'spec_helper'

# fake the docker-api
module Docker
  class Image
  end
  class Container
  end
end

module Beaker
  describe Docker do
    let(:hosts) { make_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,
    }}

    let(:image) do
      image = double('Docker::Image')
      allow( image ).to receive(:id)
      allow( image ).to receive(:tag)
      allow( image ).to receive(:delete)
      image
    end

    let(:container) do
      container = double('Docker::Container')
      allow( container ).to receive(:id)
      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,
              },
            ],
          },
        },
      })
      allow( container ).to receive(:stop)
      allow( container ).to receive(:delete)
      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, /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 '#provision' do
      before :each do
        allow( ::Docker ).to receive(:validate_version!)
        allow( docker ).to receive(:dockerfile_for)
      end

      it 'should call dockerfile_for with all the hosts' do
        hosts.each do |host|
          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 })

        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,
          })
        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

          expect( ::Docker::Container ).to receive(:all).and_return([])

          expect( ::Docker::Container ).to receive(:create).with({
            'Image' => image.id,
            'Hostname' => host.name,
            'name' => container_name,
          })
        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

      it 'should start the container' do
        expect( container ).to receive(:start).with({'PublishAllPorts' => true, 'Privileged' => true})

        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

      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'] ).to be === image
        expect( hosts[0]['docker_container'] ).to be === container
      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 ).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(:stop)
        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( image ).to receive(:delete)
        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( image ).to_not receive(:delete)
        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( image ).to receive(:delete)
        docker.cleanup
      end

    end

    describe '#dockerfile_for' do
      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' }) }.to raise_error(/platform a_sidewalk not yet supported on docker/)
      end

      it 'should add docker_image_commands as RUN statements' do
        dockerfile = docker.send(:dockerfile_for, {
          'platform' => 'el-',
          'docker_image_commands' => [
            'special one',
            'special two',
            'special three',
          ]
        })

        expect( dockerfile ).to be =~ /RUN special one\nRUN special two\nRUN special three/
      end

      it 'should add docker_image_entrypoint' do
        dockerfile = docker.send(:dockerfile_for, {
          'platform' => 'el-',
          'docker_image_entrypoint' => '/bin/bash'
        })

        expect( dockerfile ).to be =~ %r{ENTRYPOINT /bin/bash}
      end

      it 'should use zypper on sles' do
        dockerfile = docker.send(:dockerfile_for, {
          'platform' => 'sles',
        })

        expect( dockerfile ).to be =~ /RUN zypper -n in openssh/
      end
    end
  end
end
