# frozen_string_literal: true # 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 # Create a new Connection. This method takes a url (String) and options # (Hash). These are passed to Excon, so any options valid for `Excon.new` # can be passed here. def initialize(url, opts) case when !url.is_a?(String) raise ArgumentError, "Expected a String, got: '#{url}'" when !opts.is_a?(Hash) raise ArgumentError, "Expected a Hash, got: '#{opts}'" else uri = URI.parse(url) if uri.scheme == "unix" @url, @options = 'unix:///', {:socket => uri.path}.merge(opts) elsif uri.scheme =~ /^(https?|tcp)$/ @url, @options = url, opts else @url, @options = "http://#{uri}", opts end end end # The actual client that sends HTTP methods to the Docker server. This value # is not cached, since doing so may cause socket errors after bad requests. def resource Excon.new(url, options) end private :resource # Send a request to the server with the ` def request(*args, &block) retries ||= 0 request = compile_request_params(*args, &block) log_request(request) 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) if Docker.logger Docker.logger.debug( [request[:method], request[:path], request[:query], request[:body]] ) 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 # 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 # 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}" { :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? } end end