class Kontena::Client
Constants
- ACCEPT
- ACCEPT_ENCODING
- AUTHORIZATION
- CLIENT_ID
- CLIENT_SECRET
- CONTENT_JSON
- CONTENT_TYPE
- CONTENT_URLENCODED
- GZIP
- JSON_REGEX
- X_KONTENA_VERSION
Attributes
Public Class Methods
Initialize api client
@param [String] api_url
@param [Kontena::Cli::Config::Token,Hash] access_token @param [Hash] options
# 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
Public Instance Methods
Requests path supplied as argument and returns true if the request was a success. For checking if the current authentication is valid.
@param [String] token_verify_path a path that requires authentication @return [Boolean]
# 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
Generates a header hash for HTTP basic authentication. Defaults to using client_id
and client_secret
as user/pass
@param [String] username @param [String] password @return [Hash] auth_header_hash
# 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
OAuth2 client_secret
from ENV KONTENA_CLIENT_SECRET or client CLIENT_SECRET
constant
@return [String]
# File lib/kontena/client.rb, line 189 def client_secret ENV['KONTENA_CLIENT_SECRET'] || CLIENT_SECRET end
# File lib/kontena/client.rb, line 90 def debug(&block) logger.debug("CLIENT", &block) end
Delete request
@param [String] path @param [Hash,String] body @param [Hash] params @param [Hash] headers @return [Hash]
# 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
# File lib/kontena/client.rb, line 94 def error(&block) logger.error("CLIENT", &block) end
Calls the code exchange endpoint in token's config to exchange an authorization_code to a access_token
# File lib/kontena/client.rb, line 147 def exchange_code(code) return nil unless token_account return nil unless token_account['token_endpoint'] response = request( http_method: token_account['token_method'].downcase.to_sym, path: token_account['token_endpoint'], headers: { CONTENT_TYPE => token_account['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 request
@param [String] path @param [Hash,NilClass] params @param [Hash] headers @return [Hash]
# 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 request
@param [String] path @param [Lambda] response_block @param [Hash,NilClass] params @param [Hash] headers
# 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 request
@param [String] path @param [Object] obj @param [Hash] params @param [Hash] headers @return [Hash]
# 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 request
@param [String] path @param [Object] obj @param [Hash] params @param [Hash] headers @return [Hash]
# 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 request
@param [String] path @param [Object] obj @param [Hash] params @param [Hash] headers @return [Hash]
# 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
Build a token refresh request param hash
@return [Hash]
# 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
Perform refresh token request to auth provider. Updates the client's Token object and writes changes to configuration.
@param [Boolean] use_basic_auth? When true, use basic auth authentication header @return [Boolean] success?
# 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_account['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_account['token_method'].downcase.to_sym, body: refresh_request_params, headers: { CONTENT_TYPE => token_account['token_post_content_type'] }.merge( token_account['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
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.
@param http_method [Symbol] :get, :post, etc @param path [String] if it starts with / then prefix won't be used. @param body [Hash, String] will be encoded using encode_body
@param query [Hash] url query parameters @param headers [Hash] extra headers for request. @param response_block [Proc] for streaming requests, must respond to call @param expects [Array] raises unless response status code matches this list. @param auth [Boolean] use token authentication default = true @return [Hash, String] response parsed response object
# 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
Return server version from a Kontena
master by requesting '/'
@return [String] version_string
# 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
Accessor to token's account settings
# File lib/kontena/client.rb, line 366 def token_account return {} unless token if token.respond_to?(:account) token.account elsif token.kind_of?(Hash) && token['account'].kind_of?(String) config.find_account(token['account']) else {} end rescue => ex error { "Access token refresh exception" } error { ex } false end
# 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
Private Instance Methods
# File lib/kontena/client.rb, line 506 def add_version_warning(server_version) at_exit do warn Kontena.pastel.yellow("Warning: Server version is #{server_version}. You are using CLI version #{Kontena::Cli::VERSION}.") end end
# File lib/kontena/client.rb, line 496 def check_version_and_warn(server_version) return nil if $VERSION_WARNING_ADDED return nil unless server_version.to_s =~ /^\d+\.\d+\.\d+/ unless server_version[/^(\d+\.\d+)/, 1] == Kontena::Cli::VERSION[/^(\d+\.\d+)/, 1] # Just compare x.y add_version_warning(server_version) $VERSION_WARNING_ADDED = true end end
Dump json
@param [Object] obj @return [String]
# File lib/kontena/client.rb, line 528 def dump_json(obj) JSON.dump(obj) end
Encode body based on content type.
@param [Object] body @param [String] content_type @return [String] encoded_content
# File lib/kontena/client.rb, line 470 def encode_body(body, content_type) if content_type =~ JSON_REGEX # vnd.api+json should pass as json dump_json(body) elsif content_type == CONTENT_URLENCODED && body.kind_of?(Hash) URI.encode_www_form(body) else body end end
@param [Excon::Response] response
# File lib/kontena/client.rb, line 538 def handle_error_response(response) data = parse_response(response) request_path = " (#{response.path})" if data.is_a?(Hash) && data.has_key?('error') && data['error'].is_a?(Hash) raise Kontena::Errors::StandardErrorHash.new(response.status, response.reason_phrase, data['error']) elsif data.is_a?(Hash) && data.has_key?('errors') && data['errors'].is_a?(Array) && data['errors'].all? { |e| e.is_a?(Hash) } error_with_status = data['errors'].find { |error| error.key?('status') } if error_with_status status = error_with_status['status'] else status = response.status end raise Kontena::Errors::StandardErrorHash.new(status, response.reason_phrase, data) elsif data.is_a?(Hash) && data.has_key?('error') raise Kontena::Errors::StandardError.new(response.status, data['error'] + request_path) elsif data.is_a?(String) && !data.empty? raise Kontena::Errors::StandardError.new(response.status, data + request_path) else raise Kontena::Errors::StandardError.new(response.status, response.reason_phrase + request_path) end end
@return [Boolean]
# File lib/kontena/client.rb, line 533 def ignore_ssl_errors? ENV['SSL_IGNORE_ERRORS'] == 'true' || options[:ignore_ssl_errors] end
Convert expires_in into expires_at
@param [Fixnum] seconds_till_expiration @return [Fixnum] expires_at_unix_timestamp
# File lib/kontena/client.rb, line 566 def in_to_at(expires_in) if expires_in.to_i < 1 0 else Time.now.utc.to_i + expires_in.to_i end end
Parse json
@param response [Excon::Response] @return [Hash,Object,NilClass]
# File lib/kontena/client.rb, line 516 def parse_json(response) return nil if response.body.empty? JSON.parse(response.body) rescue => ex raise Kontena::Errors::StandardError.new(520, "Invalid response JSON from server for #{response.path}: #{ex.class.name}: #{ex.message}") end
Parse response. If the respons is JSON, returns a Hash representation. Otherwise returns the raw body.
@param [Excon::Response] @return [Hash,String]
# File lib/kontena/client.rb, line 486 def parse_response(response) check_version_and_warn(response.headers[X_KONTENA_VERSION]) if response.headers[CONTENT_TYPE] =~ JSON_REGEX parse_json(response) else response.body end end
Get prefixed request path unless path starts with /
@param [String] path @return [String]
# File lib/kontena/client.rb, line 445 def path_with_prefix(path) path.to_s.start_with?('/') ? path : "#{path_prefix}#{path}" end
Build request headers. Removes empty headers. @example
request_headers('Authorization' => nil)
@param [Hash] headers @return [Hash]
# File lib/kontena/client.rb, line 457 def request_headers(headers = {}, auth: true, gzip: true) headers = default_headers.merge(headers) headers.merge!(bearer_authorization_header) if auth headers[ACCEPT_ENCODING] = GZIP if gzip headers.reject{|_,v| v.nil? || (v.respond_to?(:empty?) && v.empty?)} end
Returns true if the token object belongs to a master
@return [Boolean]
# File lib/kontena/client.rb, line 436 def token_is_for_master? token_account['name'] == 'master' end