From 5176603c3b7b45477cb160bbe3a1db3e52547069 Mon Sep 17 00:00:00 2001 From: Kimmo Lehto Date: Fri, 10 May 2019 15:14:19 +0300 Subject: [PATCH 1/3] Add !-variants of request methods and improve yardocs --- k8s-client.gemspec | 1 + lib/k8s/api/metav1.rb | 3 +++ lib/k8s/api_client.rb | 8 ++++-- lib/k8s/client.rb | 23 ++++++++++------ lib/k8s/config.rb | 2 +- lib/k8s/resource_client.rb | 11 +++++++- lib/k8s/stack.rb | 8 ++++-- lib/k8s/transport.rb | 23 +++++++++++----- lib/k8s/util.rb | 17 ++++++++++++ spec/k8s/resource_client_spec.rb | 45 +++++++++++++++++++++----------- spec/k8s/transport_spec.rb | 10 +++++++ 11 files changed, 115 insertions(+), 36 deletions(-) diff --git a/k8s-client.gemspec b/k8s-client.gemspec index 69f712d..d38fe57 100644 --- a/k8s-client.gemspec +++ b/k8s-client.gemspec @@ -37,4 +37,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency "rspec", "~> 3.7" spec.add_development_dependency "webmock", "~> 3.4.2" spec.add_development_dependency "rubocop", "~> 0.59" + spec.add_development_dependency "yard", "~> 0.9" end diff --git a/lib/k8s/api/metav1.rb b/lib/k8s/api/metav1.rb index 2adc489..a917390 100644 --- a/lib/k8s/api/metav1.rb +++ b/lib/k8s/api/metav1.rb @@ -10,6 +10,9 @@ module API module MetaV1 # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#TypeMeta class Resource < Struct + # @!macro [attach] attribute + # @!attribute [r] $1 + # XXX: making these optional seems dangerous, but some APIs (GET /api/v1) are missing these attribute :kind, Types::Strict::String.optional.default(nil) attribute :apiVersion, Types::Strict::String.optional.default(nil) diff --git a/lib/k8s/api_client.rb b/lib/k8s/api_client.rb index 76db5e4..b2d53d7 100644 --- a/lib/k8s/api_client.rb +++ b/lib/k8s/api_client.rb @@ -5,6 +5,8 @@ module K8s # # Offers access to {ResourceClient} instances for the APIResource types defined in this apigroup/version class APIClient + extend K8s::Util::ExceptionlessBangMethod + # @param api_version [String] either core version (v1) or apigroup/apiversion (apps/v1) # @return [String] def self.path(api_version) @@ -36,7 +38,6 @@ def api_resources? !!@api_resources end - # @param api_resources [Array] attr_writer :api_resources # Force-update APIResources @@ -64,6 +65,7 @@ def find_api_resource(resource_name) found_resource end + exceptionless_bang_method :find_api_resource # @param resource_name [String] # @param namespace [String, nil] @@ -72,6 +74,7 @@ def find_api_resource(resource_name) def resource(resource_name, namespace: nil) ResourceClient.new(@transport, self, find_api_resource(resource_name), namespace: namespace) end + exceptionless_bang_method :resource # @param resource [K8s::Resource] # @param namespace [String, nil] default if resource is missing namespace @@ -88,6 +91,7 @@ def client_for_resource(resource, namespace: nil) ResourceClient.new(@transport, self, found_resource, namespace: resource.metadata.namespace || namespace) end + exceptionless_bang_method :client_for_resource # TODO: skip non-namespaced resources if namespace is given, or ignore namespace? # @@ -105,7 +109,7 @@ def resources(namespace: nil) # Returns flattened array with mixed resource kinds. # # @param resources [Array] default is all listable resources for api - # @param options @see [K8s::ResourceClient#list] + # @param (see K8s::ResourceClient#list) # @return [Array] def list_resources(resources = nil, **options) resources ||= self.resources.select(&:list?) diff --git a/lib/k8s/client.rb b/lib/k8s/client.rb index 1f810ce..366c83a 100644 --- a/lib/k8s/client.rb +++ b/lib/k8s/client.rb @@ -21,7 +21,8 @@ module K8s # @param server [String] http/s URL - # @param options [Hash] @see Transport.new + # @param options [Hash] + # @param (see Transport#initialize) # @return [K8s::Client] def self.client(server, **options) Client.new(Transport.new(server, **options)) @@ -31,9 +32,11 @@ def self.client(server, **options) # Uses a {Transport} instance to talk to the kube API. # Offers access to {APIClient} and {ResourceClient} instances. class Client + extend K8s::Util::ExceptionlessBangMethod + # @param config [Phraos::Kube::Config] - # @param namespace [String] @see #initialize - # @param options [Hash] @see Transport.config + # @param namespace [String] default namespace for all operations + # @param (see K8s::Transport.config) # @return [K8s::Client] def self.config(config, namespace: nil, **options) new( @@ -43,10 +46,9 @@ def self.config(config, namespace: nil, **options) end # An K8s::Client instance from in-cluster config within a kube pod, using the kubernetes service envs and serviceaccount secrets - # @see K8s::Transport#in_cluster_config # # @param namespace [String] default namespace for all operations - # @param options [Hash] options passed to transport, @see Transport#in_cluster_config + # @param (see Transport.in_cluster_config) # @return [K8s::Client] # @raise [K8s::Error::Config,Errno::ENOENT,Errno::EACCES] def self.in_cluster_config(namespace: nil, **options) @@ -95,6 +97,7 @@ def self.autoconfig(namespace: nil, **options) include MonitorMixin + # @return [K8s::Transport] attr_reader :transport # @param transport [K8s::Transport] @@ -187,7 +190,7 @@ def resources(namespace: nil) # Returns flattened array with mixed resource kinds. # # @param resources [Array] default is all listable resources for api - # @param options @see K8s::ResourceClient#list + # @param (see ResourceClient.list) # @return [Array] def list_resources(resources = nil, **options) cached_clients = @api_clients.size.positive? @@ -218,12 +221,14 @@ def client_for_resource(resource, namespace: nil) def create_resource(resource) client_for_resource(resource).create_resource(resource) end + exceptionless_bang_method :create_resource # @param resource [K8s::Resource] # @return [K8s::Resource] def get_resource(resource) client_for_resource(resource).get_resource(resource) end + exceptionless_bang_method :get_resource # Returns nils for any resources that do not exist. # This includes custom resources that were not yet defined. @@ -258,14 +263,15 @@ def get_resources(resources) def update_resource(resource) client_for_resource(resource).update_resource(resource) end + exceptionless_bang_method :update_resource # @param resource [K8s::Resource] - # @param options [Hash] - # @see ResourceClient#delete for options + # @param (see ResourceClient.delete) # @return [K8s::Resource] def delete_resource(resource, **options) client_for_resource(resource).delete_resource(resource, **options) end + exceptionless_bang_method :delete_resource # @param resource [K8s::Resource] # @param attrs [Hash] @@ -273,5 +279,6 @@ def delete_resource(resource, **options) def patch_resource(resource, attrs) client_for_resource(resource).json_patch(resource.metadata.name, attrs) end + exceptionless_bang_method :patch_resource end end diff --git a/lib/k8s/config.rb b/lib/k8s/config.rb index 9ae8b76..922af56 100644 --- a/lib/k8s/config.rb +++ b/lib/k8s/config.rb @@ -133,7 +133,7 @@ def self.from_kubeconfig_env(kubeconfig = nil) # # @param server [String] kubernetes server address # @param ca [String] server certificate authority data (base64 encoded) - # @param token [String] access token + # @param auth_token [String] access token # @param cluster_name [String] cluster name # @param user [String] user name # @param context [String] context name diff --git a/lib/k8s/resource_client.rb b/lib/k8s/resource_client.rb index 990d981..e6aedb7 100644 --- a/lib/k8s/resource_client.rb +++ b/lib/k8s/resource_client.rb @@ -35,6 +35,7 @@ def make_query(options) include Utils extend Utils + extend K8s::Util::ExceptionlessBangMethod # Pipeline list requests for multiple resource types. # @@ -152,6 +153,7 @@ def create_resource(resource) response_class: @resource_class ) end + exceptionless_bang_method :create_resource # @return [Bool] def get? @@ -168,6 +170,7 @@ def get(name, namespace: @namespace) response_class: @resource_class ) end + exceptionless_bang_method :get # @param resource [resource_class] # @return [Object] instance of resource_class @@ -178,6 +181,7 @@ def get_resource(resource) response_class: @resource_class ) end + exceptionless_bang_method :get_resource # @return [Bool] def list? @@ -264,6 +268,7 @@ def update_resource(resource) response_class: @resource_class ) end + exceptionless_bang_method :update_resource # @return [Boolean] def patch? @@ -284,6 +289,7 @@ def merge_patch(name, obj, namespace: @namespace, strategic_merge: true) response_class: @resource_class ) end + exceptionless_bang_method :merge_patch # @param name [String] # @param ops [Hash] json-patch operations @@ -298,6 +304,7 @@ def json_patch(name, ops, namespace: @namespace) response_class: @resource_class ) end + exceptionless_bang_method :json_patch # @return [Boolean] def delete? @@ -318,6 +325,7 @@ def delete(name, namespace: @namespace, propagationPolicy: nil) response_class: @resource_class # XXX: documented as returning Status ) end + exceptionless_bang_method :delete # @param namespace [String, nil] # @param labelSelector [nil, String, Hash{String => String}] @@ -340,10 +348,11 @@ def delete_collection(namespace: @namespace, labelSelector: nil, fieldSelector: # @param resource [resource_class] with metadata # @param options [Hash] - # @see #delete for possible options + # @param (see #delete) # @return [K8s::API::MetaV1::Status] def delete_resource(resource, **options) delete(resource.metadata.name, namespace: resource.metadata.namespace, **options) end + exceptionless_bang_method :delete_resource end end diff --git a/lib/k8s/stack.rb b/lib/k8s/stack.rb index 9fa1432..2bb807a 100644 --- a/lib/k8s/stack.rb +++ b/lib/k8s/stack.rb @@ -50,14 +50,18 @@ def self.delete(name, client, **options) new(name, **options).delete(client) end - attr_reader :name, :resources + # @return [String] + attr_reader :name + + # @return [Array] + attr_reader :resources # @param name [String] # @param resources [Array] # @param debug [Boolean] # @param label [String] # @param checksum_annotation [String] - # @param last_config_annotation [String] + # @param last_configuration_annotation [String] def initialize(name, resources = [], debug: false, label: self.class::LABEL, checksum_annotation: self.class::CHECKSUM_ANNOTATION, last_configuration_annotation: self.class::LAST_CONFIG_ANNOTATION) @name = name @resources = resources diff --git a/lib/k8s/transport.rb b/lib/k8s/transport.rb index 35f050a..828f308 100644 --- a/lib/k8s/transport.rb +++ b/lib/k8s/transport.rb @@ -8,6 +8,7 @@ module K8s # Excon-based HTTP transport handling request/response body JSON encoding class Transport include Logging + extend K8s::Util::ExceptionlessBangMethod quiet! # do not log warnings by default @@ -29,7 +30,7 @@ class Transport # # @param config [K8s::Config] # @param server [String] override cluster.server from config - # @param overrides @see #initialize + # @param (see #initialize) # @return [K8s::Transport] def self.config(config, server: nil, **overrides) options = {} @@ -142,13 +143,20 @@ def self.in_cluster_config(**options) ) end - attr_reader :server, :options, :path_prefix + # @return [String] server URL + attr_reader :server + + # @return [Hash] + attr_reader :options + + # @return [String] query path perfix + attr_reader :path_prefix # @param server [String] URL with protocol://host:port (paths are preserved as well) # @param auth_token [String] optional Authorization: Bearer token # @param auth_username [String] optional Basic authentication username # @param auth_password [String] optional Basic authentication password - # @param options [Hash] @see Excon.new + # @param options [Hash] see https://www.rubydoc.info/github/excon/excon/Excon.new def initialize(server, auth_token: nil, auth_username: nil, auth_password: nil, **options) uri = URI.parse(server) @server = "#{uri.scheme}://#{uri.host}:#{uri.port}" @@ -185,7 +193,7 @@ def path(*parts) # @param request_object [Object] include request body using to_json # @param content_type [String] request body content-type - # @param options [Hash] @see Excon#request + # @param options see https://www.rubydoc.info/github/excon/excon/Excon%2FConnection:request # @return [Hash] def request_options(request_object: nil, content_type: 'application/json', **options) options[:headers] ||= {} @@ -268,8 +276,8 @@ def parse_response(response, request_options, response_class: nil) end end - # @param response_class [Class] coerce into response class using #new - # @param options [Hash] @see Excon#request + # @param response_class [Class] coerce into response class using response_class.new + # @param options [Hash] see https://www.rubydoc.info/github/excon/excon/Excon%2FConnection:request # @return [response_class, Hash] def request(response_class: nil, **options) if options[:method] == 'DELETE' && need_delete_body? @@ -365,9 +373,10 @@ def get(*path, **options) **options ) end + exceptionless_bang_method :get # @param paths [Array] - # @param options [Hash] @see #request + # @param (see #request) # @return [Array] def gets(*paths, **options) requests( diff --git a/lib/k8s/util.rb b/lib/k8s/util.rb index 1ffb060..7bbe58c 100644 --- a/lib/k8s/util.rb +++ b/lib/k8s/util.rb @@ -3,6 +3,23 @@ module K8s # Miscellaneous helpers module Util + module ExceptionlessBangMethod + private + + # @!macro [attach] exceptionless_bang_method + # @method $1! + # Same as $1 but all exceptions are suppressed + def exceptionless_bang_method(meth) + define_method(meth.to_s.concat('!')) do |*args, **options| + begin + send(meth, *args, **options) + rescue StandardError + nil + end + end + end + end + module HashDeepMerge refine Hash do # @param other [Hash] diff --git a/spec/k8s/resource_client_spec.rb b/spec/k8s/resource_client_spec.rb index c75b514..fdad2cc 100644 --- a/spec/k8s/resource_client_spec.rb +++ b/spec/k8s/resource_client_spec.rb @@ -68,23 +68,38 @@ end context "GET /api/v1/nodes/*" do - before do - stub_request(:get, 'localhost:8080/api/v1/nodes/ubuntu-xenial') - .to_return( - status: 200, - body: fixture('api/nodes-get.json'), - headers: { 'Content-Type' => 'application/json' } - ) - end - describe '#get' do - it "returns a resource" do - obj = subject.get('ubuntu-xenial') + let(:status) { 200 } + + before do + stub_request(:get, 'localhost:8080/api/v1/nodes/ubuntu-xenial') + .to_return( + status: status, + body: fixture('api/nodes-get.json'), + headers: { 'Content-Type' => 'application/json' } + ) + end + context '200' do + it "returns a resource" do + obj = subject.get('ubuntu-xenial') + + expect(obj).to match K8s::Resource + expect(obj.kind).to eq "Node" + expect(obj.metadata.namespace).to be nil + expect(obj.metadata.name).to eq "ubuntu-xenial" + end + end - expect(obj).to match K8s::Resource - expect(obj.kind).to eq "Node" - expect(obj.metadata.namespace).to be nil - expect(obj.metadata.name).to eq "ubuntu-xenial" + context '404' do + let(:status) { 404 } + + it 'raises K8s::Error::NotFound' do + expect{subject.get('ubuntu-xenial')}.to raise_error(K8s::Error::NotFound) + end + + it 'returns nil when called with the ! variant' do + expect(subject.get!('ubuntu-xenial')).to be_nil + end end end end diff --git a/spec/k8s/transport_spec.rb b/spec/k8s/transport_spec.rb index cd10394..958f179 100644 --- a/spec/k8s/transport_spec.rb +++ b/spec/k8s/transport_spec.rb @@ -673,6 +673,16 @@ ) }.to raise_error(K8s::Error::NotFound) end + + it "returns nil when called via the ! variant" do + expect{ + subject.gets( + '/api/v1/namespaces/default/services/foo', + '/api/v1/namespaces/default/configmaps/bar', + skip_forbidden: true, + ) + }.to raise_error(K8s::Error::NotFound) + end end end end From 1c33a447a4760561392d0ba66c1533a75f14cca9 Mon Sep 17 00:00:00 2001 From: Kimmo Lehto Date: Fri, 10 May 2019 15:25:19 +0300 Subject: [PATCH 2/3] Clean spec --- spec/k8s/transport_spec.rb | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/spec/k8s/transport_spec.rb b/spec/k8s/transport_spec.rb index 958f179..ac56de5 100644 --- a/spec/k8s/transport_spec.rb +++ b/spec/k8s/transport_spec.rb @@ -498,6 +498,10 @@ it "raises Forbidden" do expect{subject.get('/api/v1/nodes')}.to raise_error K8s::Error::Forbidden, 'GET /api/v1/nodes => HTTP 403 Forbidden: nodes is forbidden: User "system:anonymous" cannot list nodes at the cluster scope' end + + it "returns nil if called with the ! variant" do + expect(subject.get!('/api/v1/nodes')).to be_nil + end end end @@ -673,16 +677,6 @@ ) }.to raise_error(K8s::Error::NotFound) end - - it "returns nil when called via the ! variant" do - expect{ - subject.gets( - '/api/v1/namespaces/default/services/foo', - '/api/v1/namespaces/default/configmaps/bar', - skip_forbidden: true, - ) - }.to raise_error(K8s::Error::NotFound) - end end end end From e677dc28b233e7bc785c58f3c1aec15560677479 Mon Sep 17 00:00:00 2001 From: Kimmo Lehto Date: Tue, 14 May 2019 13:24:50 +0300 Subject: [PATCH 3/3] Wrong param name in .autoconfig --- lib/k8s/client.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/k8s/client.rb b/lib/k8s/client.rb index 366c83a..b00fb06 100644 --- a/lib/k8s/client.rb +++ b/lib/k8s/client.rb @@ -48,7 +48,7 @@ def self.config(config, namespace: nil, **options) # An K8s::Client instance from in-cluster config within a kube pod, using the kubernetes service envs and serviceaccount secrets # # @param namespace [String] default namespace for all operations - # @param (see Transport.in_cluster_config) + # @param (see K8s::Transport.in_cluster_config) # @return [K8s::Client] # @raise [K8s::Error::Config,Errno::ENOENT,Errno::EACCES] def self.in_cluster_config(namespace: nil, **options) @@ -66,7 +66,8 @@ def self.in_cluster_config(namespace: nil, **options) # # Will raise when no means of configuration is available # - # @param options [Hash] default namespace for all operations + # @param namespace [String] default namespace for all operations + # @param (see K8s::Transport.config) # @raise [K8s::Error::Config,Errno::ENOENT,Errno::EACCES] # @return [K8s::Client] def self.autoconfig(namespace: nil, **options) @@ -190,7 +191,7 @@ def resources(namespace: nil) # Returns flattened array with mixed resource kinds. # # @param resources [Array] default is all listable resources for api - # @param (see ResourceClient.list) + # @param (see K8s::ResourceClient.list) # @return [Array] def list_resources(resources = nil, **options) cached_clients = @api_clients.size.positive?