File: connection.rb

package info (click to toggle)
ruby-docker-api 2.2.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 432 kB
  • sloc: ruby: 4,044; sh: 138; makefile: 5
file content (158 lines) | stat: -rw-r--r-- 4,926 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# 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