app/models/service_discovery/cluster_client.rb
# frozen_string_literal: true
require 'kubeclient'
module ServiceDiscovery
class ClusterClient
class ClusterClientError < StandardError; end
class ResourceNotFound < ClusterClientError
def initialize(resource_type, name, namespace, labels = {})
resource_name = namespace.present? ? "#{namespace}/#{name}" : name
error_message = "Resource #{resource_name} of kind #{resource_type.to_s.camelize} not found"
error_message += " with labels #{labels}" if labels.present?
super(error_message)
end
end
def initialize(opts = {})
service_discovery_configs = ThreeScale.config.service_discovery.to_h
options = opts.reverse_merge(service_discovery_configs.slice(:server_scheme, :server_host, :server_port, :bearer_token))
@clients = [
self.class.build_client(options),
self.class.build_client_for_projects(options)
]
end
attr_reader :clients
def method_missing(method_sym, *args, &block)
client = client_that_responds_to(method_sym)
return client.public_send(method_sym, *args, &block) if client
super
end
def respond_to_missing?(method_sym, include_private = false)
clients_respond_to?(method_sym) || super
end
def namespaces
# 'view' cluster-role permission is required
get_namespaces.map { |resource| ClusterNamespace.new(resource, self) }
end
def projects
get_projects.map { |resource| ClusterProject.new(resource, self) }
end
def services(namespace: nil, labels: {})
within_namespace(namespace, with_labels: labels) do |search_criteria|
get_services(search_criteria).map { |resource| ClusterService.new(resource, self) }
end
end
def find_namespace_by(name:)
find_resource(:namespace, name)
end
def find_project_by(name:)
find_resource(:project, name)
end
def find_service_by(name:, namespace:)
find_resource(:service, name, namespace)
end
def discoverable_services(namespace: nil, labels: {})
services(namespace: namespace, labels: labels.merge(ClusterService.discovery_label_selector)).select(&:discoverable?)
end
def find_discoverable_service_by(name:, namespace:)
cluster_service = find_service_by(namespace: namespace, name: name)
raise_not_found('Service', name, namespace) unless cluster_service.discoverable?
cluster_service
end
def projects_with_discoverables
# Less efficient than fetching all discoverable services (unscoped) and then selecting the unique namespaces,
# but it allows using an user's (or user-level service account's) token to fetch the list of projects
projects.select { |project| discoverable_services(namespace: project.namespace).any? }
end
DEFAULT_CLUSTER_SCHEME = 'https'
DEFAULT_CLUSTER_HOST = 'kubernetes.default.svc.cluster.local'
DEFAULT_CLUSTER_PORT = 443
def self.build_api_endpoint(options = {})
server_scheme = options[:server_scheme] || DEFAULT_CLUSTER_SCHEME
server_host = options[:server_host] || DEFAULT_CLUSTER_HOST
server_port = options[:server_port] || DEFAULT_CLUSTER_PORT
api_path = options[:api_path].presence
"#{server_scheme}://#{server_host}:#{server_port}/#{api_path}"
end
def self.build_client_options(options = {})
[
build_api_endpoint(options),
'v1',
{
ssl_options: { verify_ssl: OpenSSL::SSL::VERIFY_NONE },
auth_options: { bearer_token: options[:bearer_token] }
}
]
end
def self.build_client(options = {})
uri, version, client_options = build_client_options(options.reverse_merge(api_path: 'api'))
Kubeclient::Client.new(uri, version, **client_options)
end
def self.build_client_for_projects(options = {})
api_path = options.fetch(:api_path_for_projects, 'apis/project.openshift.io')
build_client(options.merge(api_path: api_path))
end
protected
def within_namespace(namespace, with_labels: {})
search_criteria = {}
search_criteria[:namespace] = namespace.presence
if with_labels.present?
label_selector = with_labels.map { |label, value| "#{label}=#{value}" }.join(',')
search_criteria[:label_selector] = label_selector
end
yield search_criteria
end
def find_resource(type, name, namespace = nil)
begin
args = [name, namespace.presence].compact
resource_data = public_send("get_#{type.to_s.downcase}", *args)
raise_not_found(type, name, namespace) unless resource_data
rescue KubeException => exception
handle_kube_exception(exception, type, name, namespace)
end
klass = "ServiceDiscovery::Cluster#{type.to_s.camelize}".constantize
klass.new(resource_data, self)
end
def handle_kube_exception(exception, *args)
case exception.error_code
when 404
raise_not_found(*args)
else
raise ClusterClientError, exception.to_s
end
end
def raise_not_found(resource_type, resource_name, namespace = nil, fields = {})
raise ResourceNotFound.new(resource_type, resource_name, namespace, fields)
end
def client_that_responds_to(method_sym)
clients.each do |client|
client_responds_to_method = client.respond_to?(method_sym)
return client if client_responds_to_method
end
nil
end
def clients_respond_to?(method_sym)
client_that_responds_to(method_sym).present?
end
end
end