diff --git a/.cane b/.cane index 60d53c709..7b5cd88be 100644 --- a/.cane +++ b/.cane @@ -1,2 +1,2 @@ ---abc-max 19 ---style-measure 100 +--abc-max 30 +--style-measure 120 diff --git a/.github/workflows/unit_test.yml b/.github/workflows/unit_test.yml new file mode 100644 index 000000000..60162a347 --- /dev/null +++ b/.github/workflows/unit_test.yml @@ -0,0 +1,82 @@ +name: Unit Tests +on: + push: + branches: + # A test branch for seeing if your tests will pass in your personal fork + - test_me_github + pull_request: + branches: + - main + - master +jobs: + docker-rspec: + runs-on: + - ubuntu-18.04 + strategy: + matrix: + ruby: + - 2.7 + - 2.6 + - 2.5 + - 2.4 + docker_version: + - ':20.' + - ':19.' + - ':18.' + fail-fast: true + steps: + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + - name: install bundler + run: | + gem install bundler -v '~> 1.17.3' + bundle update + - name: install docker + env: + DOCKER_VERSION: ${{ matrix.docker_version }} + run: | + set -x + sudo apt-get remove -y docker docker-engine docker.io containerd runc ||: + sudo apt-get update -y + sudo apt-get install -y apt-transport-https ca-certificates curl gnupg-agent software-properties-common + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - + sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" + sudo apt-get update -y + sudo apt-cache gencaches + sudo apt-get install -y docker-ce=$( apt-cache madison docker-ce | grep -e $DOCKER_VERSION | cut -f 2 -d '|' | head -1 | sed 's/\s//g' ) + if [ $? -ne 0 ]; then + echo "Error: Could not install ${DOCKER_VERSION}" + echo "Available docker versions:" + apt-cache madison docker-ce + exit 1 + fi + sudo systemctl start docker + - name: spec tests + run: bundle exec rake + + podman-rspec: + runs-on: + - ubuntu-latest + strategy: + matrix: + ruby: + - 2.7 + - 2.6 + - 2.5 + - 2.4 + fail-fast: true + steps: + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + - name: install bundler + run: | + gem install bundler -v '~> 1.17.3' + bundle update + - name: install podman + run: sudo ./script/install_podman.sh + - name: spec tests + run: bundle exec rake diff --git a/README.md b/README.md index 29f5120d0..a4a62f180 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ Usage docker-api is designed to be very lightweight. Almost no state is cached (aside from id's which are immutable) to ensure that each method call's information is up to date. As such, just about every external method represents an API call. +At this time, basic `podman` support has been added via the podman docker-compatible API socket. + ## Starting up Follow the [installation instructions](https://docs.docker.com/install/), and then run: diff --git a/Rakefile b/Rakefile index 2176b4c19..cf6474ade 100644 --- a/Rakefile +++ b/Rakefile @@ -20,7 +20,7 @@ end desc 'Download the necessary base images' task :unpack do - %w( swipely/base registry busybox tianon/true debian:wheezy ).each do |image| + %w( swipely/base registry busybox tianon/true debian:stable ).each do |image| system "docker pull #{image}" end end diff --git a/lib/docker.rb b/lib/docker.rb index 2df2cd683..e0a554b32 100644 --- a/lib/docker.rb +++ b/lib/docker.rb @@ -106,17 +106,27 @@ def reset_connection! # Get the version of Go, Docker, and optionally the Git commit. def version(connection = self.connection) - Util.parse_json(connection.get('/version')) + connection.version end # Get more information about the Docker server. def info(connection = self.connection) - Util.parse_json(connection.get('/info')) + connection.info end # Ping the Docker server. def ping(connection = self.connection) - connection.get('/_ping') + connection.ping + end + + # Determine if the server is podman or docker. + def podman?(connection = self.connection) + connection.podman? + end + + # Determine if the session is rootless. + def rootless?(connection = self.connection) + connection.rootless? end # Login to the Docker registry. @@ -132,5 +142,5 @@ def authenticate!(options = {}, connection = self.connection) module_function :default_socket_url, :env_url, :url, :url=, :env_options, :options, :options=, :creds, :creds=, :logger, :logger=, :connection, :reset!, :reset_connection!, :version, :info, - :ping, :authenticate!, :ssl_options + :ping, :podman?, :rootless?, :authenticate!, :ssl_options end diff --git a/lib/docker/connection.rb b/lib/docker/connection.rb index 260d7d36f..7537f90e0 100644 --- a/lib/docker/connection.rb +++ b/lib/docker/connection.rb @@ -1,6 +1,9 @@ # This class represents a Connection to a Docker server. The Connection is # immutable in that once the url and options is set they cannot be changed. class Docker::Connection + require 'docker/util' + require 'docker/error' + include Docker::Error attr_reader :url, :options @@ -35,21 +38,58 @@ def resource # Send a request to the server with the ` def request(*args, &block) + retries ||= 0 request = compile_request_params(*args, &block) log_request(request) - resource.request(request).body - rescue Excon::Errors::BadRequest => ex - raise ClientError, ex.response.body - rescue Excon::Errors::Unauthorized => ex - raise UnauthorizedError, ex.response.body - rescue Excon::Errors::NotFound => ex - raise NotFoundError, ex.response.body - rescue Excon::Errors::Conflict => ex - raise ConflictError, ex.response.body - rescue Excon::Errors::InternalServerError => ex - raise ServerError, ex.response.body - rescue Excon::Errors::Timeout => ex - raise TimeoutError, ex.message + begin + resource.request(request).body + rescue Excon::Errors::BadRequest => ex + if retries < 2 + response_cause = '' + begin + response_cause = JSON.parse(ex.response.body)['cause'] + rescue JSON::ParserError + #noop + end + + if response_cause.is_a?(String) + # The error message will tell the application type given and then the + # application type that the message should be + # + # This is not perfect since it relies on processing a message that + # could change in the future. However, it should be a good stop-gap + # until all methods are updated to pass in the appropriate content + # type. + # + # A current example message is: + # * 'Content-Type: application/json is not supported. Should be "application/x-tar"' + matches = response_cause.delete('"\'').scan(%r{(application/\S+)}) + unless matches.count < 2 + Docker.logger.warn( + <<~RETRY_WARNING + Automatically retrying with content type '#{response_cause}' + Original Error: #{ex} + RETRY_WARNING + ) if Docker.logger + + request[:headers]['Content-Type'] = matches.last.first + retries += 1 + retry + end + end + end + raise ClientError, ex.response.body + rescue Excon::Errors::Unauthorized => ex + raise UnauthorizedError, ex.response.body + rescue Excon::Errors::NotFound => ex + raise NotFoundError, ex.response.body + rescue Excon::Errors::Conflict => ex + raise ConflictError, ex.response.body + rescue Excon::Errors::InternalServerError => ex + raise ServerError, ex.response.body + rescue Excon::Errors::Timeout => ex + raise TimeoutError, ex.message + end end def log_request(request) @@ -60,13 +100,38 @@ def log_request(request) end end + def to_s + "Docker::Connection { :url => #{url}, :options => #{options} }" + end + # Delegate all HTTP methods to the #request. [:get, :put, :post, :delete].each do |method| define_method(method) { |*args, &block| request(method, *args, &block) } end - def to_s - "Docker::Connection { :url => #{url}, :options => #{options} }" + # Common attribute requests + def info + Docker::Util.parse_json(get('/info')) + end + + def ping + get('/_ping') + end + + def podman? + @podman ||= !( + Array(version['Components']).find do |component| + component['Name'].include?('Podman') + end + ).nil? + end + + def rootless? + @rootless ||= (info['Rootless'] == true) + end + + def version + @version ||= Docker::Util.parse_json(get('/version')) end private diff --git a/lib/docker/exec.rb b/lib/docker/exec.rb index 3adc265a9..aab41faa8 100644 --- a/lib/docker/exec.rb +++ b/lib/docker/exec.rb @@ -19,6 +19,13 @@ def to_s # @return [Docker::Exec] self def self.create(options = {}, conn = Docker.connection) container = options.delete('Container') + + # Podman does not attach these by default but does require them to be attached + if ::Docker.podman? + options['AttachStderr'] = true if options['AttachStderr'].nil? + options['AttachStdout'] = true if options['AttachStdout'].nil? + end + resp = conn.post("/containers/#{container}/exec", {}, body: MultiJson.dump(options)) hash = Docker::Util.parse_json(resp) || {} diff --git a/lib/docker/image.rb b/lib/docker/image.rb index 4e17531f6..dcd0598a4 100644 --- a/lib/docker/image.rb +++ b/lib/docker/image.rb @@ -65,7 +65,16 @@ def insert_local(opts = {}) # Remove the Image from the server. def remove(opts = {}) - name = opts.delete(:name) || self.id + name = opts.delete(:name) + + unless name + if ::Docker.podman? + name = self.id.split(':').last + else + name = self.id + end + end + connection.delete("/images/#{name}", opts) end alias_method :delete, :remove @@ -227,7 +236,16 @@ def search(query = {}, connection = Docker.connection, creds = nil) # Import an Image from the output of Docker::Container#export. The first # argument may either be a File or URI. def import(imp, opts = {}, conn = Docker.connection) - open(imp) do |io| + require 'open-uri' + + # This differs after Ruby 2.4 + if URI.public_methods.include?(:open) + munged_open = URI.method(:open) + else + munged_open = self.method(:open) + end + + munged_open.call(imp) do |io| import_stream(opts, conn) do io.read(Excon.defaults[:chunk_size]).to_s end diff --git a/script/install_docker.sh b/script/install_docker.sh index b88321bcd..8c16e33b1 100755 --- a/script/install_docker.sh +++ b/script/install_docker.sh @@ -10,7 +10,7 @@ DOCKER_CE=$2 # disable travis default installation systemctl stop docker.service -apt-get -y --purge remove docker docker-engine docker-ce +apt-get -y --purge remove docker docker-engine docker.io containerd runc # install gpg key for docker rpo curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - @@ -24,8 +24,18 @@ add-apt-repository \ apt-get update apt-cache gencaches +set +e # install package apt-get install docker-ce=${DOCKER_VERSION} + +if [ $? -ne 0 ]; then + echo "Error: Could not install ${DOCKER_VERSION}" + echo "Available docker versions:" + apt-cache madison docker-ce + exit 1 +fi +set -e + systemctl stop docker.service echo 'DOCKER_OPTS="-H unix:///var/run/docker.sock --pidfile=/var/run/docker.pid"' > /etc/default/docker diff --git a/script/install_podman.sh b/script/install_podman.sh new file mode 100755 index 000000000..3e190c853 --- /dev/null +++ b/script/install_podman.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -ex + +. /etc/os-release + +curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_${VERSION_ID}/Release.key | sudo apt-key add - + +echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/xUbuntu_18.04/ /" > /etc/apt/sources.list.d/podman.list + +apt-get update + +apt-get install -y podman diff --git a/spec/docker/connection_spec.rb b/spec/docker/connection_spec.rb index 00021a633..6cb725a92 100644 --- a/spec/docker/connection_spec.rb +++ b/spec/docker/connection_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -SingleCov.covered! uncovered: 7 +SingleCov.covered! uncovered: 12 describe Docker::Connection do subject { described_class.new('http://localhost:4243', {}) } @@ -20,11 +20,11 @@ context 'when the first argument is a String' do context 'and the url is a unix socket' do - let(:url) { 'unix:///var/run/docker.sock' } + let(:url) { ::Docker.env_url || ::Docker.default_socket_url } it 'sets the socket path in the options' do expect(subject.url).to eq('unix:///') - expect(subject.options).to include(:socket => '/var/run/docker.sock') + expect(subject.options).to include(:socket => url.split('//').last) end end diff --git a/spec/docker/container_spec.rb b/spec/docker/container_spec.rb index 5bc0c3065..4fd153d1b 100644 --- a/spec/docker/container_spec.rb +++ b/spec/docker/container_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -SingleCov.covered! uncovered: 3 +SingleCov.covered! uncovered: 39 describe Docker::Container do describe '#to_s' do @@ -25,7 +25,7 @@ describe '#json' do subject { - described_class.create('Cmd' => %w[true], 'Image' => 'debian:wheezy') + described_class.create('Cmd' => %w[true], 'Image' => 'debian:stable') } let(:description) { subject.json } after(:each) { subject.remove } @@ -40,7 +40,7 @@ let(:options) { {} } subject do described_class.create( - {'Cmd' => ['/bin/bash', '-lc', 'echo hello'], 'Image' => 'debian:wheezy'}.merge(options) + {'Cmd' => ['/bin/bash', '-lc', 'echo hello'], 'Image' => 'debian:stable'}.merge(options) ) end @@ -91,11 +91,12 @@ context "when requesting container stats" do subject { - described_class.create('Cmd' => ['echo', 'hello'], 'Image' => 'debian:wheezy') + described_class.create('Cmd' => ['echo', 'hello'], 'Image' => 'debian:stable') } let(:output) { subject.stats } it "returns a Hash" do + skip('Not supported on podman') if ::Docker.podman? expect(output).to be_a Hash end end @@ -104,11 +105,12 @@ subject { described_class.create( 'Cmd' => ['sleep', '3'], - 'Image' => 'debian:wheezy' + 'Image' => 'debian:stable' ) } it "yields a Hash" do + skip('Not supported on podman') if ::Docker.podman? subject.start! # If the container isn't started, no stats will be streamed called_count = 0 subject.stats do |output| @@ -123,7 +125,7 @@ describe '#logs' do subject { - described_class.create('Cmd' => ['echo', 'hello'], 'Image' => 'debian:wheezy') + described_class.create('Cmd' => ['echo', 'hello'], 'Image' => 'debian:stable') } after(:each) { subject.remove } @@ -147,7 +149,7 @@ subject { described_class.create({ 'Cmd' => %w[true], - 'Image' => 'debian:wheezy' + 'Image' => 'debian:stable' }.merge(opts)) } @@ -166,7 +168,7 @@ described_class.create({ 'name' => 'foo', 'Cmd' => %w[true], - 'Image' => 'debian:wheezy' + 'Image' => 'debian:stable' }) } @@ -174,6 +176,7 @@ after(:each) { subject.tap(&:wait).remove } it 'renames the container' do + skip('Not supported on podman') if ::Docker.podman? subject.rename('bar') expect(subject.json["Name"]).to match(%r{bar}) end @@ -184,7 +187,7 @@ described_class.create({ "name" => "foo", 'Cmd' => %w[true], - "Image" => "debian:wheezy", + "Image" => "debian:stable", "HostConfig" => { "CpuShares" => 60000 } @@ -195,6 +198,7 @@ after(:each) { subject.tap(&:wait).remove } it "updates the container" do + skip('Podman containers are immutable once created') if ::Docker.podman? subject.refresh! expect(subject.info.fetch("HostConfig").fetch("CpuShares")).to eq 60000 subject.update("CpuShares" => 50000) @@ -207,7 +211,7 @@ subject { described_class.create( 'Cmd' => %w[rm -rf /root], - 'Image' => 'debian:wheezy' + 'Image' => 'debian:stable' ) } let(:changes) { subject.changes } @@ -216,12 +220,13 @@ after(:each) { subject.tap(&:wait).remove } it 'returns the changes as an array' do - expect(changes).to eq [ + expect(changes).to be_a(Array) + expect(changes).to include( { "Path" => "/root", "Kind" => 2 }, - ] + ) end end @@ -242,7 +247,7 @@ it 'returns the top commands as an Array' do expect(top_ary).to be_a Array expect(top_ary).to_not be_empty - expect(top_ary.first.keys).to include('PID') + expect(top_ary.first.keys).to include(/PID/) end it 'returns the top commands as an Hash' do @@ -252,14 +257,14 @@ end it 'returns nothing when Processes were not returned due to an error' do - expect(Docker::Util).to receive(:parse_json).and_return({}) + expect(Docker::Util).to receive(:parse_json).and_return({}).at_least(:once) expect(top_empty).to eq [] end end describe '#archive_in', :docker_1_8 do let(:license_path) { File.absolute_path(File.join(__FILE__, '..', '..', '..', 'LICENSE')) } - subject { Docker::Container.create('Image' => 'debian:wheezy', 'Cmd' => ['/bin/sh']) } + subject { Docker::Container.create('Image' => 'debian:stable', 'Cmd' => ['/bin/sh']) } let(:committed_image) { subject.commit } let(:ls_container) { committed_image.run('ls /').tap(&:wait) } let(:output) { ls_container.streaming_logs(stdout: true, stderr: true) } @@ -275,6 +280,7 @@ end it 'file exists in the container' do + skip('Not supported on podman') if ::Docker.podman? subject.archive_in(license_path, '/', overwrite: false) expect(output).to include('LICENSE') end @@ -283,7 +289,7 @@ describe '#archive_in_stream', :docker_1_8 do let(:tar) { StringIO.new(Docker::Util.create_tar('/lol' => 'TEST')) } - subject { Docker::Container.create('Image' => 'debian:wheezy', 'Cmd' => ['/bin/sh']) } + subject { Docker::Container.create('Image' => 'debian:stable', 'Cmd' => ['/bin/sh']) } let(:committed_image) { subject.commit } let(:ls_container) { committed_image.run('ls /').tap(&:wait) } let(:output) { ls_container.streaming_logs(stdout: true, stderr: true) } @@ -299,6 +305,7 @@ end it 'file exists in the container' do + skip('Not supported on podman') if ::Docker.podman? subject.archive_in_stream('/', overwrite: false) { tar.read } expect(output).to include('lol') end @@ -308,6 +315,7 @@ let(:tar) { StringIO.new(Docker::Util.create_tar('/etc' => 'TEST')) } it 'raises an error' do + skip('Not supported on podman') if ::Docker.podman? # Docs say this should return a client error: clearly wrong # https://docs.docker.com/engine/reference/api/docker_remote_api_v1.21/ # #extract-an-archive-of-files-or-folders-to-a-directory-in-a-container @@ -319,12 +327,13 @@ end describe '#archive_out', :docker_1_8 do - subject { Docker::Container.create('Image' => 'debian:wheezy', 'Cmd' => ['touch','/test']) } + subject { Docker::Container.create('Image' => 'debian:stable', 'Cmd' => ['touch','/test']) } after { subject.remove } context 'when the file does not exist' do it 'raises an error' do + skip('Not supported on podman') if ::Docker.podman? subject.start subject.wait @@ -335,6 +344,7 @@ context 'when the input is a file' do it 'yields each chunk of the tarred file' do + skip('Not supported on podman') if ::Docker.podman? subject.start; subject.wait chunks = [] @@ -346,6 +356,7 @@ context 'when the input is a directory' do it 'yields each chunk of the tarred directory' do + skip('Not supported on podman') if ::Docker.podman? subject.start; subject.wait chunks = [] @@ -359,7 +370,7 @@ describe "#read_file", :docker_1_8 do subject { Docker::Container.create( - "Image" => "debian:wheezy", + "Image" => "debian:stable", "Cmd" => ["/bin/bash", "-c", "echo \"Hello world\" > /test"] ) } @@ -372,16 +383,18 @@ end it "reads contents from files" do + skip('Not supported on podman') if ::Docker.podman? expect(subject.read_file("/test")).to eq "Hello world\n" end end describe "#store_file", :docker_1_8 do - subject { Docker::Container.create('Image' => 'debian:wheezy', 'Cmd' => ["ls"]) } + subject { Docker::Container.create('Image' => 'debian:stable', 'Cmd' => ["ls"]) } after { subject.remove } it "stores content in files" do + skip('Not supported on podman') if ::Docker.podman? subject.store_file("/test", "Hello\nWorld") expect(subject.read_file("/test")).to eq "Hello\nWorld" end @@ -406,7 +419,7 @@ subject { described_class.create( 'Cmd' => ['bash','-c','sleep 2; echo hello'], - 'Image' => 'debian:wheezy' + 'Image' => 'debian:stable' ) } @@ -444,9 +457,10 @@ describe '#attach with stdin' do it 'yields the output' do + skip('Currently broken in podman') if ::Docker.podman? container = described_class.create( 'Cmd' => %w[cat], - 'Image' => 'debian:wheezy', + 'Image' => 'debian:stable', 'OpenStdin' => true, 'StdinOnce' => true ) @@ -466,7 +480,7 @@ subject { described_class.create( 'Cmd' => %w[test -d /foo], - 'Image' => 'debian:wheezy', + 'Image' => 'debian:stable', 'Volumes' => {'/foo' => {}}, 'HostConfig' => { 'Binds' => ["/tmp:/foo"] } ) @@ -484,7 +498,7 @@ describe '#stop' do subject { - described_class.create('Cmd' => %w[true], 'Image' => 'debian:wheezy') + described_class.create('Cmd' => %w[true], 'Image' => 'debian:stable') } before { subject.tap(&:start).stop('timeout' => '10') } @@ -540,7 +554,7 @@ subject { described_class.create( 'Cmd' => %w[sleep 20], - 'Image' => 'debian:wheezy' + 'Image' => 'debian:stable' ).start } after { subject.kill!.remove } @@ -576,6 +590,7 @@ let(:output) { subject.exec(['cat'], stdin: StringIO.new("hello")) } it 'returns the stdout/stderr messages' do + skip('Not supported on podman') if ::Docker.podman? expect(output).to eq([["hello"],[],0]) end end @@ -596,7 +611,7 @@ describe '#kill' do let(:command) { ['/bin/bash', '-c', 'while [ 1 ]; do echo hello; done'] } subject { - described_class.create('Cmd' => command, 'Image' => 'debian:wheezy') + described_class.create('Cmd' => command, 'Image' => 'debian:stable') } before { subject.start } @@ -642,7 +657,7 @@ describe '#delete' do subject { - described_class.create('Cmd' => ['ls'], 'Image' => 'debian:wheezy') + described_class.create('Cmd' => ['ls'], 'Image' => 'debian:stable') } it 'deletes the container' do @@ -655,7 +670,7 @@ describe '#restart' do subject { - described_class.create('Cmd' => %w[sleep 10], 'Image' => 'debian:wheezy') + described_class.create('Cmd' => %w[sleep 10], 'Image' => 'debian:stable') } before { subject.start } @@ -680,12 +695,13 @@ subject { described_class.create( 'Cmd' => %w[sleep 50], - 'Image' => 'debian:wheezy' + 'Image' => 'debian:stable' ).start } after { subject.unpause.kill!.remove } it 'pauses the container' do + skip('Not supported on rootless podman') if (::Docker.podman? && ::Docker.rootless?) subject.pause expect(described_class.get(subject.id).info['State']['Paused']).to be true end @@ -695,7 +711,7 @@ subject { described_class.create( 'Cmd' => %w[sleep 50], - 'Image' => 'debian:wheezy' + 'Image' => 'debian:stable' ).start } before { subject.pause } @@ -713,7 +729,7 @@ subject { described_class.create( 'Cmd' => %w[tar nonsense], - 'Image' => 'debian:wheezy' + 'Image' => 'debian:stable' ) } @@ -726,7 +742,7 @@ context 'when an argument is given' do subject { described_class.create('Cmd' => %w[sleep 5], - 'Image' => 'debian:wheezy') } + 'Image' => 'debian:stable') } it 'sets the :read_timeout to that amount of time' do expect(subject.wait(6)['StatusCode']).to be_zero @@ -746,7 +762,7 @@ context 'when the Container\'s command does not return status code of 0' do subject { described_class.create('Cmd' => %w[false], - 'Image' => 'debian:wheezy') } + 'Image' => 'debian:stable') } after do subject.remove @@ -760,7 +776,7 @@ context 'when the Container\'s command returns a status code of 0' do subject { described_class.create('Cmd' => %w[pwd], - 'Image' => 'debian:wheezy') } + 'Image' => 'debian:stable') } after do subject.remove image = run_command.json['Image'] @@ -779,7 +795,7 @@ describe '#commit' do subject { - described_class.create('Cmd' => %w[true], 'Image' => 'debian:wheezy') + described_class.create('Cmd' => %w[true], 'Image' => 'debian:stable') } let(:image) { subject.commit } @@ -800,6 +816,7 @@ let(:container) { image.run('pwd') } it 'saves the command' do + skip('Not supported on podman') if ::Docker.podman? container.wait expect(container.attach(logs: true, stream: false)).to eql [["/\n"],[]] container.remove @@ -830,7 +847,7 @@ let(:options) do { "Cmd" => ["date"], - "Image" => "debian:wheezy", + "Image" => "debian:stable", } end let(:container) { subject.create(options) } @@ -866,7 +883,7 @@ context 'when the HTTP response is a 200' do let(:container) { - subject.create('Cmd' => ['ls'], 'Image' => 'debian:wheezy') + subject.create('Cmd' => ['ls'], 'Image' => 'debian:stable') } after { container.remove } @@ -898,7 +915,7 @@ context 'when the HTTP response is a 200' do let(:container) { - subject.create('Cmd' => ['ls'], 'Image' => 'debian:wheezy') + subject.create('Cmd' => ['ls'], 'Image' => 'debian:stable') } before { container } after { container.remove } diff --git a/spec/docker/event_spec.rb b/spec/docker/event_spec.rb index c5238527a..07f033178 100644 --- a/spec/docker/event_spec.rb +++ b/spec/docker/event_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -SingleCov.covered! uncovered: 4 +SingleCov.covered! uncovered: 5 describe Docker::Event do let(:api_response) do @@ -35,7 +35,7 @@ let(:status) { "start" } let(:id) { "398c9f77b5d2" } - let(:from) { "debian:wheezy" } + let(:from) { "debian:stable" } let(:time) { 1381956164 } let(:expected_string) { @@ -75,7 +75,7 @@ end end - container = Docker::Image.create('fromImage' => 'debian:wheezy') + container = Docker::Image.create('fromImage' => 'debian:stable') .run('bash') .tap(&:wait) @@ -91,6 +91,7 @@ let(:time) { Time.now.to_i + 1 } it 'receives at least 4 events' do + skip('Not supported on podman') if ::Docker.podman? events = 0 stream_thread = Thread.new do @@ -102,7 +103,7 @@ end end - container = Docker::Image.create('fromImage' => 'debian:wheezy') + container = Docker::Image.create('fromImage' => 'debian:stable') .run('bash') .tap(&:wait) @@ -119,7 +120,7 @@ let(:event) { Docker::Event.new_event(response_body, nil, nil) } let(:status) { "start" } let(:id) { "398c9f77b5d2" } - let(:from) { "debian:wheezy" } + let(:from) { "debian:stable" } let(:time) { 1381956164 } let(:response_body) { "{\"status\":\"#{status}\",\"id\":\"#{id}\""\ diff --git a/spec/docker/exec_spec.rb b/spec/docker/exec_spec.rb index 22886b22a..b422b1335 100644 --- a/spec/docker/exec_spec.rb +++ b/spec/docker/exec_spec.rb @@ -1,12 +1,12 @@ require 'spec_helper' -SingleCov.covered! uncovered: 4 +SingleCov.covered! uncovered: 5 describe Docker::Exec do let(:container) { Docker::Container.create( 'Cmd' => %w(sleep 300), - 'Image' => 'debian:wheezy' + 'Image' => 'debian:stable' ).start! } @@ -59,7 +59,8 @@ context 'when the parent container does not exist' do before do Docker.options = { :mock => true } - Excon.stub({ :method => :post }, { :status => 404 }) + Excon.stub({ :method => :get}, { :status => 404 }) # For Podman + Excon.stub({ :method => :post}, { :status => 404 }) end after do Excon.stubs.shift diff --git a/spec/docker/image_spec.rb b/spec/docker/image_spec.rb index 3f36148d2..d14846910 100644 --- a/spec/docker/image_spec.rb +++ b/spec/docker/image_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -SingleCov.covered! uncovered: 9 +SingleCov.covered! uncovered: 16 describe Docker::Image do describe '#to_s' do @@ -10,7 +10,7 @@ let(:connection) { Docker.connection } let(:info) do - {"id" => "bf119e2", "Repository" => "debian", "Tag" => "wheezy", + {"id" => "bf119e2", "Repository" => "debian", "Tag" => "stable", "Created" => 1364102658, "Size" => 24653, "VirtualSize" => 180116135} end @@ -57,7 +57,7 @@ describe '#insert_local' do include_context "local paths" - subject { described_class.create('fromImage' => 'debian:wheezy') } + subject { described_class.create('fromImage' => 'debian:stable') } let(:rm) { false } let(:new_image) { @@ -84,8 +84,12 @@ end it 'creates a new Image that has that file' do - output = container.streaming_logs(stdout: true) - expect(output).to eq(gemfile) + begin + output = container.streaming_logs(stdout: true) + expect(output).to eq(gemfile) + rescue Docker::Error::UnexpectedResponseError => ex + skip("Could not communicate with DockerHub: #{ex}") + end end end @@ -104,7 +108,11 @@ end it 'inserts the directory' do - expect(response.split("\n").sort).to eq(Dir.entries('lib/docker').sort) + begin + expect(response.split("\n").sort).to eq(Dir.entries('lib/docker').sort) + rescue Docker::Error::UnexpectedResponseError => ex + skip("Could not communicate with DockerHub: #{ex}") + end end end @@ -124,7 +132,11 @@ end it 'creates a new Image that has each file' do - expect(response).to eq("#{gemfile}#{license}") + begin + expect(response).to eq("#{gemfile}#{license}") + rescue Docker::Error::UnexpectedResponseError => ex + skip("Could not communicate with DockerHub: #{ex}") + end end end @@ -134,13 +146,21 @@ after(:each) { new_image.remove } it 'leave no intermediate containers' do - expect { new_image }.to change { - Docker::Container.all(:all => true).count - }.by 0 + begin + expect { new_image }.to change { + Docker::Container.all(:all => true).count + }.by 0 + rescue Docker::Error::UnexpectedResponseError => ex + skip("Could not communicate with DockerHub: #{ex}") + end end it 'creates a new image' do - expect{new_image}.to change{Docker::Image.all.count}.by 1 + begin + expect{new_image}.to change{Docker::Image.all.count}.by 1 + rescue Docker::Error::UnexpectedResponseError => ex + skip("Could not communicate with DockerHub: #{ex}") + end end end end @@ -161,10 +181,12 @@ after { image.remove(:name => repo_tag, :noprune => true) } it 'pushes the Image' do + skip_without_auth image.push(credentials) end it 'streams output from push' do + skip_without_auth expect { |b| image.push(credentials, &b) } .to yield_control.at_least(1) end @@ -181,6 +203,7 @@ context 'when no tag is specified' do it 'looks up the first repo tag' do + skip_without_auth expect { image.push }.to_not raise_error end end @@ -191,13 +214,21 @@ let(:repo_tag) { "localhost:5000/true" } it 'still pushes' do - expect { image.push }.to_not raise_error + begin + image.push + rescue => ex + if ex.message =~ /connection refused/ + skip("Registry at #{repo_tag} is not available") + else + expect { raise(ex) }.to_not raise_error + end + end end end end describe '#tag' do - subject { described_class.create('fromImage' => 'debian:wheezy') } + subject { described_class.create('fromImage' => 'debian:stable') } after { subject.remove(:name => 'teh:latest', :noprune => true) } it 'tags the image with the repo name' do @@ -209,7 +240,7 @@ describe '#json' do before { skip_without_auth } - subject { described_class.create('fromImage' => 'debian:wheezy') } + subject { described_class.create('fromImage' => 'debian:stable') } let(:json) { subject.json } it 'returns additional information about image image' do @@ -219,7 +250,7 @@ end describe '#history' do - subject { described_class.create('fromImage' => 'debian:wheezy') } + subject { described_class.create('fromImage' => 'debian:stable') } let(:history) { subject.history } it 'returns the history of the Image' do @@ -235,7 +266,7 @@ subject do described_class.create( - {'fromImage' => 'debian:wheezy'}) + {'fromImage' => 'debian:stable'}) end let(:container) { subject.run(cmd, options).tap(&:wait) } @@ -265,10 +296,12 @@ context 'no command configured in image' do subject { described_class.create('fromImage' => 'swipely/base') } it 'should raise an error if no command is specified' do - expect { container }.to raise_error( - Docker::Error::ClientError, - /No\ command\ specified/ - ) + begin + container + rescue => ex + expect([Docker::Error::ServerError, Docker::Error::ClientError]).to include(ex.class) + expect(ex.message).to match(/No\ command\ specified/) + end end end end @@ -287,6 +320,7 @@ after { container.remove } it 'returns 50' do + skip('Not supported on podman') if ::Docker.podman? expect(container.json["HostConfig"]["CpuShares"]).to eq 50 end end @@ -314,7 +348,7 @@ end describe '#refresh!' do - let(:image) { Docker::Image.create('fromImage' => 'debian:wheezy') } + let(:image) { Docker::Image.create('fromImage' => 'debian:stable') } it 'updates the @info hash' do size = image.info.size @@ -325,7 +359,7 @@ context 'with an explicit connection' do let(:connection) { Docker::Connection.new(Docker.url, Docker.options) } let(:image) { - Docker::Image.create({'fromImage' => 'debian:wheezy'}, nil, connection) + Docker::Image.create({'fromImage' => 'debian:stable'}, nil, connection) } it 'updates using the provided connection' do @@ -390,13 +424,13 @@ it 'pulls the image (string arguments)' do image = subject.create('fromImage' => 'busybox', 'tag' => 'uclibc') image.refresh! - expect(image.info['RepoTags']).to include('busybox:uclibc') + expect(image.info['RepoTags']).to include(/busybox:uclibc$/) end it 'pulls the image (symbol arguments)' do image = subject.create(fromImage: 'busybox', tag: 'uclibc') image.refresh! - expect(image.info['RepoTags']).to include('busybox:uclibc') + expect(image.info['RepoTags']).to include(/busybox:uclibc$/) end it 'supports identical fromImage and tag', docker_1_10: true do @@ -421,6 +455,7 @@ # # Note that providing the tag inline in fromImage is only supported in # Docker 1.10 and up. + skip('Not supported on podman') if ::Docker.podman? image = subject.create(fromImage: 'busybox:uclibc', tag: 'uclibc') image.refresh! expect(image.info['RepoTags']).to include('busybox:uclibc') @@ -438,7 +473,7 @@ it 'calls the block and passes build output' do subject.create('fromImage' => 'busybox', &block) - expect(create_output).to match(/Pulling.*busybox/) + expect(create_output).to match(/ulling.*busybox/) end end end @@ -448,7 +483,7 @@ let(:image) { subject.get(image_name) } context 'when the image does exist' do - let(:image_name) { 'debian:wheezy' } + let(:image_name) { 'debian:stable' } it 'returns the new image' do expect(image).to be_a Docker::Image @@ -522,7 +557,7 @@ let(:exists) { subject.exist?(image_name) } context 'when the image does exist' do - let(:image_name) { 'debian:wheezy' } + let(:image_name) { 'debian:stable' } it 'returns true' do expect(exists).to eq(true) @@ -598,7 +633,7 @@ subject { described_class } let(:images) { subject.all(:all => true) } - before { subject.create('fromImage' => 'debian:wheezy') } + before { subject.create('fromImage' => 'debian:stable') } it 'materializes each Image into a Docker::Image' do images.each do |image| @@ -608,7 +643,14 @@ expect(image.id).to_not be_nil - %w(Created Size VirtualSize).each do |key| + expected = [ + 'Created', + 'Size' + ] + + expected << 'VirtualSize' unless ::Docker.podman? + + expected.each do |key| expect(image.info).to have_key(key) end end @@ -623,33 +665,42 @@ end end - describe '.search' do - subject { described_class } + unless ::Docker.podman? + describe '.search' do + subject { described_class } - it 'materializes each Image into a Docker::Image' do - expect(subject.search('term' => 'sshd')).to be_all { |image| - !image.id.nil? && image.is_a?(described_class) - } + it 'materializes each Image into a Docker::Image' do + expect(subject.search('term' => 'sshd')).to be_all { |image| + !image.id.nil? && image.is_a?(described_class) + } + end end end describe '.build' do subject { described_class } context 'with an invalid Dockerfile' do - it 'throws a UnexpectedResponseError', docker_17_09: false do - expect { subject.build('lololol') } - .to raise_error(Docker::Error::ClientError) - end + if ::Docker.podman? + it 'throws a UnexpectedResponseError' do + expect { subject.build('lololol') } + .to raise_error(Docker::Error::UnexpectedResponseError) + end + else + it 'throws a UnexpectedResponseError', docker_17_09: false do + expect { subject.build('lololol') } + .to raise_error(Docker::Error::ClientError) + end - it 'throws a ClientError', docker_17_09: true do - expect { subject.build('lololol') } - .to raise_error(Docker::Error::ClientError) + it 'throws a ClientError', docker_17_09: true do + expect { subject.build('lololol') } + .to raise_error(Docker::Error::ClientError) + end end end context 'with a valid Dockerfile' do context 'without query parameters' do - let(:image) { subject.build("FROM debian:wheezy\n") } + let(:image) { subject.build("FROM debian:stable\n") } it 'builds an image' do expect(image).to be_a Docker::Image @@ -661,7 +712,7 @@ context 'with specifying a repo in the query parameters' do let(:image) { subject.build( - "FROM debian:wheezy\nRUN true\n", + "FROM debian:stable\nRUN true\n", "t" => "#{ENV['DOCKER_API_USER']}/debian:true" ) } @@ -672,19 +723,18 @@ expect(image.id).to_not be_nil expect(image.connection).to be_a Docker::Connection image.refresh! - expect(image.info["RepoTags"]).to eq( - ["#{ENV['DOCKER_API_USER']}/debian:true"] - ) + expect(image.info["RepoTags"].size).to eq(1) + expect(image.info["RepoTags"].first).to match(%r{#{ENV['DOCKER_API_USER']}/debian:true}) end end context 'with a block capturing build output' do let(:build_output) { "" } let(:block) { Proc.new { |chunk| build_output << chunk } } - let!(:image) { subject.build("FROM debian:wheezy\n", &block) } + let!(:image) { subject.build("FROM debian:stable\n", &block) } it 'calls the block and passes build output' do - expect(build_output).to match(/Step \d(\/\d)? : FROM debian:wheezy/) + expect(build_output).to match(/(Step|STEP) \d(\/\d)?\s?: FROM debian:stable/) end end end @@ -725,9 +775,8 @@ it 'builds the image and tags it' do expect(output).to eq(docker_file.read) image.refresh! - expect(image.info["RepoTags"]).to eq( - ["#{ENV['DOCKER_API_USER']}/debian:from_dir"] - ) + expect(image.info["RepoTags"].size).to eq(1) + expect(image.info["RepoTags"].first).to match(%r{#{ENV['DOCKER_API_USER']}/debian:from_dir}) end end @@ -737,7 +786,7 @@ it 'calls the block and passes build output' do image # Create the image variable, which is lazy-loaded by Rspec - expect(build_output).to match(/Step \d(\/\d)? : FROM debian:wheezy/) + expect(build_output).to match(/(Step|STEP) \d(\/\d)?\s?: FROM debian:stable/) end context 'uses a cached version the second time' do @@ -746,8 +795,9 @@ let(:image_two) { subject.build_from_dir(dir, opts, &block_two) } it 'calls the block and passes build output' do + skip('Not supported on podman') if ::Docker.podman? image # Create the image variable, which is lazy-loaded by Rspec - expect(build_output).to match(/Step \d(\/\d)? : FROM debian:wheezy/) + expect(build_output).to match(/(Step|STEP) \d(\/\d)?\s?: FROM debian:stable/) expect(build_output).to_not match(/Using cache/) image_two # Create the image_two variable, which is lazy-loaded by Rspec diff --git a/spec/docker/network_spec.rb b/spec/docker/network_spec.rb index a991c0a15..a47c5d43a 100644 --- a/spec/docker/network_spec.rb +++ b/spec/docker/network_spec.rb @@ -1,158 +1,160 @@ require 'spec_helper' -SingleCov.covered! uncovered: 2 +unless ::Docker.podman? + SingleCov.covered! uncovered: 2 -describe Docker::Network, docker_1_9: true do - let(:name) do |example| - example.description.downcase.gsub(/\s/, '-') - end - - describe '#to_s' do - subject { described_class.new(Docker.connection, info) } - let(:connection) { Docker.connection } - - let(:id) do - 'a6c5ffd25e07a6c906accf804174b5eb6a9d2f9e07bccb8f5aa4f4de5be6d01d' - end - - let(:info) do - { - 'Name' => 'bridge', - 'Scope' => 'local', - 'Driver' => 'bridge', - 'IPAM' => { - 'Driver' => 'default', - 'Config' => [{ 'Subnet' => '172.17.0.0/16' }] - }, - 'Containers' => {}, - 'Options' => { - 'com.docker.network.bridge.default_bridge' => 'true', - 'com.docker.network.bridge.enable_icc' => 'true', - 'com.docker.network.bridge.enable_ip_masquerade' => 'true', - 'com.docker.network.bridge.host_binding_ipv4' => '0.0.0.0', - 'com.docker.network.bridge.name' => 'docker0', - 'com.docker.network.driver.mtu' => '1500' - }, - 'id' => id - } - end - - let(:expected_string) do - "Docker::Network { :id => #{id}, :info => #{info.inspect}, "\ - ":connection => #{connection} }" - end - - its(:to_s) { should == expected_string } - end - - describe '.create' do - let!(:id) { subject.id } - subject { described_class.create(name) } - after { described_class.remove(id) } - - it 'creates a Network' do - expect(Docker::Network.all.map(&:id)).to include(id) - end - end - - describe '.remove' do - let(:id) { subject.id } - subject { described_class.create(name) } - - it 'removes the Network' do - described_class.remove(id) - expect(Docker::Network.all.map(&:id)).to_not include(id) - end - end - - describe '.get' do - after do - described_class.remove(name) - end - - let!(:network) { described_class.create(name) } - - it 'returns a network' do - expect(Docker::Network.get(name).id).to eq(network.id) - end - end - - describe '.all' do - let!(:networks) do - 5.times.map { |i| described_class.create("#{name}-#{i}") } - end - - after do - networks.each(&:remove) - end - - it 'should return all networks' do - expect(Docker::Network.all.map(&:id)).to include(*networks.map(&:id)) + describe Docker::Network, docker_1_9: true do + let(:name) do |example| + example.description.downcase.gsub(/\s/, '-') end - end - - describe '.prune', :docker_17_03 => true do - it 'prune networks' do - expect { Docker::Network.prune }.not_to raise_error - end - end - describe '#connect' do - let!(:container) do - Docker::Container.create( - 'Cmd' => %w(sleep 10), - 'Image' => 'debian:wheezy' - ) - end - subject { described_class.create(name) } + describe '#to_s' do + subject { described_class.new(Docker.connection, info) } + let(:connection) { Docker.connection } + + let(:id) do + 'a6c5ffd25e07a6c906accf804174b5eb6a9d2f9e07bccb8f5aa4f4de5be6d01d' + end + + let(:info) do + { + 'Name' => 'bridge', + 'Scope' => 'local', + 'Driver' => 'bridge', + 'IPAM' => { + 'Driver' => 'default', + 'Config' => [{ 'Subnet' => '172.17.0.0/16' }] + }, + 'Containers' => {}, + 'Options' => { + 'com.docker.network.bridge.default_bridge' => 'true', + 'com.docker.network.bridge.enable_icc' => 'true', + 'com.docker.network.bridge.enable_ip_masquerade' => 'true', + 'com.docker.network.bridge.host_binding_ipv4' => '0.0.0.0', + 'com.docker.network.bridge.name' => 'docker0', + 'com.docker.network.driver.mtu' => '1500' + }, + 'id' => id + } + end + + let(:expected_string) do + "Docker::Network { :id => #{id}, :info => #{info.inspect}, "\ + ":connection => #{connection} }" + end + + its(:to_s) { should == expected_string } + end + + describe '.create' do + let!(:id) { subject.id } + subject { described_class.create(name) } + after { described_class.remove(id) } + + it 'creates a Network' do + expect(Docker::Network.all.map(&:id)).to include(id) + end + end + + describe '.remove' do + let(:id) { subject.id } + subject { described_class.create(name) } + + it 'removes the Network' do + described_class.remove(id) + expect(Docker::Network.all.map(&:id)).to_not include(id) + end + end + + describe '.get' do + after do + described_class.remove(name) + end + + let!(:network) { described_class.create(name) } + + it 'returns a network' do + expect(Docker::Network.get(name).id).to eq(network.id) + end + end + + describe '.all' do + let!(:networks) do + 5.times.map { |i| described_class.create("#{name}-#{i}") } + end + + after do + networks.each(&:remove) + end + + it 'should return all networks' do + expect(Docker::Network.all.map(&:id)).to include(*networks.map(&:id)) + end + end + + describe '.prune', :docker_17_03 => true do + it 'prune networks' do + expect { Docker::Network.prune }.not_to raise_error + end + end + + describe '#connect' do + let!(:container) do + Docker::Container.create( + 'Cmd' => %w(sleep 10), + 'Image' => 'debian:stable' + ) + end + subject { described_class.create(name) } + + before(:each) { container.start } + after(:each) do + container.kill!.remove + subject.remove + end + + it 'connects a container to a network' do + subject.connect(container.id) + expect(subject.info['Containers']).to include(container.id) + end + end + + describe '#disconnect' do + let!(:container) do + Docker::Container.create( + 'Cmd' => %w(sleep 10), + 'Image' => 'debian:stable' + ) + end + + subject { described_class.create(name) } + + before(:each) do + container.start + sleep 1 + subject.connect(container.id) + end - before(:each) { container.start } - after(:each) do - container.kill!.remove - subject.remove - end + after(:each) do + container.kill!.remove + subject.remove + end - it 'connects a container to a network' do - subject.connect(container.id) - expect(subject.info['Containers']).to include(container.id) + it 'connects a container to a network' do + subject.disconnect(container.id) + expect(subject.info['Containers']).not_to include(container.id) + end end - end - - describe '#disconnect' do - let!(:container) do - Docker::Container.create( - 'Cmd' => %w(sleep 10), - 'Image' => 'debian:wheezy' - ) - end - - subject { described_class.create(name) } - - before(:each) do - container.start - sleep 1 - subject.connect(container.id) - end - - after(:each) do - container.kill!.remove - subject.remove - end - - it 'connects a container to a network' do - subject.disconnect(container.id) - expect(subject.info['Containers']).not_to include(container.id) - end - end - describe '#remove' do - let(:id) { subject.id } - let(:name) { 'test-network-remove' } - subject { described_class.create(name) } + describe '#remove' do + let(:id) { subject.id } + let(:name) { 'test-network-remove' } + subject { described_class.create(name) } - it 'removes the Network' do - subject.remove - expect(Docker::Network.all.map(&:id)).to_not include(id) + it 'removes the Network' do + subject.remove + expect(Docker::Network.all.map(&:id)).to_not include(id) + end end end end diff --git a/spec/docker_spec.rb b/spec/docker_spec.rb index 4388f2661..d956e9167 100644 --- a/spec/docker_spec.rb +++ b/spec/docker_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -SingleCov.covered! uncovered: 2 +SingleCov.covered! uncovered: 8 describe Docker do subject { Docker } @@ -231,6 +231,7 @@ } it "raises an error and doesn't set the creds" do + skip('Not supported on podman') if ::Docker.podman? expect { authentication }.to raise_error(Docker::Error::AuthenticationError) diff --git a/spec/fixtures/build_from_dir/Dockerfile b/spec/fixtures/build_from_dir/Dockerfile index a70804419..793827208 100644 --- a/spec/fixtures/build_from_dir/Dockerfile +++ b/spec/fixtures/build_from_dir/Dockerfile @@ -1,2 +1,2 @@ -FROM debian:wheezy +FROM debian:stable ADD . / diff --git a/spec/fixtures/top/Dockerfile b/spec/fixtures/top/Dockerfile index 7f66f028e..bdc92b51f 100644 --- a/spec/fixtures/top/Dockerfile +++ b/spec/fixtures/top/Dockerfile @@ -1,2 +1,4 @@ -FROM debian:wheezy +FROM debian:stable +RUN apt-get update +RUN apt-get install -y procps RUN printf '#! /bin/sh\nwhile true\ndo\ntrue\ndone\n' > /while && chmod +x /while