Class: Kontena::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/kontena/client.rb

Direct Known Subclasses

StacksClient

Constant Summary collapse

CLIENT_ID =
ENV['KONTENA_CLIENT_ID']     || '15faec8a7a9b4f1e8b7daebb1307f1d8'.freeze
CLIENT_SECRET =
ENV['KONTENA_CLIENT_SECRET'] || 'fb8942ae00da4c7b8d5a1898effc742f'.freeze
CONTENT_URLENCODED =
'application/x-www-form-urlencoded'.freeze
CONTENT_JSON =
'application/json'.freeze
JSON_REGEX =
/application\/(.+?\+)?json/.freeze
CONTENT_TYPE =
'Content-Type'.freeze
X_KONTENA_VERSION =
'X-Kontena-Version'.freeze
ACCEPT =
'Accept'.freeze
AUTHORIZATION =
'Authorization'.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(api_url, token = nil, options = {}) ⇒ Client

Initialize api client

Parameters:



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
# File 'lib/kontena/client.rb', line 42

def initialize(api_url, token = nil, options = {})
  @api_url, @token, @options = api_url, token, options
  uri = URI.parse(@api_url)
  @host = uri.host

  @logger = Logger.new(ENV["DEBUG"] ? $stderr : $stdout)
  @logger.level = ENV["DEBUG"].nil? ? Logger::INFO : Logger::DEBUG
  @logger.progname = 'CLIENT'

  @options[:default_headers] ||= {}

  excon_opts = {
    omit_default_port: true,
    connect_timeout: ENV["EXCON_CONNECT_TIMEOUT"] ? ENV["EXCON_CONNECT_TIMEOUT"].to_i : 5,
    read_timeout:    ENV["EXCON_READ_TIMEOUT"]    ? ENV["EXCON_READ_TIMEOUT"].to_i    : 30,
    write_timeout:   ENV["EXCON_WRITE_TIMEOUT"]   ? ENV["EXCON_WRITE_TIMEOUT"].to_i   : 5,
    ssl_verify_peer: ignore_ssl_errors? ? false : true
  }
  if ENV["DEBUG"]
    require 'kontena/debug_instrumentor'
    excon_opts[:instrumentor] = Kontena::DebugInstrumentor
  end

  cert_file = File.join(Dir.home, "/.kontena/certs/#{uri.host}.pem")
  if File.exist?(cert_file) && File.readable?(cert_file)
    excon_opts[:ssl_ca_file] = cert_file
    key = OpenSSL::X509::Certificate.new(File.read(cert_file))
    if key.issuer.to_s == "/C=FI/O=Test/OU=Test/CN=Test"
      logger.debug "Key looks like a self-signed cert made by Kontena CLI, setting verify_peer_host to 'Test'"
      excon_opts[:ssl_verify_peer_host] = 'Test'
    end
  end

  logger.debug "Excon opts: #{excon_opts.inspect}"

  @http_client = Excon.new(api_url, excon_opts)

  @default_headers = {
    ACCEPT => CONTENT_JSON,
    CONTENT_TYPE => CONTENT_JSON,
    'User-Agent' => "kontena-cli/#{Kontena::Cli::VERSION}"
  }.merge(options[:default_headers])

  if token
    if token.kind_of?(String)
      @token = { 'access_token' => token }
    else
      @token = token
    end
    @default_headers.merge!('Authorization' => "Bearer #{@token['access_token']}")
  end

  @api_url = api_url
  @path_prefix = options[:prefix] || '/v1/'
end

Instance Attribute Details

#api_url ⇒ Object (readonly)

Returns the value of attribute api_url.



34
35
36
# File 'lib/kontena/client.rb', line 34

def api_url
  @api_url
end

#default_headers ⇒ Object

Returns the value of attribute default_headers.



27
28
29
# File 'lib/kontena/client.rb', line 27

def default_headers
  @default_headers
end

#host ⇒ Object (readonly)

Returns the value of attribute host.



35
36
37
# File 'lib/kontena/client.rb', line 35

def host
  @host
end

#http_client ⇒ Object (readonly)

Returns the value of attribute http_client.



29
30
31
# File 'lib/kontena/client.rb', line 29

def http_client
  @http_client
end

#last_response ⇒ Object (readonly)

Returns the value of attribute last_response.



30
31
32
# File 'lib/kontena/client.rb', line 30

def last_response
  @last_response
end

#logger ⇒ Object (readonly)

Returns the value of attribute logger.



33
34
35
# File 'lib/kontena/client.rb', line 33

def logger
  @logger
end

#options ⇒ Object (readonly)

Returns the value of attribute options.



31
32
33
# File 'lib/kontena/client.rb', line 31

def options
  @options
end

#path_prefix ⇒ Object

Returns the value of attribute path_prefix.



28
29
30
# File 'lib/kontena/client.rb', line 28

def path_prefix
  @path_prefix
end

#token ⇒ Object (readonly)

Returns the value of attribute token.



32
33
34
# File 'lib/kontena/client.rb', line 32

def token
  @token
end

Instance Method Details

#authentication_ok?(token_verify_path) ⇒ Boolean

Requests path supplied as argument and returns true if the request was a success. For checking if the current authentication is valid.

Parameters:

  • token_verify_path (String) —

    a path that requires authentication

Returns:

  • (Boolean)


130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/kontena/client.rb', line 130

def authentication_ok?(token_verify_path)
  return false unless token
  return false unless token['access_token']
  return false unless token_verify_path

  final_path = token_verify_path.gsub(/\:access\_token/, token['access_token'])
  logger.debug "Requesting user info from #{final_path}"
  request(path: final_path)
  true
rescue => ex
  logger.debug "Authentication verification exception: #{ex.class.name} : #{ex.message}\n#{ex.backtrace.join("\n  ")}"
  false
end

#basic_auth_header(user = nil, pass = nil) ⇒ Hash

Generates a header hash for HTTP basic authentication. Defaults to using client_id and client_secret as user/pass

Parameters:

Returns:

  • (Hash) —

    auth_header_hash



104
105
106
107
108
109
110
111
# File 'lib/kontena/client.rb', line 104

def basic_auth_header(user = nil, pass = nil)
  user ||= client_id
  pass ||= client_secret
  {
    AUTHORIZATION =>
      "Basic #{Base64.encode64([user, pass].join(':')).gsub(/[\r\n]/, '')}"
  }
end

#bearer_authorization_header ⇒ Hash

Generates a bearer token authentication header hash if a token object is available. Otherwise returns an empty hash.

Returns:

  • (Hash) —

    authentication_header



117
118
119
120
121
122
123
# File 'lib/kontena/client.rb', line 117

def bearer_authorization_header
  if token && token['access_token']
    {AUTHORIZATION => "Bearer #{token['access_token']}"}
  else
    {}
  end
end

#client_id ⇒ String

OAuth2 client_id from ENV KONTENA_CLIENT_ID or client CLIENT_ID constant

Returns:



180
181
182
# File 'lib/kontena/client.rb', line 180

def client_id
  ENV['KONTENA_CLIENT_ID'] || CLIENT_ID
end

#client_secret ⇒ String

OAuth2 client_secret from ENV KONTENA_CLIENT_SECRET or client CLIENT_SECRET constant

Returns:



187
188
189
# File 'lib/kontena/client.rb', line 187

def client_secret
  ENV['KONTENA_CLIENT_SECRET'] || CLIENT_SECRET
end

#delete(path, body = nil, params = {}, headers = {}, auth = true) ⇒ Hash

Delete request

Parameters:

  • path (String)
  • body (Hash, String) (defaults to: nil)
  • params (Hash) (defaults to: {})
  • headers (Hash) (defaults to: {})

Returns:

  • (Hash)


241
242
243
# File 'lib/kontena/client.rb', line 241

def delete(path, body = nil, params = {}, headers = {}, auth = true)
  request(http_method: :delete, path: path, body: body, query: params, headers: headers, auth: auth)
end

#exchange_code(code) ⇒ Object

Calls the code exchange endpoint in token's config to exchange an authorization_code to a access_token



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/kontena/client.rb', line 146

def exchange_code(code)
  return nil unless 
  return nil unless ['token_endpoint']

  response = request(
    http_method: ['token_method'].downcase.to_sym,
    path: ['token_endpoint'],
    headers: { CONTENT_TYPE => ['token_post_content_type'] },
    body: {
      'grant_type' => 'authorization_code',
      'code' => code,
      'client_id' => Kontena::Client::CLIENT_ID,
      'client_secret' => Kontena::Client::CLIENT_SECRET
    },
    expects: [200,201],
    auth: false
  )
  response['expires_at'] ||= in_to_at(response['expires_in'])
  response
end

#get(path, params = nil, headers = {}, auth = true) ⇒ Hash

Get request

Parameters:

  • path (String)
  • params (Hash, NilClass) (defaults to: nil)
  • headers (Hash) (defaults to: {})

Returns:

  • (Hash)


197
198
199
# File 'lib/kontena/client.rb', line 197

def get(path, params = nil, headers = {}, auth = true)
  request(path: path, query: params, headers: headers, auth: auth)
end

#get_stream(path, response_block, params = nil, headers = {}, auth = true) ⇒ Object

Get stream request

Parameters:

  • path (String)
  • response_block (Lambda)
  • params (Hash, NilClass) (defaults to: nil)
  • headers (Hash) (defaults to: {})


251
252
253
# File 'lib/kontena/client.rb', line 251

def get_stream(path, response_block, params = nil, headers = {}, auth = true)
  request(path: path, query: params, headers: headers, response_block: response_block, auth: auth)
end

#patch(path, obj, params = {}, headers = {}, auth = true) ⇒ Hash

Patch request

Parameters:

  • path (String)
  • obj (Object)
  • params (Hash) (defaults to: {})
  • headers (Hash) (defaults to: {})

Returns:

  • (Hash)


230
231
232
# File 'lib/kontena/client.rb', line 230

def patch(path, obj, params = {}, headers = {}, auth = true)
  request(http_method: :patch, path: path, body: obj, query: params, headers: headers, auth: auth)
end

#post(path, obj, params = {}, headers = {}, auth = true) ⇒ Hash

Post request

Parameters:

  • path (String)
  • obj (Object)
  • params (Hash) (defaults to: {})
  • headers (Hash) (defaults to: {})

Returns:

  • (Hash)


208
209
210
# File 'lib/kontena/client.rb', line 208

def post(path, obj, params = {}, headers = {}, auth = true)
  request(http_method: :post, path: path, body: obj, query: params, headers: headers, auth: auth)
end

#put(path, obj, params = {}, headers = {}, auth = true) ⇒ Hash

Put request

Parameters:

  • path (String)
  • obj (Object)
  • params (Hash) (defaults to: {})
  • headers (Hash) (defaults to: {})

Returns:

  • (Hash)


219
220
221
# File 'lib/kontena/client.rb', line 219

def put(path, obj, params = {}, headers = {}, auth = true)
  request(http_method: :put, path: path, body: obj, query: params, headers: headers, auth: auth)
end

#refresh_request_params ⇒ Hash

Build a token refresh request param hash

Returns:

  • (Hash)


350
351
352
353
354
355
356
357
# File 'lib/kontena/client.rb', line 350

def refresh_request_params
  {
    refresh_token: token['refresh_token'],
    grant_type: 'refresh_token',
    client_id: client_id,
    client_secret: client_secret
  }
end

#refresh_token ⇒ Boolean

Perform refresh token request to auth provider. Updates the client's Token object and writes changes to configuration.

Parameters:

  • use_basic_auth? (Boolean) —

    When true, use basic auth authentication header

Returns:

  • (Boolean) —

    success?



380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
# File 'lib/kontena/client.rb', line 380

def refresh_token
  logger.debug "Performing token refresh"
  return false if token.nil?
  return false if token['refresh_token'].nil?
  uri = URI.parse(['token_endpoint'])
  endpoint_data = { path: uri.path }
  endpoint_data[:host] = uri.host if uri.host
  endpoint_data[:port] = uri.port if uri.port

  logger.debug "Token refresh endpoint: #{endpoint_data.inspect}"

  return false unless endpoint_data[:path]

  response = request(
    {
      http_method: ['token_method'].downcase.to_sym,
      body: refresh_request_params,
      headers: {
        CONTENT_TYPE => ['token_post_content_type']
      }.merge(
        ['code_requires_basic_auth'] ? basic_auth_header : {}
      ),
      expects: [200, 201, 400, 401, 403],
      auth: false
    }.merge(endpoint_data)
  )

  if response && response['access_token']
    logger.debug "Got response to refresh request"
    token['access_token']  = response['access_token']
    token['refresh_token'] = response['refresh_token']
    token['expires_at'] = in_to_at(response['expires_in'])
    token.config.write if token.respond_to?(:config)
    true
  else
    logger.debug "Got null or bad response to refresh request: #{last_response.inspect}"
    false
  end
rescue => ex
  logger.debug "Access token refresh exception: #{ex.class.name} : #{ex.message}\n#{ex.backtrace.join("\n  ")}"
  false
end

#request(http_method: :get, path: '/', body: nil, query: {}, headers: {}, response_block: nil, expects: [200, 201, 204], host: nil, port: nil, auth: true) ⇒ Hash, String

Perform a HTTP request. Will try to refresh the access token and retry if it's expired or if the server responds with HTTP 401.

Automatically parses a JSON response into a hash.

After the request has been performed, the response can be inspected using client.last_response.

Parameters:

  • http_method (Symbol) (defaults to: :get) —

    :get, :post, etc

  • path (String) (defaults to: '/') —

    if it starts with / then prefix won't be used.

  • body (Hash, String) (defaults to: nil) —

    will be encoded using #encode_body

  • query (Hash) (defaults to: {}) —

    url query parameters

  • headers (Hash) (defaults to: {}) —

    extra headers for request.

  • response_block (Proc) (defaults to: nil) —

    for streaming requests, must respond to #call

  • expects (Array) (defaults to: [200, 201, 204]) —

    raises unless response status code matches this list.

  • auth (Boolean) (defaults to: true) —

    use token authentication default = true

Returns:

  • (Hash, String) —

    response parsed response object



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# File 'lib/kontena/client.rb', line 283

def request(http_method: :get, path:'/', body: nil, query: {}, headers: {}, response_block: nil, expects: [200, 201, 204], host: nil, port: nil, auth: true)

  retried ||= false

  if auth && token_expired?
    raise Excon::Errors::Unauthorized, "Token expired or not valid, you need to login again, use: kontena #{token_is_for_master? ? "master" : "cloud"} login"
  end

  request_headers = request_headers(headers, auth)

  if body.nil?
    body_content = ''
    request_headers.delete(CONTENT_TYPE)
  else
    body_content =  encode_body(body, request_headers[CONTENT_TYPE])
    request_headers.merge!('Content-Length' => body_content.bytesize)
  end

  uri = URI.parse(path)
  host_options = {}

  if uri.host
    host_options[:host]   = uri.host
    host_options[:port]   = uri.port
    host_options[:scheme] = uri.scheme
    path                  = uri.request_uri
  else
    host_options[:host] = host if host
    host_options[:port] = port if port
  end

  request_options = {
      method: http_method,
      expects: Array(expects),
      path: path_with_prefix(path),
      headers: request_headers,
      body: body_content,
      query: query
  }.merge(host_options)

  request_options.merge!(response_block: response_block) if response_block

  # Store the response into client.last_response
  @last_response = http_client.request(request_options)

  parse_response(@last_response)
rescue Excon::Errors::Unauthorized
  if token
    logger.debug 'Server reports access token expired'

    if retried || !token || !token['refresh_token']
      raise Kontena::Errors::StandardError.new(401, 'The access token has expired and needs to be refreshed')
    end

    retried = true
    retry if refresh_token
  end
  raise Kontena::Errors::StandardError.new(401, 'Unauthorized')
rescue Excon::Errors::HTTPStatusError => error
  logger.debug "Request #{error.request[:method].upcase} #{error.request[:path]}: #{error.response.status} #{error.response.reason_phrase}: #{error.response.body}"

  handle_error_response(error.response)
end

#server_version ⇒ String

Return server version from a Kontena master by requesting '/'

Returns:



170
171
172
173
174
175
# File 'lib/kontena/client.rb', line 170

def server_version
  request(auth: false, expects: 200)['version']
rescue => ex
  logger.debug "Server version exception: #{ex.class.name} : #{ex.message}\n#{ex.backtrace.join("\n  ")}"
  nil
end

#token_account ⇒ Object

Accessor to token's account settings



360
361
362
363
364
365
366
367
368
369
370
371
372
# File 'lib/kontena/client.rb', line 360

def 
  return {} unless token
  if token.respond_to?(:account)
    token.
  elsif token.kind_of?(Hash) && token['account'].kind_of?(String)
    config.(token['account'])
  else
    {}
  end
rescue => ex
  logger.debug "Access token refresh exception: #{ex.class.name} : #{ex.message}\n#{ex.backtrace.join("\n  ")}"
  false
end

#token_expired? ⇒ Boolean

Returns:

  • (Boolean)


255
256
257
258
259
260
261
262
263
264
# File 'lib/kontena/client.rb', line 255

def token_expired?
  return false unless token
  if token.respond_to?(:expired?)
    token.expired?
  elsif token['expires_at'].to_i > 0
    token['expires_at'].to_i < Time.now.utc.to_i
  else
    false
  end
end