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
ACCEPT_ENCODING =
'Accept-Encoding'.freeze
GZIP =
'gzip'.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

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

Initialize api client

Parameters:



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

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 : 10,
    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   : 10,
    ssl_verify_peer: ignore_ssl_errors? ? false : true,
    middlewares:     Excon.defaults[:middlewares] + [Excon::Middleware::Decompress]
  }
  if Kontena.debug?
    require 'kontena/debug_instrumentor'
    excon_opts[:instrumentor] = Kontena::DebugInstrumentor
  end
  excon_opts[:ssl_ca_file] = @options[:ssl_cert_path]
  excon_opts[:ssl_verify_peer_host] = @options[:ssl_subject_cn]

  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_urlObject (readonly)

Returns the value of attribute api_url.



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

def api_url
  @api_url
end

#default_headersObject

Returns the value of attribute default_headers.



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

def default_headers
  @default_headers
end

#hostObject (readonly)

Returns the value of attribute host.



25
26
27
# File 'lib/kontena/client.rb', line 25

def host
  @host
end

#http_clientObject (readonly)

Returns the value of attribute http_client.



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

def http_client
  @http_client
end

#last_responseObject (readonly)

Returns the value of attribute last_response.



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

def last_response
  @last_response
end

#loggerObject (readonly)

Returns the value of attribute logger.



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

def logger
  @logger
end

#optionsObject (readonly)

Returns the value of attribute options.



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

def options
  @options
end

#path_prefixObject

Returns the value of attribute path_prefix.



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

def path_prefix
  @path_prefix
end

#tokenObject (readonly)

Returns the value of attribute token.



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

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
143
# 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'])
  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



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_headerHash

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_idString

OAuth2 client_id from ENV KONTENA_CLIENT_ID or client CLIENT_ID constant

Returns:



182
183
184
# File 'lib/kontena/client.rb', line 182

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

#client_secretString

OAuth2 client_secret from ENV KONTENA_CLIENT_SECRET or client CLIENT_SECRET constant

Returns:



189
190
191
# File 'lib/kontena/client.rb', line 189

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

#debug(&block) ⇒ Object



90
91
92
# File 'lib/kontena/client.rb', line 90

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)


243
244
245
# File 'lib/kontena/client.rb', line 243

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



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

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



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

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)


199
200
201
# File 'lib/kontena/client.rb', line 199

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


253
254
255
# File 'lib/kontena/client.rb', line 253

def get_stream(path, response_block, params = nil, headers = {}, auth = true)
  request(path: path, query: params, headers: headers, response_block: response_block, auth: auth, gzip: false)
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)


232
233
234
# File 'lib/kontena/client.rb', line 232

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)


210
211
212
# File 'lib/kontena/client.rb', line 210

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)


221
222
223
# File 'lib/kontena/client.rb', line 221

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_paramsHash

Build a token refresh request param hash

Returns:

  • (Hash)


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

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

#refresh_tokenBoolean

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?



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

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



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

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

  retried ||= false

  if auth && token_expired?
    raise Excon::Error::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: auth, gzip: gzip)

  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::Error::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::Error::HTTPStatus => error
  if error.response.headers['Content-Encoding'] == 'gzip'
    error.response.body = Zlib::GzipReader.new(StringIO.new(error.response.body)).read
  end

  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_versionString

Return server version from a Kontena master by requesting ‘/’

Returns:



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

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

#token_accountObject

Accessor to token’s account settings



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

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)


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

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