Skip to content

Automatically retry with correct content-type #569

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .cane
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
--abc-max 19
--style-measure 100
--abc-max 30
--style-measure 120
82 changes: 82 additions & 0 deletions .github/workflows/unit_test.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 14 additions & 4 deletions lib/docker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
95 changes: 80 additions & 15 deletions lib/docker/connection.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
7 changes: 7 additions & 0 deletions lib/docker/exec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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) || {}
Expand Down
22 changes: 20 additions & 2 deletions lib/docker/image.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion script/install_docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 -
Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions script/install_podman.sh
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions spec/docker/connection_spec.rb
Original file line number Diff line number Diff line change
@@ -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', {}) }
Expand All @@ -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

Expand Down
Loading