k8s-client-renewed 0.10.5.pre.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,233 @@
1
+ # K8s::Client
2
+
3
+ [![Build Status](https://travis-ci.com/kontena/k8s-client.svg?branch=master)](https://travis-ci.com/kontena/k8s-client)
4
+ [![Gem Version](https://badge.fury.io/rb/k8s-client.svg)](https://badge.fury.io/rb/k8s-client)
5
+ [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/github/kontena/k8s-client/master)
6
+
7
+
8
+ Ruby client library for the Kubernetes (1.9+) API
9
+
10
+ ## Highlights
11
+
12
+ * Clean API for dynamic Kubernetes API Groups / Resources
13
+ * Fast API requests using HTTP connection keepalive
14
+ * Fast API discovery and resource listings using pipelined HTTP requests
15
+ * Typed errors with useful debugging information
16
+
17
+ ## Installation
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ ```ruby
22
+ gem 'k8s-client'
23
+ ```
24
+
25
+ And then execute:
26
+
27
+ $ bundle
28
+
29
+ Or install it yourself as:
30
+
31
+ $ gem install k8s-client
32
+
33
+ And then load the code using:
34
+
35
+ ```ruby
36
+ require 'k8s-client'
37
+ ```
38
+
39
+ To use the [yajl-ruby](https://github.com/brianmario/yajl-ruby) JSON parser backend, add `yalj-ruby` to your Gemfile:
40
+
41
+ ```ruby
42
+ gem 'yajl-ruby'
43
+ gem 'k8s-client'
44
+ ```
45
+
46
+ And then load the code using:
47
+
48
+ ```ruby
49
+ require 'k8s-client'
50
+ require 'k8s/json_parser/yajl'
51
+ ```
52
+
53
+ ## Usage
54
+
55
+ ### Overview
56
+ The top-level `K8s::Client` provides access to separate `APIClient` instances for each Kubernetes API Group (`v1`, `apps/v1`, etc.), which in turns provides access to separate `ResourceClient` instances for each API resource type (`nodes`, `pods`, `deployments`, etc.).
57
+
58
+ Individual resources are returned as `K8s::Resource` instances, which provide attribute access (`resource.metadata.name`). The resource instances are returned by methods such as `client.api('v1').resource('nodes').get('foo')`, and passed as arguments for `client.api('v1').resource('nodes').create_resource(res)`. Resources can also be loaded from disk using `K8s::Resource.from_files(path)`, and passed to the top-level methods such as `client.create_resource(res)`, which lookup the correct API/Resource client from the resource `apiVersion` and `kind`.
59
+
60
+ The different `K8s::Error::API` subclasses represent different HTTP response codes, such as `K8s::Error::NotFound` or `K8s::Error::Conflict`.
61
+
62
+ ### Creating a client
63
+
64
+ #### Unauthenticated client
65
+
66
+ ```ruby
67
+ client = K8s.client('https://localhost:6443', ssl_verify_peer: false)
68
+ ```
69
+
70
+ The keyword options are [Excon](https://github.com/excon/excon/) options.
71
+
72
+ #### Client from kubeconfig
73
+
74
+ ```ruby
75
+ client = K8s::Client.config(
76
+ K8s::Config.load_file(
77
+ File.expand_path '~/.kube/config'
78
+ )
79
+ )
80
+ ```
81
+
82
+ #### Supported kubeconfig options
83
+
84
+ Not all kubeconfig options are supported, only the following kubeconfig options work:
85
+
86
+ * `current-context`
87
+ * `context.cluster`
88
+ * `context.user`
89
+ * `cluster.server`
90
+ * `cluster.insecure_skip_tls_verify`
91
+ * `cluster.certificate_authority`
92
+ * `cluster.certificate_authority_data`
93
+ * `user.client_certificate` + `user.client_key`
94
+ * `user.client_certificate_data` + `user.client_key_data`
95
+ * `user.token`
96
+
97
+ ##### With overrides
98
+
99
+ ```ruby
100
+ client = K8s::Client.config(K8s::Config.load_file('~/.kube/config'),
101
+ server: 'http://localhost:8001',
102
+ )
103
+ ```
104
+
105
+ #### In-cluster client from pod envs/secrets
106
+
107
+ ```ruby
108
+ client = K8s::Client.in_cluster_config
109
+ ```
110
+
111
+ ### Logging
112
+
113
+ #### Quiet
114
+
115
+ To supress any warning messages:
116
+
117
+ ```ruby
118
+ K8s::Logging.quiet!
119
+ K8s::Transport.quiet!
120
+ ```
121
+
122
+ The `K8s::Transport` is quiet by default, but other components may log warnings in the future.
123
+
124
+ #### Debugging
125
+
126
+ Log all API requests
127
+
128
+ ```ruby
129
+ K8s::Logging.debug!
130
+ K8s::Transport.verbose!
131
+ ```
132
+
133
+ ```
134
+ I, [2018-08-09T14:19:50.404739 #1] INFO -- K8s::Transport: Using config with server=https://167.99.39.233:6443
135
+ I, [2018-08-09T14:19:50.629521 #1] INFO -- K8s::Transport<https://167.99.39.233:6443>: GET /version => HTTP 200: <K8s::Resource> in 0.224s
136
+ I, [2018-08-09T14:19:50.681367 #1] INFO -- K8s::Transport<https://167.99.39.233:6443>: GET /api/v1 => HTTP 200: <K8s::Resource> in 0.046s
137
+ I, [2018-08-09T14:19:51.018740 #1] INFO -- K8s::Transport<https://167.99.39.233:6443>: GET /api/v1/pods => HTTP 200: <K8s::Resource> in 0.316s
138
+ ```
139
+
140
+ Using `K8s::Transport.debug!` will also log request/response bodies. The `EXCON_DEBUG=true` env will log all request/response attributes, including headers.
141
+
142
+ ### Prefetching API resources
143
+
144
+ Operations like mapping a resource `kind` to an API resource URL require knowledge of the API resource lists for the API group. Mapping resources for multiple API groups would require fetching the API resource lists for each API group in turn, leading to additional request latency. This can be optimized using resource prefetching:
145
+
146
+ ```ruby
147
+ client.apis(prefetch_resources: true)
148
+ ```
149
+
150
+ This will fetch the API resource lists for all API groups in a single pipelined request.
151
+
152
+ ### Listing resources
153
+
154
+ ```ruby
155
+ client.api('v1').resource('pods', namespace: 'default').list(labelSelector: {'role' => 'test'}).each do |pod|
156
+ puts "namespace=#{pod.metadata.namespace} pod: #{pod.metadata.name} node=#{pod.spec.nodeName}"
157
+ end
158
+ ```
159
+
160
+ ### Updating resources
161
+
162
+ ```ruby
163
+ node = client.api('v1').resource('nodes').get('test-node')
164
+
165
+ node[:spec][:unschedulable] = true
166
+
167
+ client.api('v1').resource('nodes').update_resource(node)
168
+ ```
169
+
170
+ ### Deleting resources
171
+
172
+ ```ruby
173
+ pod = client.api('v1').resource('pods', namespace: 'default').delete('test-pod')
174
+ ```
175
+
176
+ ```ruby
177
+ pods = client.api('v1').resource('pods', namespace: 'default').delete_collection(labelSelector: {'role' => 'test'})
178
+ ```
179
+
180
+ ### Creating resources
181
+
182
+ #### Programmatically defined resources
183
+ ```ruby
184
+ service = K8s::Resource.new(
185
+ apiVersion: 'v1',
186
+ kind: 'Service',
187
+ metadata: {
188
+ namespace: 'default',
189
+ name: 'test',
190
+ },
191
+ spec: {
192
+ type: 'ClusterIP',
193
+ ports: [
194
+ { port: 80 },
195
+ ],
196
+ selector: {'app' => 'test'},
197
+ },
198
+ )
199
+
200
+ logger.info "Create service=#{service.metadata.name} in namespace=#{service.metadata.namespace}"
201
+
202
+ service = client.api('v1').resource('services').create_resource(service)
203
+ ```
204
+
205
+ #### From file(s)
206
+
207
+ ```ruby
208
+ resources = K8s::Resource.from_files('./test.yaml')
209
+
210
+ for resource in resources
211
+ resource = client.create_resource(resource)
212
+ end
213
+ ```
214
+
215
+ ### Patching resources
216
+
217
+ ```ruby
218
+ client.api('apps/v1').resource('deployments', namespace: 'default').merge_patch('test', {
219
+ spec: { replicas: 3 },
220
+ })
221
+ ```
222
+
223
+ ### Watching resources
224
+
225
+ ```ruby
226
+ client.api('v1').resource('pods', namespace: 'default').watch(labelSelector: {'role' => 'test'}) do |watch_event|
227
+ puts "type=#{watch_event.type} pod=#{watch_event.resource.metadata.name}"
228
+ end
229
+ ```
230
+
231
+ ## Contributing
232
+
233
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kontena/k8s-client.
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require 'rubocop/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ RuboCop::RakeTask.new(:rubocop).tap do |task|
10
+ task.options = %w(--fail-level A -S --format c)
11
+ if RUBY_ENGINE == 'ruby' && RbConfig::CONFIG['host_os'] !~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/
12
+ task.options << '--parallel'
13
+ end
14
+ end
15
+
16
+ task default: [:spec, :rubocop]
@@ -0,0 +1,10 @@
1
+ rspec:
2
+ build: .
3
+ volumes:
4
+ - .:/app
5
+ entrypoint: bundle exec rspec
6
+ k8s-client:
7
+ build: .
8
+ volumes:
9
+ - .:/app
10
+ entrypoint: bundle exec bin/k8s-client
@@ -0,0 +1,39 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "k8s/client/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "k8s-client-renewed"
8
+ spec.version = K8s::Client::VERSION
9
+ spec.authors = ["Kontena, Inc."]
10
+ spec.email = ["info@kontena.io"]
11
+ spec.license = "Apache-2.0"
12
+
13
+ spec.summary = "Kubernetes client library"
14
+ spec.homepage = "https://github.com/kontena/k8s-client"
15
+
16
+ # Specify which files should be added to the gem when it is released.
17
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
18
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
19
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
20
+ end
21
+ spec.bindir = "bin"
22
+ spec.executables = []
23
+ spec.require_paths = ["lib"]
24
+ # spec.required_ruby_version = '> 2.4'
25
+
26
+ spec.add_runtime_dependency "excon"#, "~> 0.66"
27
+ spec.add_runtime_dependency "recursive-open-struct"#, "~> 1.1.0"
28
+ spec.add_runtime_dependency 'hashdiff'#, '~> 1.0.0'
29
+ spec.add_runtime_dependency 'jsonpath'#, '~> 0.9.5'
30
+ spec.add_runtime_dependency "yaml-safe_load_stream-renewed"#, "~> 0.1"
31
+
32
+ # spec.add_development_dependency "bundler"#, ">= 1.17", "< 3.0"
33
+ spec.add_development_dependency "byebug"#, "~> 10.0"
34
+ spec.add_development_dependency "rake"#, "~> 10.0"
35
+ spec.add_development_dependency "rspec"#, "~> 3.7"
36
+ spec.add_development_dependency "webmock"#, "~> 3.6.2"
37
+ spec.add_development_dependency "rubocop"#, "~> 0.59"
38
+ spec.add_development_dependency 'yajl-ruby'#, '~> 1.4.0'
39
+ end
data/lib/k8s-client.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'k8s/client'
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K8s
4
+ # Per-APIGroup/version client.
5
+ #
6
+ # Offers access to {ResourceClient} instances for the APIResource types defined in this apigroup/version
7
+ class APIClient
8
+ # @param api_version [String] either core version (v1) or apigroup/apiversion (apps/v1)
9
+ # @return [String]
10
+ def self.path(api_version)
11
+ if api_version.include? '/'
12
+ File.join('/apis', api_version)
13
+ else
14
+ File.join('/api', api_version)
15
+ end
16
+ end
17
+
18
+ # @param transport [K8s::Transport]
19
+ # @param api_version [String] "group/version" or "version" (core)
20
+ def initialize(transport, api_version)
21
+ @transport = transport
22
+ @api_version = api_version
23
+ end
24
+
25
+ # @return [String]
26
+ attr_reader :api_version
27
+
28
+ # @param path [Array<String>] join path from parts
29
+ # @return [String]
30
+ def path(*path)
31
+ @transport.path(self.class.path(@api_version), *path)
32
+ end
33
+
34
+ # @return [Bool] loaded yet?
35
+ def api_resources?
36
+ !!@api_resources
37
+ end
38
+
39
+ # @param api_resources [Array<K8s::Resource>]
40
+ attr_writer :api_resources
41
+
42
+ # Force-update APIResources
43
+ #
44
+ # @return [Array<K8s::Resource>]
45
+ def api_resources!
46
+ @api_resources = @transport.get(path).resources
47
+ end
48
+
49
+ # Cached APIResources
50
+ #
51
+ # @return [Array<K8s::Resource>]
52
+ def api_resources
53
+ @api_resources || api_resources!
54
+ end
55
+
56
+ # @param resource_name [String]
57
+ # @raise [K8s::Error::UndefinedResource]
58
+ # @return [K8s::Resource]
59
+ def find_api_resource(resource_name)
60
+ found_resource = api_resources.find{ |api_resource| api_resource.name == resource_name }
61
+ found_resource ||= api_resources!.find{ |api_resource| api_resource.name == resource_name }
62
+ raise K8s::Error::UndefinedResource, "Unknown resource #{resource_name} for #{@api_version}" unless found_resource
63
+
64
+ found_resource
65
+ end
66
+
67
+ # @param resource_name [String]
68
+ # @param namespace [String, nil]
69
+ # @raise [K8s::Error::UndefinedResource]
70
+ # @return [K8s::ResourceClient]
71
+ def resource(resource_name, namespace: nil)
72
+ ResourceClient.new(@transport, self, find_api_resource(resource_name), namespace: namespace)
73
+ end
74
+
75
+ # @param resource [K8s::Resource]
76
+ # @param namespace [String, nil] default if resource is missing namespace
77
+ # @raise [K8s::Error::UndefinedResource]
78
+ # @return [K8s::ResourceClient]
79
+ def client_for_resource(resource, namespace: nil)
80
+ unless @api_version == resource.apiVersion
81
+ raise K8s::Error::UndefinedResource, "Invalid apiVersion=#{resource.apiVersion} for #{@api_version} client"
82
+ end
83
+
84
+ found_resource = api_resources.find{ |api_resource| api_resource.kind == resource.kind }
85
+ found_resource ||= api_resources!.find{ |api_resource| api_resource.kind == resource.kind }
86
+ raise K8s::Error::UndefinedResource, "Unknown resource kind=#{resource.kind} for #{@api_version}" unless found_resource
87
+
88
+ ResourceClient.new(@transport, self, found_resource, namespace: resource.metadata.namespace || namespace)
89
+ end
90
+
91
+ # TODO: skip non-namespaced resources if namespace is given, or ignore namespace?
92
+ #
93
+ # @param namespace [String, nil]
94
+ # @return [Array<K8s::ResourceClient>]
95
+ def resources(namespace: nil)
96
+ api_resources.map{ |api_resource|
97
+ ResourceClient.new(@transport, self, api_resource,
98
+ namespace: namespace)
99
+ }
100
+ end
101
+
102
+ # Pipeline list requests for multiple resource types.
103
+ #
104
+ # Returns flattened array with mixed resource kinds.
105
+ #
106
+ # @param resources [Array<K8s::ResourceClient>] default is all listable resources for api
107
+ # @param options @see [K8s::ResourceClient#list]
108
+ # @return [Array<K8s::Resource>]
109
+ def list_resources(resources = nil, **options)
110
+ resources ||= self.resources.select(&:list?)
111
+
112
+ ResourceClient.list(resources, @transport, **options)
113
+ end
114
+ end
115
+ end
data/lib/k8s/client.rb ADDED
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'base64'
5
+
6
+ require 'monitor'
7
+ require 'uri'
8
+
9
+ require 'k8s/json_parser'
10
+ require 'k8s/util'
11
+
12
+ require 'k8s/config'
13
+ require 'k8s/logging'
14
+ require 'k8s/api_client'
15
+ require "k8s/error"
16
+ require 'k8s/resource'
17
+ require 'k8s/resource_client'
18
+ require 'k8s/stack'
19
+ require 'k8s/transport'
20
+
21
+ module K8s
22
+ # @param server [String] http/s URL
23
+ # @param options [Hash] @see Transport.new
24
+ # @return [K8s::Client]
25
+ def self.client(server, **options)
26
+ Client.new(Transport.new(server, **options))
27
+ end
28
+
29
+ # Top-level client wrapper.
30
+ # Uses a {Transport} instance to talk to the kube API.
31
+ # Offers access to {APIClient} and {ResourceClient} instances.
32
+ class Client
33
+ # @param config [Phraos::Kube::Config]
34
+ # @param namespace [String] @see #initialize
35
+ # @param options [Hash] @see Transport.config
36
+ # @return [K8s::Client]
37
+ def self.config(config, namespace: nil, **options)
38
+ new(
39
+ Transport.config(config, **options),
40
+ namespace: namespace
41
+ )
42
+ end
43
+
44
+ # An K8s::Client instance from in-cluster config within a kube pod, using the kubernetes service envs and serviceaccount secrets
45
+ # @see K8s::Transport#in_cluster_config
46
+ #
47
+ # @param namespace [String] default namespace for all operations
48
+ # @param options [Hash] options passed to transport, @see Transport#in_cluster_config
49
+ # @return [K8s::Client]
50
+ # @raise [K8s::Error::Config,Errno::ENOENT,Errno::EACCES]
51
+ def self.in_cluster_config(namespace: nil, **options)
52
+ new(Transport.in_cluster_config(**options), namespace: namespace)
53
+ end
54
+
55
+ # Attempts to create a K8s::Client instance automatically using environment variables, existing configuration
56
+ # files or in cluster configuration.
57
+ #
58
+ # Look-up order:
59
+ # - KUBE_TOKEN, KUBE_CA, KUBE_SERVER environment variables
60
+ # - KUBECONFIG environment variable
61
+ # - $HOME/.kube/config file
62
+ # - In cluster configuration
63
+ #
64
+ # Will raise when no means of configuration is available
65
+ #
66
+ # @param options [Hash] default namespace for all operations
67
+ # @raise [K8s::Error::Config,Errno::ENOENT,Errno::EACCES]
68
+ # @return [K8s::Client]
69
+ def self.autoconfig(namespace: nil, **options)
70
+ if ENV.values_at('KUBE_TOKEN', 'KUBE_CA', 'KUBE_SERVER').none? { |v| v.nil? || v.empty? }
71
+ unless Base64.decode64(ENV['KUBE_CA']).match?(/CERTIFICATE/)
72
+ raise ArgumentError, 'KUBE_CA does not seem to be base64 encoded'
73
+ end
74
+
75
+ begin
76
+ token = options[:auth_token] || Base64.strict_decode64(ENV['KUBE_TOKEN'])
77
+ rescue ArgumentError
78
+ raise ArgumentError, 'KUBE_TOKEN does not seem to be base64 encoded'
79
+ end
80
+
81
+ configuration = K8s::Config.build(server: ENV['KUBE_SERVER'], ca: ENV['KUBE_CA'], auth_token: token)
82
+ elsif !ENV['KUBECONFIG'].to_s.empty?
83
+ configuration = K8s::Config.from_kubeconfig_env(ENV['KUBECONFIG'])
84
+ else
85
+ found_config = [
86
+ File.join(Dir.home, '.kube', 'config'),
87
+ '/etc/kubernetes/admin.conf',
88
+ '/etc/kubernetes/kubelet.conf'
89
+ ].find { |f| File.exist?(f) && File.readable?(f) }
90
+
91
+ configuration = K8s::Config.load_file(found_config) if found_config
92
+ end
93
+
94
+ if configuration
95
+ config(configuration, namespace: namespace, **options)
96
+ else
97
+ in_cluster_config(namespace: namespace, **options)
98
+ end
99
+ end
100
+
101
+ include MonitorMixin
102
+
103
+ attr_reader :transport
104
+
105
+ # @param transport [K8s::Transport]
106
+ # @param namespace [String, nil] default namespace for all operations
107
+ def initialize(transport, namespace: nil)
108
+ @transport = transport
109
+ @namespace = namespace
110
+
111
+ @api_clients = {}
112
+ super()
113
+ end
114
+
115
+ # @raise [K8s::Error]
116
+ # @return [K8s::Resource]
117
+ def version
118
+ @version ||= @transport.version
119
+ end
120
+
121
+ # @param api_version [String] "group/version" or "version" (core)
122
+ # @return [APIClient]
123
+ def api(api_version = 'v1')
124
+ @api_clients[api_version] ||= APIClient.new(@transport, api_version)
125
+ end
126
+
127
+ # Force-update /apis cache.
128
+ # Required if creating new CRDs/apiservices.
129
+ #
130
+ # @return [Array<String>]
131
+ def api_groups!
132
+ synchronize do
133
+ @api_groups = @transport.get(
134
+ '/apis',
135
+ response_class: K8s::Resource
136
+ ).groups.flat_map{ |api_group| api_group.versions.map(&:groupVersion) }
137
+
138
+ @api_clients.clear
139
+ end
140
+
141
+ @api_groups
142
+ end
143
+
144
+ # Cached /apis preferred group apiVersions
145
+ # @return [Array<String>]
146
+ def api_groups
147
+ @api_groups || api_groups!
148
+ end
149
+
150
+ # @param api_versions [Array<String>] defaults to all APIs
151
+ # @param prefetch_resources [Boolean] prefetch any missing api_resources for each api_version
152
+ # @param skip_missing [Boolean] return APIClient without api_resources? if 404
153
+ # @return [Array<APIClient>]
154
+ def apis(api_versions = nil, prefetch_resources: false, skip_missing: false)
155
+ api_versions ||= ['v1'] + api_groups
156
+
157
+ if prefetch_resources
158
+ # api groups that are missing their api_resources
159
+ api_paths = api_versions
160
+ .uniq
161
+ .reject{ |api_version| api(api_version).api_resources? }
162
+ .map{ |api_version| APIClient.path(api_version) }
163
+
164
+ # load into APIClient.api_resources=
165
+ begin
166
+ @transport.gets(*api_paths, skip_missing: skip_missing).each do |api_resource_list|
167
+ api(api_resource_list.groupVersion).api_resources = api_resource_list.resources if api_resource_list
168
+ end
169
+ rescue K8s::Error::NotFound, K8s::Error::ServiceUnavailable # rubocop:disable Lint/HandleExceptions
170
+ # kubernetes api is in unstable state
171
+ # because this is only performance optimization, better to skip prefetch and move on
172
+ end
173
+ end
174
+
175
+ api_versions.map{ |api_version| api(api_version) }
176
+ end
177
+
178
+ # @param namespace [String, nil]
179
+ # @return [Array<K8s::ResourceClient>]
180
+ def resources(namespace: nil)
181
+ apis(prefetch_resources: true).map { |api|
182
+ begin
183
+ api.resources(namespace: namespace)
184
+ rescue K8s::Error::ServiceUnavailable, K8s::Error::NotFound
185
+ []
186
+ end
187
+ }.flatten
188
+ end
189
+
190
+ # Pipeline list requests for multiple resource types.
191
+ #
192
+ # Returns flattened array with mixed resource kinds.
193
+ #
194
+ # @param resources [Array<K8s::ResourceClient>] default is all listable resources for api
195
+ # @param options @see K8s::ResourceClient#list
196
+ # @return [Array<K8s::Resource>]
197
+ def list_resources(resources = nil, **options)
198
+ cached_clients = @api_clients.size.positive?
199
+ resources ||= self.resources.select(&:list?)
200
+
201
+ begin
202
+ ResourceClient.list(resources, @transport, **options)
203
+ rescue K8s::Error::NotFound
204
+ raise unless cached_clients
205
+
206
+ cached_clients = false
207
+ api_groups!
208
+ retry
209
+ end
210
+ end
211
+
212
+ # @param resource [K8s::Resource]
213
+ # @param namespace [String, nil] default if resource is missing namespace
214
+ # @raise [K8s::Error::NotFound] API Group does not exist
215
+ # @raise [K8s::Error::UndefinedResource]
216
+ # @return [K8s::ResourceClient]
217
+ def client_for_resource(resource, namespace: nil)
218
+ api(resource.apiVersion).client_for_resource(resource, namespace: namespace)
219
+ end
220
+
221
+ # @param resource [K8s::Resource]
222
+ # @return [K8s::Resource]
223
+ def create_resource(resource)
224
+ client_for_resource(resource).create_resource(resource)
225
+ end
226
+
227
+ # @param resource [K8s::Resource]
228
+ # @return [K8s::Resource]
229
+ def get_resource(resource)
230
+ client_for_resource(resource).get_resource(resource)
231
+ end
232
+
233
+ # Returns nils for any resources that do not exist.
234
+ # This includes custom resources that were not yet defined.
235
+ #
236
+ # @param resources [Array<K8s::Resource>]
237
+ # @return [Array<K8s::Resource, nil>] matching resources array 1:1
238
+ def get_resources(resources)
239
+ # prefetch api resources, skip missing APIs
240
+ resource_apis = apis(resources.map(&:apiVersion), prefetch_resources: true, skip_missing: true)
241
+
242
+ # map each resource to excon request options, or nil if resource is not (yet) defined
243
+ requests = resources.zip(resource_apis).map{ |resource, api_client|
244
+ next nil unless api_client.api_resources?
245
+
246
+ resource_client = api_client.client_for_resource(resource)
247
+
248
+ {
249
+ method: 'GET',
250
+ path: resource_client.path(resource.metadata.name, namespace: resource.metadata.namespace),
251
+ response_class: resource_client.resource_class
252
+ }
253
+ }
254
+
255
+ # map non-nil requests to response objects, or nil for nil request options
256
+ Util.compact_map(requests) { |reqs|
257
+ @transport.requests(*reqs, skip_missing: true)
258
+ }
259
+ end
260
+
261
+ # @param resource [K8s::Resource]
262
+ # @return [K8s::Resource]
263
+ def update_resource(resource)
264
+ client_for_resource(resource).update_resource(resource)
265
+ end
266
+
267
+ # @param resource [K8s::Resource]
268
+ # @param options [Hash]
269
+ # @see ResourceClient#delete for options
270
+ # @return [K8s::Resource]
271
+ def delete_resource(resource, **options)
272
+ client_for_resource(resource).delete_resource(resource, **options)
273
+ end
274
+
275
+ # @param resource [K8s::Resource]
276
+ # @param attrs [Hash]
277
+ # @return [K8s::Client]
278
+ def patch_resource(resource, attrs)
279
+ client_for_resource(resource).json_patch(resource.metadata.name, attrs)
280
+ end
281
+ end
282
+ end