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:



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

def initialize(api_url, token = nil, options = {})
  require 'json'
  require 'excon'
  require 'uri'
  require 'base64'
  require 'socket'
  require 'openssl'
  require 'uri'
  require 'time'
  require 'kontena/errors'
  require 'kontena/cli/version'
  require 'kontena/cli/config'

  @api_url, @token, @options = api_url, token, options
  uri = URI.parse(@api_url)
  @host = uri.host

  @logger = Kontena.logger

  @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"
      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

  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
  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.



22
23
24
# File 'lib/kontena/client.rb', line 22

def api_url
  @api_url
end

#default_headers ⇒ Object

Returns the value of attribute default_headers.



15
16
17
# File 'lib/kontena/client.rb', line 15

def default_headers
  @default_headers
end

#host ⇒ Object (readonly)

Returns the value of attribute host.



23
24
25
# File 'lib/kontena/client.rb', line 23

def host
  @host
end

#http_client ⇒ Object (readonly)

Returns the value of attribute http_client.



17
18
19
# File 'lib/kontena/client.rb', line 17

def http_client
  @http_client
end

#last_response ⇒ Object (readonly)

Returns the value of attribute last_response.



18
19
20
# File 'lib/kontena/client.rb', line 18

def last_response
  @last_response
end

#logger ⇒ Object (readonly)

Returns the value of attribute logger.



21
22
23
# File 'lib/kontena/client.rb', line 21

def logger
  @logger
end

#options ⇒ Object (readonly)

Returns the value of attribute options.



19
20
21
# File 'lib/kontena/client.rb', line 19

def options
  @options
end

#path_prefix ⇒ Object

Returns the value of attribute path_prefix.



16
17
18
# File 'lib/kontena/client.rb', line 16

def path_prefix
  @path_prefix
end

#token ⇒ Object (readonly)

Returns the value of attribute token.



20
21
22
# File 'lib/kontena/client.rb', line 20

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)


135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/kontena/client.rb', line 135

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'])
  debug { "Requesting user info from #{final_path}" }
  request(path: final_path)
  true
rescue => ex
  error { "Authentication verification exception" }
  error { ex }
  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



109
110
111
112
113
114
115
116
# File 'lib/kontena/client.rb', line 109

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



122
123
124
125
126
127
128
# File 'lib/kontena/client.rb', line 122

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:



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

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:



194
195
196
# File 'lib/kontena/client.rb', line 194

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

#debug(&block) ⇒ Object



95
96
97
# File 'lib/kontena/client.rb', line 95

def debug(&block)
  logger.debug("CLIENT", &block)
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)


248
249
250
# File 'lib/kontena/client.rb', line 248

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

#error(&block) ⇒ Object



99
100
101
# File 'lib/kontena/client.rb', line 99

def error(&block)
  logger.error("CLIENT", &block)
end

#exchange_code(code) ⇒ Object

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



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/kontena/client.rb', line 152

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)


204
205
206
# File 'lib/kontena/client.rb', line 204

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: {})


258
259
260
# File 'lib/kontena/client.rb', line 258

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)


237
238
239
# File 'lib/kontena/client.rb', line 237

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)


215
216
217
# File 'lib/kontena/client.rb', line 215

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)


226
227
228
# File 'lib/kontena/client.rb', line 226

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)


357
358
359
360
361
362
363
364
# File 'lib/kontena/client.rb', line 357

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?



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
422
423
424
425
426
427
428
429
430
# File 'lib/kontena/client.rb', line 388

def refresh_token
  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

  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']
    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
    debug { "Got null or bad response to refresh request: #{last_response.inspect}" }
    false
  end
rescue => ex
  error { "Access token refresh exception" }
  error { ex }
  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



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
346
347
348
349
350
351
352
# File 'lib/kontena/client.rb', line 290

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
    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
  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:



176
177
178
179
180
181
182
# File 'lib/kontena/client.rb', line 176

def server_version
  request(auth: false, expects: 200)['version']
rescue => ex
  error { "Server version exception" }
  error { ex }
  nil
end

#token_account ⇒ Object

Accessor to token's account settings



367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/kontena/client.rb', line 367

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
  error { "Access token refresh exception" }
  error { ex }
  false
end

#token_expired? ⇒ Boolean

Returns:

  • (Boolean)


262
263
264
265
266
267
268
269
270
271
# File 'lib/kontena/client.rb', line 262

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