Skip to content

Commit

Permalink
[WIP] Add httpclient as alternative HTTP library
Browse files Browse the repository at this point in the history
  • Loading branch information
andrzej-stencel committed Oct 30, 2020
1 parent 4e58f80 commit 68bcfa7
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 37 deletions.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,20 @@ We try to support the last 3 minor versions, matching the [official support poli
Kubernetes 1.2 and below have known issues and are unsupported.
Kubernetes 1.3 presumed to still work although nobody is really testing on such old versions...

### Changing the underlying HTTP library

Kubeclient uses [rest_client](https://github.com/rest-client/rest-client) by default to perform HTTP requests. You can change this by specifying the `http_client_type` option when initializing `Kubeclient::Client`. Currently the only other option is [httpclient](https://github.com/nahi/httpclient).

To use a different HTTP library, specify the `http_client_type` option when initializing `Kubeclient::Client`:

```ruby
client = Kubeclient::Client.new(
'https://localhost:8443/api/', "v1", http_client_type: 'httpclient'
)
```

Using a different underlying HTTP library might have various advantages. For example, the `httpclient` gem is able to re-use HTTP connections between requests, as opposed to `rest_client`, which closes the HTTP connection after each request (see <https://github.com/rest-client/rest-client/issues/453>).

## Supported actions & examples:

Summary of main CRUD actions:
Expand Down Expand Up @@ -548,7 +562,7 @@ Other formats are:
- `:ros` (default) for `RecursiveOpenStruct`
- `:parsed` for `JSON.parse`
- `:parsed_symbolized` for `JSON.parse(..., symbolize_names: true)`
- a class of your choice (this will instantiate a new instance of that class with the raw value of the response body)
- a class of your choice (this will instantiate a new instance of that class with the raw value of the response body)

### Watch — Receive entities updates

Expand Down
1 change: 1 addition & 0 deletions kubeclient.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'googleauth', '~> 0.5.1'
spec.add_development_dependency('mocha', '~> 1.5')
spec.add_development_dependency 'openid_connect', '~> 1.1'
spec.add_development_dependency 'httpclient', '~> 2.0'

spec.add_dependency 'jsonpath', '~> 1.0'
spec.add_dependency 'rest-client', '~> 2.0'
Expand Down
2 changes: 2 additions & 0 deletions lib/kubeclient.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
require 'kubeclient/exec_credentials'
require 'kubeclient/gcp_auth_provider'
require 'kubeclient/http_error'
require 'kubeclient/httpclient_wrapper'
require 'kubeclient/missing_kind_compatibility'
require 'kubeclient/oidc_auth_provider'
require 'kubeclient/resource'
require 'kubeclient/resource_not_found_error'
require 'kubeclient/rest_client_wrapper'
require 'kubeclient/version'
require 'kubeclient/watch_stream'

Expand Down
125 changes: 89 additions & 36 deletions lib/kubeclient/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ module ClientMixin
DEFAULT_HTTP_PROXY_URI = nil
DEFAULT_HTTP_MAX_REDIRECTS = 10

DEFAULT_HTTP_CLIENT_TYPE = 'rest_client'.freeze

SEARCH_ARGUMENTS = {
'labelSelector' => :label_selector,
'fieldSelector' => :field_selector,
Expand Down Expand Up @@ -68,7 +70,8 @@ def initialize_client(
timeouts: DEFAULT_TIMEOUTS,
http_proxy_uri: DEFAULT_HTTP_PROXY_URI,
http_max_redirects: DEFAULT_HTTP_MAX_REDIRECTS,
as: :ros
as: :ros,
http_client_type: DEFAULT_HTTP_CLIENT_TYPE
)
validate_auth_options(auth_options)
handle_uri(uri, path)
Expand All @@ -86,6 +89,7 @@ def initialize_client(
@http_proxy_uri = http_proxy_uri ? http_proxy_uri.to_s : nil
@http_max_redirects = http_max_redirects
@as = as
@http_client_type = http_client_type

if auth_options[:bearer_token]
bearer_token(@auth_options[:bearer_token])
Expand Down Expand Up @@ -282,6 +286,7 @@ def self.underscore_entity(entity_name)
.downcase
end

# *DEPRECATED:* Use +create_http_client+ instead.
def create_rest_client(path = nil)
path ||= @api_endpoint.path
options = {
Expand All @@ -300,9 +305,44 @@ def create_rest_client(path = nil)
RestClient::Resource.new(@api_endpoint.merge(path).to_s, options)
end

# *DEPRECATED:* Use +http_client+ instead.
def rest_client
@rest_client ||= begin
create_rest_client("#{@api_endpoint.path}/#{@api_version}")
http_client
end

# Returns the instance of the HTTP library used for making HTTP requests.
# By default this will be an instance of RestClient from `rest_client` gem.
# You can select a different HTTP library when creating and instance of +Kubeclient::Client+,
# see the +http_client_type+ option on +Kubeclient::Client.new+.
def http_client
http.client
end

# Creates and returns a new instance of a class that implements the interface
# defined by +HTTPWrapper+ class.
def create_http_client(url = nil, options = nil, http_client_type = nil)
url = "#{@api_endpoint}/#{@api_version}" if url.nil?
if options.nil?
options = {
ssl_ca_file: @ssl_options[:ca_file],
ssl_cert_store: @ssl_options[:cert_store],
verify_ssl: @ssl_options[:verify_ssl],
ssl_client_cert: @ssl_options[:client_cert],
ssl_client_key: @ssl_options[:client_key],
proxy: @http_proxy_uri,
max_redirects: @http_max_redirects,
user: @auth_options[:username],
password: @auth_options[:password],
open_timeout: @timeouts[:open],
read_timeout: @timeouts[:read]
}
end
http_client_type = @http_client_type if http_client_type.nil?

if http_client_type == 'httpclient'
HTTPClientWrapper.new(url, options)
else
RestClientWrapper.new(url, options)
end
end

Expand Down Expand Up @@ -352,8 +392,7 @@ def get_entities(entity_type, resource_name, options = {})

ns_prefix = build_namespace_prefix(options[:namespace])
response = handle_exception do
rest_client[ns_prefix + resource_name]
.get({ 'params' => params }.merge(@headers))
http.get(ns_prefix + resource_name, params: params, headers: @headers)
end
format_response(options[:as] || @as, response.body, entity_type)
end
Expand All @@ -365,8 +404,7 @@ def get_entities(entity_type, resource_name, options = {})
def get_entity(resource_name, name, namespace = nil, options = {})
ns_prefix = build_namespace_prefix(namespace)
response = handle_exception do
rest_client[ns_prefix + resource_name + "/#{name}"]
.get(@headers)
http.get(ns_prefix + resource_name + "/#{name}", headers: @headers)
end
format_response(options[:as] || @as, response.body)
end
Expand All @@ -377,14 +415,10 @@ def delete_entity(resource_name, name, namespace = nil, delete_options: {})
ns_prefix = build_namespace_prefix(namespace)
payload = delete_options_hash.to_json unless delete_options_hash.empty?
response = handle_exception do
rs = rest_client[ns_prefix + resource_name + "/#{name}"]
RestClient::Request.execute(
rs.options.merge(
method: :delete,
url: rs.url,
headers: { 'Content-Type' => 'application/json' }.merge(@headers),
payload: payload
)
http.delete(
ns_prefix + resource_name + "/#{name}",
body: payload,
headers: { 'Content-Type' => 'application/json' }.merge(@headers)
)
end
format_response(@as, response.body)
Expand All @@ -403,8 +437,11 @@ def create_entity(entity_type, resource_name, entity_config)
hash[:kind] = entity_type
hash[:apiVersion] = @api_group + @api_version
response = handle_exception do
rest_client[ns_prefix + resource_name]
.post(hash.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
http.post(
ns_prefix + resource_name,
body: hash.to_json,
headers: { 'Content-Type' => 'application/json' }.merge(@headers)
)
end
format_response(@as, response.body)
end
Expand All @@ -413,20 +450,23 @@ def update_entity(resource_name, entity_config)
name = entity_config[:metadata][:name]
ns_prefix = build_namespace_prefix(entity_config[:metadata][:namespace])
response = handle_exception do
rest_client[ns_prefix + resource_name + "/#{name}"]
.put(entity_config.to_h.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
http.put(
ns_prefix + resource_name + "/#{name}",
body: entity_config.to_h.to_json,
headers: { 'Content-Type' => 'application/json' }.merge(@headers)
)
end
format_response(@as, response.body)
end

def patch_entity(resource_name, name, patch, strategy, namespace)
ns_prefix = build_namespace_prefix(namespace)
response = handle_exception do
rest_client[ns_prefix + resource_name + "/#{name}"]
.patch(
patch.to_json,
{ 'Content-Type' => "application/#{strategy}+json" }.merge(@headers)
)
http.patch(
ns_prefix + resource_name + "/#{name}",
body: patch.to_json,
headers: { 'Content-Type' => "application/#{strategy}+json" }.merge(@headers)
)
end
format_response(@as, response.body)
end
Expand All @@ -435,11 +475,11 @@ def apply_entity(resource_name, resource, field_manager:, force: true)
name = "#{resource[:metadata][:name]}?fieldManager=#{field_manager}&force=#{force}"
ns_prefix = build_namespace_prefix(resource[:metadata][:namespace])
response = handle_exception do
rest_client[ns_prefix + resource_name + "/#{name}"]
.patch(
resource.to_json,
{ 'Content-Type' => 'application/apply-patch+yaml' }.merge(@headers)
)
http.patch(
ns_prefix + resource_name + "/#{name}",
body: resource.to_json,
headers: { 'Content-Type' => 'application/apply-patch+yaml' }.merge(@headers)
)
end
format_response(@as, response.body)
end
Expand Down Expand Up @@ -471,8 +511,11 @@ def get_pod_log(pod_name, namespace,

ns = build_namespace_prefix(namespace)
handle_exception do
rest_client[ns + "pods/#{pod_name}/log"]
.get({ 'params' => params }.merge(@headers))
http.get(
ns + "pods/#{pod_name}/log",
params: params,
headers: @headers
)
end
end

Expand Down Expand Up @@ -503,14 +546,17 @@ def proxy_url(kind, name, port, namespace = '')
@entities[kind.to_s].resource_name
end
ns_prefix = build_namespace_prefix(namespace)
rest_client["#{ns_prefix}#{entity_name_plural}/#{name}:#{port}/proxy"].url
"#{@api_endpoint}/#{@api_version}/#{ns_prefix}#{entity_name_plural}/#{name}:#{port}/proxy"
end

def process_template(template)
ns_prefix = build_namespace_prefix(template[:metadata][:namespace])
response = handle_exception do
rest_client[ns_prefix + 'processedtemplates']
.post(template.to_h.to_json, { 'Content-Type' => 'application/json' }.merge(@headers))
http.post(
ns_prefix + 'processedtemplates',
body: template.to_h.to_json,
headers: { 'Content-Type' => 'application/json' }.merge(@headers)
)
end
JSON.parse(response)
end
Expand All @@ -523,12 +569,19 @@ def api_valid?
end

def api
response = handle_exception { create_rest_client.get(@headers) }
response = handle_exception { create_http_client(@api_endpoint.to_s).get(headers: @headers) }
JSON.parse(response)
end

private

# Returns an object that implements the interface defined by the +HTTPWrapper+ class.
def http
@http ||= begin
create_http_client
end
end

IRREGULAR_NAMES = {
# In a few cases, the given kind itself is still plural.
# https://github.com/kubernetes/kubernetes/issues/8115
Expand Down Expand Up @@ -599,7 +652,7 @@ def load_entities
end

def fetch_entities
JSON.parse(handle_exception { rest_client.get(@headers) })
JSON.parse(handle_exception { http.get(headers: @headers) })
end

def bearer_token(bearer_token)
Expand Down
30 changes: 30 additions & 0 deletions lib/kubeclient/http_wrapper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module Kubeclient
# Defines the common API for libraries to be used by Kubeclient for making HTTP requests.
# To create a wrapper for a library, create a new class that inherits from this class
# and override the +request+ method. See +RestClientWrapper+ or +HTTPClientWrapper+ for examples.
class HTTPWrapper
def delete(path = nil, **options)
request(:delete, path, **options)
end

def get(path = nil, **options)
request(:get, path, **options)
end

def patch(path = nil, **options)
request(:patch, path, **options)
end

def post(path = nil, **options)
request(:post, path, **options)
end

def put(path = nil, **options)
request(:put, path, **options)
end

def request
raise 'Must implement the `request` method.'
end
end
end
23 changes: 23 additions & 0 deletions lib/kubeclient/httpclient_wrapper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
require_relative 'http_wrapper'

module Kubeclient
# Wraps the API of +httpclient+ gem to be used by Kubeclient for making HTTP requests.
class HTTPClientWrapper < HTTPWrapper
def initialize(url, options)
@url = url
@options = options
@client = HTTPClient.new
end

attr_reader :client

def request(method, path = nil, **options)
uri = [@url, path].compact.join('/')
query = options[:params] if options.key?(:params)
body = options[:body] if options.key?(:body)
headers = options[:headers] if options.key(:headers)

@client.request(method, uri, query, body, headers)
end
end
end
35 changes: 35 additions & 0 deletions lib/kubeclient/rest_client_wrapper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
require_relative 'http_wrapper'

module Kubeclient
# Wraps the API of +rest_client+ gem to be used by Kubeclient for making HTTP requests.
class RestClientWrapper < HTTPWrapper
def initialize(url, options)
@client = RestClient::Resource.new(url, options)
end

attr_reader :client

def request(method, path = nil, **options)
url = path.nil? ? @client.url : @client[path].url
headers_with_params = create_headers_with_params(options[:headers], options[:params])
payload = options[:body]

execute_options = @client.options.merge(
method: method,
url: url,
headers: headers_with_params,
payload: payload
)
RestClient::Request.execute(execute_options)
end

private

# In RestClient, you pass params hash inside the headers hash, in :params key.
def create_headers_with_params(headers, params)
headers_with_params = {}.merge(headers || {})
headers_with_params[:params] = params if params
headers_with_params
end
end
end
Loading

0 comments on commit 68bcfa7

Please sign in to comment.