diff --git a/docker-api.gemspec b/docker-api.gemspec index 1a131c76..3d652824 100644 --- a/docker-api.gemspec +++ b/docker-api.gemspec @@ -16,7 +16,7 @@ Gem::Specification.new do |gem| gem.add_dependency 'multi_json' gem.add_development_dependency 'rake' gem.add_development_dependency 'rspec', '~> 3.0' - gem.add_development_dependency 'rspec-its' + gem.add_development_dependency 'rspec-its', '~> 1' gem.add_development_dependency 'pry' gem.add_development_dependency 'single_cov' gem.add_development_dependency 'webmock' diff --git a/lib/docker/connection.rb b/lib/docker/connection.rb index d678d243..c41f26e4 100644 --- a/lib/docker/connection.rb +++ b/lib/docker/connection.rb @@ -137,24 +137,24 @@ def version end private - # Given an HTTP method, path, optional query, extra options, and block, - # compiles a request. + def compile_request_params(http_method, path, query = nil, opts = nil, &block) - query ||= {} opts ||= {} - headers = opts.delete(:headers) || {} - content_type = opts[:body].nil? ? 'text/plain' : 'application/json' - user_agent = "Swipely/Docker-API #{Docker::VERSION}" + query ||= opts.delete(:query) || {} + + default_headers = { + 'Content-Type' => opts[:body].nil? ? 'text/plain' : 'application/json', + 'User-Agent' => "Swipely/Docker-API #{Docker::VERSION}", + } + headers = default_headers.merge(opts.delete(:headers) || {}) + { - :method => http_method, - :path => path, - :query => query, - :headers => { 'Content-Type' => content_type, - 'User-Agent' => user_agent, - }.merge(headers), - :expects => (200..204).to_a << 301 << 304, - :idempotent => http_method == :get, - :request_block => block, - }.merge(opts).reject { |_, v| v.nil? } + method: http_method, + path: path, + headers: headers, + query: query, + expects: (200..204).to_a << 301 << 304, + idempotent: http_method == :get, + }.merge(opts).tap { |params| params[:request_block] = block if block } end end diff --git a/lib/docker/event.rb b/lib/docker/event.rb index 4945bca4..f4b753b4 100644 --- a/lib/docker/event.rb +++ b/lib/docker/event.rb @@ -1,40 +1,49 @@ # frozen_string_literal: true # This class represents a Docker Event. +# @see https://github.com/moby/moby/blob/master/api/types/events/events.go +# @see https://docs.docker.com/reference/api/engine/version/v1.49/#tag/System/operation/SystemEvents class Docker::Event include Docker::Error # Represents the actor object nested within an event class Actor - attr_accessor :ID, :Attributes + attr_reader :info - def initialize(actor_attributes = {}) - [:ID, :Attributes].each do |sym| - value = actor_attributes[sym] - if value.nil? - value = actor_attributes[sym.to_s] - end - send("#{sym}=", value) - end + def initialize(actor_data = {}) + @info = actor_data.transform_keys { |k| k.downcase.to_sym } + end - if self.Attributes.nil? - self.Attributes = {} - end + def id + info[:id] end + alias_method :ID, :id - alias_method :id, :ID - alias_method :attributes, :Attributes + def attributes + info[:attributes] || {} + end + alias_method :Attributes, :attributes end class << self include Docker::Error def stream(opts = {}, conn = Docker.connection, &block) - conn.get('/events', opts, :response_block => lambda { |b, r, t| - b.each_line do |line| - block.call(new_event(line, r, t)) + # Disable timeouts by default + opts[:read_timeout] = nil unless opts.key? :read_timeout + + # By default, avoid retrying timeout errors. Set opts[:retry_errors] to override this. + opts[:retry_errors] ||= Excon::DEFAULT_RETRY_ERRORS.reject do |cls| + cls == Excon::Error::Timeout + end + + opts[:response_block] = lambda do |chunk, remaining, total| + chunk.each_line do |event_json| + block.call(new_event(event_json, remaining, total)) end - }) + end + + conn.get('/events', opts.delete(:query), opts) end def since(since, opts = {}, conn = Docker.connection, &block) @@ -43,52 +52,61 @@ def since(since, opts = {}, conn = Docker.connection, &block) def new_event(body, remaining, total) return if body.nil? || body.empty? - json = Docker::Util.parse_json(body) - Docker::Event.new(json) + info = Docker::Util.parse_json(body) + Docker::Event.new(info) end end - attr_accessor :Type, :Action, :time, :timeNano - attr_reader :Actor - # Deprecated interface - attr_accessor :status, :from + attr_reader :info - def initialize(event_attributes = {}) - [:Type, :Action, :Actor, :time, :timeNano, :status, :from].each do |sym| - value = event_attributes[sym] - if value.nil? - value = event_attributes[sym.to_s] - end - send("#{sym}=", value) - end + def initialize(event_data = {}) + @info = event_data.transform_keys { |k| k.downcase.to_sym } + end - if @Actor.nil? - value = event_attributes[:id] - if value.nil? - value = event_attributes['id'] - end - self.Actor = Actor.new(ID: value) - end + def action + info[:action] end + alias_method :Action, :action - def ID - self.actor.ID + def actor + @actor = Actor.new(info[:actor] || {}) if !defined? @actor + @actor end + alias_method :Actor, :actor - def Actor=(actor) - return if actor.nil? - if actor.is_a? Actor - @Actor = actor - else - @Actor = Actor.new(actor) - end + def from + # @deprecated Use `actor.attributes['image']` instead + # Only applicable to container events. See Docker docs for details. + info[:from] || actor.attributes['image'] + end + + def id + # @deprecated Use `actor.id` instead + info[:id] || actor.id + end + + def scope + info[:scope] end - alias_method :type, :Type - alias_method :action, :Action - alias_method :actor, :Actor - alias_method :time_nano, :timeNano - alias_method :id, :ID + def status + # @deprecated Use `action` instead + info[:status] || action + end + + def time + info[:time] + end + + def time_nano + info[:timenano] + end + alias_method :timeNano, :time_nano + + def type + info[:type] + end + alias_method :Type, :type def to_s if type.nil? && action.nil? diff --git a/script/install_podman.sh b/script/install_podman.sh index f5d48789..1e6d8543 100755 --- a/script/install_podman.sh +++ b/script/install_podman.sh @@ -1,12 +1,5 @@ #!/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_${VERSION_ID}/ /" > /etc/apt/sources.list.d/podman.list - apt-get update - apt-get install -y podman diff --git a/spec/docker/event_spec.rb b/spec/docker/event_spec.rb index 0ba6effe..da97cc96 100644 --- a/spec/docker/event_spec.rb +++ b/spec/docker/event_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'spec_helper' +require 'securerandom' SingleCov.covered! uncovered: 5 @@ -18,6 +19,7 @@ 'Type' => 'container', 'from' => 'tianon/true', 'id' => 'bb2c783a32330b726f18d1eb44d80c899ef45771b4f939326e0fefcfc7e05db8', + 'scope' => 'local', 'status' => 'start', 'time' => 1461083270, 'timeNano' => 1461083270652069004 @@ -87,6 +89,43 @@ container.remove end + + context 'with timeouts' do + # @see https://github.com/upserve/docker-api/issues/584 + + it 'does not (seem to) time out by default' do + # @note Excon passes timeout-related arguments directly to IO.select, which in turn makes a system call, + # so it's not possible to mock this (e.g. w/Timecop). + skip_slow_test + expect { Timeout.timeout(65) { stream_events_with_timeout } } + .to raise_error Timeout::Error + end + + it 'times out immediately if read_timeout < Timeout.timeout' do + expect { Timeout.timeout(1) { stream_events_with_timeout(0) } } + .to raise_error Docker::Error::TimeoutError + end + + it 'times out after timeout(1) if read_timeout=2' do + expect { Timeout.timeout(1) { stream_events_with_timeout(2) } } + .to raise_error Timeout::Error + end + + private + + def stream_events_with_timeout(read_timeout = []) + opts = { + # Filter to avoid unexpected Docker events interfering with timeout behavior + query: { filters: { container: [SecureRandom.uuid] }.to_json }, + # Use [] to differentiate between explicit nil and not providing an arg (falling back to the default) + read_timeout: read_timeout, + }.reject { |_, v| v.empty? rescue false } + + Docker::Event.stream(opts) do |event| + raise "Unexpected event interfered with timeout test: #{event}" + end + end + end end describe ".since" do @@ -155,6 +194,7 @@ event.actor.id ).to eq('bb2c783a32330b726f18d1eb44d80c899ef45771b4f939326e0fefcfc7e05db8') expect(event.actor.attributes).to eq('image' => 'tianon/true', 'name' => 'true-dat') + expect(event.scope).to eq('local') expect(event.time).to eq 1461083270 expect(event.time_nano).to eq 1461083270652069004 end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8be80cab..117b91b9 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -24,6 +24,10 @@ def project_dir end module SpecHelpers + def skip_slow_test + skip "Disabled because ENV['RUN_SLOW_TESTS'] not set" unless ENV['RUN_SLOW_TESTS'] + end + def skip_without_auth skip "Disabled because of missing auth" if ENV['DOCKER_API_USER'] == 'debbie_docker' end