k8s-ruby 0.10.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ rspec:
2
+ build: .
3
+ volumes:
4
+ - .:/app
5
+ entrypoint: bundle exec rspec
6
+ k8s-ruby:
7
+ build: .
8
+ volumes:
9
+ - .:/app
10
+ entrypoint: bundle exec bin/k8s-client
@@ -0,0 +1,40 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "k8s/ruby/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "k8s-ruby"
8
+ spec.version = K8s::Ruby::VERSION
9
+ spec.authors = ["rdx.net", "Kontena, Inc."]
10
+ spec.email = ["firstname.lastname@rdx.net"]
11
+ spec.license = "Apache-2.0"
12
+
13
+ spec.summary = "Kubernetes client library for Ruby"
14
+ spec.homepage = "https://github.com/rdxnet/k8s-ruby"
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.71"
27
+ spec.add_runtime_dependency "dry-struct", "~> 0.5.0"
28
+ spec.add_runtime_dependency "dry-types", "~> 0.13.0"
29
+ spec.add_runtime_dependency "recursive-open-struct", "~> 1.1.0"
30
+ spec.add_runtime_dependency 'hashdiff', '~> 1.0.0'
31
+ spec.add_runtime_dependency 'jsonpath', '~> 0.9.5'
32
+ spec.add_runtime_dependency 'yajl-ruby', '~> 1.4.0'
33
+ spec.add_runtime_dependency "yaml-safe_load_stream", "~> 0.1"
34
+
35
+ spec.add_development_dependency "bundler", ">= 1.17", "< 3.0"
36
+ spec.add_development_dependency "rake", "~> 10.0"
37
+ spec.add_development_dependency "rspec", "~> 3.7"
38
+ spec.add_development_dependency "webmock", "~> 3.6.2"
39
+ spec.add_development_dependency "rubocop", "~> 0.59"
40
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'k8s/ruby/version'
4
+ require_relative 'k8s/client'
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry-types'
4
+ require 'dry-struct'
5
+
6
+ module K8s
7
+ # Kube API definitions
8
+ module API
9
+ # Common Dry::Types used in the API
10
+ module Types
11
+ include Dry::Types.module
12
+ end
13
+
14
+ # Common API struct type, handling JSON transforms with symbol keys
15
+ class Struct < Dry::Struct
16
+ # input from JSON with string keys
17
+ transform_keys(&:to_sym)
18
+
19
+ # @param data [Hash]
20
+ # @return [self]
21
+ def self.from_json(data)
22
+ new(Yajl::Parser.parse(data))
23
+ end
24
+
25
+ # @return [String]
26
+ def to_json(*_args)
27
+ to_hash.to_json
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'k8s/api'
4
+
5
+ module K8s
6
+ module API
7
+ # API types common to all apigroups/versions
8
+ #
9
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1
10
+ module MetaV1
11
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#TypeMeta
12
+ class Resource < Struct
13
+ # XXX: making these optional seems dangerous, but some APIs (GET /api/v1) are missing these
14
+ attribute :kind, Types::Strict::String.optional.default(nil)
15
+ attribute :apiVersion, Types::Strict::String.optional.default(nil)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ require 'k8s/api/metav1/api_group'
22
+ require 'k8s/api/metav1/api_resource'
23
+ require 'k8s/api/metav1/list'
24
+ require 'k8s/api/metav1/object'
25
+ require 'k8s/api/metav1/status'
26
+ require 'k8s/api/metav1/watch_event'
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K8s
4
+ module API
5
+ module MetaV1
6
+ # structured list of available APIGroup versions
7
+ # groupVersion provided for convenience
8
+ class APIGroupVersion < Struct
9
+ attribute :groupVersion, Types::Strict::String
10
+ attribute :version, Types::Strict::String
11
+ end
12
+
13
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#APIGroup
14
+ class APIGroup < Struct
15
+ attribute :name, Types::Strict::String
16
+ attribute :versions, Types::Strict::Array.of(APIGroupVersion)
17
+ attribute :preferredVersion, APIGroupVersion
18
+ end
19
+
20
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#APIGroupList
21
+ class APIGroupList < Resource
22
+ attribute :groups, Types::Strict::Array.of(APIGroup)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K8s
4
+ module API
5
+ module MetaV1
6
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#APIResource
7
+ class APIResource < Struct
8
+ attribute :name, Types::Strict::String
9
+ attribute :singularName, Types::Strict::String
10
+ attribute :namespaced, Types::Strict::Bool
11
+ attribute :group, Types::Strict::String.optional.default(nil)
12
+ attribute :version, Types::Strict::String.optional.default(nil)
13
+ attribute :kind, Types::Strict::String
14
+ attribute :verbs, Types::Strict::Array.of(Types::Strict::String)
15
+ attribute :shortNames, Types::Strict::Array.of(Types::Strict::String).optional.default(proc { [] })
16
+ attribute :categories, Types::Strict::Array.of(Types::Strict::String).optional.default(proc { [] })
17
+ end
18
+
19
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#APIResourceList
20
+ class APIResourceList < Resource
21
+ attribute :groupVersion, Types::Strict::String
22
+ attribute :resources, Types::Strict::Array.of(APIResource)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K8s
4
+ module API
5
+ module MetaV1
6
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#ListMeta
7
+ class ListMeta < Struct
8
+ attribute :selfLink, Types::Strict::String.optional.default(nil)
9
+ attribute :resourceVersion, Types::Strict::String.optional.default(nil)
10
+ attribute :continue, Types::Strict::String.optional.default(nil)
11
+ end
12
+
13
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#List
14
+ class List < Resource
15
+ attribute :metadata, ListMeta
16
+ attribute :items, Types::Strict::Array # untyped
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'k8s/api/metav1/status'
4
+
5
+ module K8s
6
+ module API
7
+ module MetaV1
8
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#OwnerReference
9
+ class OwnerReference < Resource
10
+ attribute :name, Types::Strict::String
11
+ attribute :uid, Types::Strict::String
12
+ attribute :controller, Types::Strict::Bool.optional.default(nil)
13
+ attribute :blockOwnerDeletion, Types::Strict::Bool.optional.default(nil)
14
+ end
15
+
16
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Initializer
17
+ class Initializer < Struct
18
+ attribute :name, Types::Strict::String
19
+ end
20
+
21
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Initializers
22
+ class Initializers < Struct
23
+ attribute :pending, Initializer
24
+ attribute :result, Status.optional.default(nil)
25
+ end
26
+
27
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#ObjectMeta
28
+ class ObjectMeta < Resource
29
+ attribute :name, Types::Strict::String.optional.default(nil)
30
+ attribute :generateName, Types::Strict::String.optional.default(nil)
31
+ attribute :namespace, Types::Strict::String.optional.default(nil)
32
+ attribute :selfLink, Types::Strict::String.optional.default(nil)
33
+ attribute :uid, Types::Strict::String.optional.default(nil)
34
+ attribute :resourceVersion, Types::Strict::String.optional.default(nil)
35
+ attribute :generation, Types::Strict::Integer.optional.default(nil)
36
+ attribute :creationTimestamp, Types::DateTime.optional.default(nil)
37
+ attribute :deletionTimestamp, Types::DateTime.optional.default(nil)
38
+ attribute :deletionGracePeriodSeconds, Types::Strict::Integer.optional.default(nil)
39
+ attribute :labels, Types::Strict::Hash.map(Types::Strict::String, Types::Strict::String).optional.default(nil)
40
+ attribute :annotations, Types::Strict::Hash.map(Types::Strict::String, Types::Strict::String).optional.default(nil)
41
+ attribute :ownerReferences, Types::Strict::Array.of(OwnerReference).optional.default([])
42
+ attribute :initializers, Initializers.optional.default(nil)
43
+ attribute :finalizers, Types::Strict::Array.of(Types::Strict::String).optional.default([])
44
+ attribute :clusterName, Types::Strict::String.optional.default(nil)
45
+ end
46
+
47
+ # common attributes shared by all object types
48
+ class ObjectCommon < Resource
49
+ attribute :metadata, ObjectMeta.optional.default(nil)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K8s
4
+ module API
5
+ module MetaV1
6
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#Status
7
+ class Status < Resource
8
+ # structured cause
9
+ class Cause < Struct
10
+ attribute :reason, Types::Strict::String.optional.default(nil)
11
+ attribute :message, Types::Strict::String.optional.default(nil) # human-readable
12
+ attribute :field, Types::Strict::String.optional.default(nil) # human-readable
13
+ end
14
+
15
+ # structured details
16
+ class Details < Struct
17
+ attribute :name, Types::Strict::String.optional.default(nil)
18
+ attribute :group, Types::Strict::String.optional.default(nil)
19
+ attribute :kind, Types::Strict::String.optional.default(nil)
20
+ attribute :uid, Types::Strict::String.optional.default(nil)
21
+ attribute :causes, Types::Strict::Array.of(Cause).optional.default(nil)
22
+ attribute :retryAfterSeconds, Types::Strict::Integer.optional.default(nil)
23
+ end
24
+
25
+ attribute :metadata, ListMeta
26
+ attribute :status, Types::Strict::String.enum('Success', 'Failure').optional.default(nil)
27
+ attribute :message, Types::Strict::String.optional.default(nil) # human-readable
28
+ attribute :reason, Types::Strict::String.optional.default(nil) # machine-readable
29
+ attribute :details, Details.optional.default(nil)
30
+ attribute :code, Types::Strict::Integer.optional.default(nil)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../resource'
4
+
5
+ module K8s
6
+ module API
7
+ module MetaV1
8
+ # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#WatchEvent
9
+ class WatchEvent < Resource
10
+ attribute :type, Types::Strict::String
11
+ attribute :object, Types::Strict::Hash
12
+
13
+ # @return [K8s::Resource]
14
+ def resource
15
+ @resource ||= K8s::Resource.new(object)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module K8s
4
+ module API
5
+ # GET /version
6
+ class Version < Struct
7
+ attribute :buildDate, Types::Strict::String # TODO: parse datetime?
8
+ attribute :compiler, Types::Strict::String
9
+ attribute :gitCommit, Types::Strict::String
10
+ attribute :gitTreeState, Types::Strict::String
11
+ attribute :gitVersion, Types::Strict::String
12
+ attribute :major, Types::Strict::String
13
+ attribute :minor, Types::Strict::String
14
+ attribute :platform, Types::Strict::String
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,116 @@
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::API::MetaV1::APIResource>]
40
+ attr_writer :api_resources
41
+
42
+ # Force-update APIResources
43
+ #
44
+ # @return [Array<K8s::API::MetaV1::APIResource>]
45
+ def api_resources!
46
+ @api_resources = @transport.get(path,
47
+ response_class: K8s::API::MetaV1::APIResourceList).resources
48
+ end
49
+
50
+ # Cached APIResources
51
+ #
52
+ # @return [Array<K8s::API::MetaV1::APIResource>]
53
+ def api_resources
54
+ @api_resources || api_resources!
55
+ end
56
+
57
+ # @param resource_name [String]
58
+ # @raise [K8s::Error::UndefinedResource]
59
+ # @return [K8s::API::MetaV1::APIResource]
60
+ def find_api_resource(resource_name)
61
+ found_resource = api_resources.find{ |api_resource| api_resource.name == resource_name }
62
+ found_resource ||= api_resources!.find{ |api_resource| api_resource.name == resource_name }
63
+ raise K8s::Error::UndefinedResource, "Unknown resource #{resource_name} for #{@api_version}" unless found_resource
64
+
65
+ found_resource
66
+ end
67
+
68
+ # @param resource_name [String]
69
+ # @param namespace [String, nil]
70
+ # @raise [K8s::Error::UndefinedResource]
71
+ # @return [K8s::ResourceClient]
72
+ def resource(resource_name, namespace: nil)
73
+ ResourceClient.new(@transport, self, find_api_resource(resource_name), namespace: namespace)
74
+ end
75
+
76
+ # @param resource [K8s::Resource]
77
+ # @param namespace [String, nil] default if resource is missing namespace
78
+ # @raise [K8s::Error::UndefinedResource]
79
+ # @return [K8s::ResourceClient]
80
+ def client_for_resource(resource, namespace: nil)
81
+ unless @api_version == resource.apiVersion
82
+ raise K8s::Error::UndefinedResource, "Invalid apiVersion=#{resource.apiVersion} for #{@api_version} client"
83
+ end
84
+
85
+ found_resource = api_resources.find{ |api_resource| api_resource.kind == resource.kind }
86
+ found_resource ||= api_resources!.find{ |api_resource| api_resource.kind == resource.kind }
87
+ raise K8s::Error::UndefinedResource, "Unknown resource kind=#{resource.kind} for #{@api_version}" unless found_resource
88
+
89
+ ResourceClient.new(@transport, self, found_resource, namespace: resource.metadata.namespace || namespace)
90
+ end
91
+
92
+ # TODO: skip non-namespaced resources if namespace is given, or ignore namespace?
93
+ #
94
+ # @param namespace [String, nil]
95
+ # @return [Array<K8s::ResourceClient>]
96
+ def resources(namespace: nil)
97
+ api_resources.map{ |api_resource|
98
+ ResourceClient.new(@transport, self, api_resource,
99
+ namespace: namespace)
100
+ }
101
+ end
102
+
103
+ # Pipeline list requests for multiple resource types.
104
+ #
105
+ # Returns flattened array with mixed resource kinds.
106
+ #
107
+ # @param resources [Array<K8s::ResourceClient>] default is all listable resources for api
108
+ # @param options @see [K8s::ResourceClient#list]
109
+ # @return [Array<K8s::Resource>]
110
+ def list_resources(resources = nil, **options)
111
+ resources ||= self.resources.select(&:list?)
112
+
113
+ ResourceClient.list(resources, @transport, **options)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,283 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'base64'
5
+ require 'yajl'
6
+ require 'monitor'
7
+ require 'uri'
8
+
9
+ require 'k8s/util'
10
+
11
+ require 'k8s/api/metav1'
12
+ require 'k8s/api/version'
13
+ require 'k8s/config'
14
+ require 'k8s/logging'
15
+ require 'k8s/api_client'
16
+ require "k8s/error"
17
+ require 'k8s/resource'
18
+ require 'k8s/resource_client'
19
+ require 'k8s/stack'
20
+ require 'k8s/transport'
21
+
22
+ module K8s
23
+ # @param server [String] http/s URL
24
+ # @param options [Hash] @see Transport.new
25
+ # @return [K8s::Client]
26
+ def self.client(server, **options)
27
+ Client.new(Transport.new(server, **options))
28
+ end
29
+
30
+ # Top-level client wrapper.
31
+ # Uses a {Transport} instance to talk to the kube API.
32
+ # Offers access to {APIClient} and {ResourceClient} instances.
33
+ class Client
34
+ # @param config [Phraos::Kube::Config]
35
+ # @param namespace [String] @see #initialize
36
+ # @param options [Hash] @see Transport.config
37
+ # @return [K8s::Client]
38
+ def self.config(config, namespace: nil, **options)
39
+ new(
40
+ Transport.config(config, **options),
41
+ namespace: namespace
42
+ )
43
+ end
44
+
45
+ # An K8s::Client instance from in-cluster config within a kube pod, using the kubernetes service envs and serviceaccount secrets
46
+ # @see K8s::Transport#in_cluster_config
47
+ #
48
+ # @param namespace [String] default namespace for all operations
49
+ # @param options [Hash] options passed to transport, @see Transport#in_cluster_config
50
+ # @return [K8s::Client]
51
+ # @raise [K8s::Error::Config,Errno::ENOENT,Errno::EACCES]
52
+ def self.in_cluster_config(namespace: nil, **options)
53
+ new(Transport.in_cluster_config(**options), namespace: namespace)
54
+ end
55
+
56
+ # Attempts to create a K8s::Client instance automatically using environment variables, existing configuration
57
+ # files or in cluster configuration.
58
+ #
59
+ # Look-up order:
60
+ # - KUBE_TOKEN, KUBE_CA, KUBE_SERVER environment variables
61
+ # - KUBECONFIG environment variable
62
+ # - $HOME/.kube/config file
63
+ # - In cluster configuration
64
+ #
65
+ # Will raise when no means of configuration is available
66
+ #
67
+ # @param options [Hash] default namespace for all operations
68
+ # @raise [K8s::Error::Config,Errno::ENOENT,Errno::EACCES]
69
+ # @return [K8s::Client]
70
+ def self.autoconfig(namespace: nil, **options)
71
+ if ENV.values_at('KUBE_TOKEN', 'KUBE_CA', 'KUBE_SERVER').none? { |v| v.nil? || v.empty? }
72
+ unless Base64.decode64(ENV['KUBE_CA']).match?(/CERTIFICATE/)
73
+ raise ArgumentError, 'KUBE_CA does not seem to be base64 encoded'
74
+ end
75
+
76
+ begin
77
+ token = options[:auth_token] || Base64.strict_decode64(ENV['KUBE_TOKEN'])
78
+ rescue ArgumentError
79
+ raise ArgumentError, 'KUBE_TOKEN does not seem to be base64 encoded'
80
+ end
81
+
82
+ configuration = K8s::Config.build(server: ENV['KUBE_SERVER'], ca: ENV['KUBE_CA'], auth_token: token)
83
+ elsif !ENV['KUBECONFIG'].to_s.empty?
84
+ configuration = K8s::Config.from_kubeconfig_env(ENV['KUBECONFIG'])
85
+ else
86
+ found_config = [
87
+ File.join(Dir.home, '.kube', 'config'),
88
+ '/etc/kubernetes/admin.conf',
89
+ '/etc/kubernetes/kubelet.conf'
90
+ ].find { |f| File.exist?(f) && File.readable?(f) }
91
+
92
+ configuration = K8s::Config.load_file(found_config) if found_config
93
+ end
94
+
95
+ if configuration
96
+ config(configuration, namespace: namespace, **options)
97
+ else
98
+ in_cluster_config(namespace: namespace, **options)
99
+ end
100
+ end
101
+
102
+ include MonitorMixin
103
+
104
+ attr_reader :transport
105
+
106
+ # @param transport [K8s::Transport]
107
+ # @param namespace [String, nil] default namespace for all operations
108
+ def initialize(transport, namespace: nil)
109
+ @transport = transport
110
+ @namespace = namespace
111
+
112
+ @api_clients = {}
113
+ super()
114
+ end
115
+
116
+ # @raise [K8s::Error]
117
+ # @return [K8s::API::Version]
118
+ def version
119
+ @version ||= @transport.version
120
+ end
121
+
122
+ # @param api_version [String] "group/version" or "version" (core)
123
+ # @return [APIClient]
124
+ def api(api_version = 'v1')
125
+ @api_clients[api_version] ||= APIClient.new(@transport, api_version)
126
+ end
127
+
128
+ # Force-update /apis cache.
129
+ # Required if creating new CRDs/apiservices.
130
+ #
131
+ # @return [Array<String>]
132
+ def api_groups!
133
+ synchronize do
134
+ @api_groups = @transport.get(
135
+ '/apis',
136
+ response_class: K8s::API::MetaV1::APIGroupList
137
+ ).groups.flat_map{ |api_group| api_group.versions.map(&:groupVersion) }
138
+
139
+ @api_clients.clear
140
+ end
141
+
142
+ @api_groups
143
+ end
144
+
145
+ # Cached /apis preferred group apiVersions
146
+ # @return [Array<String>]
147
+ def api_groups
148
+ @api_groups || api_groups!
149
+ end
150
+
151
+ # @param api_versions [Array<String>] defaults to all APIs
152
+ # @param prefetch_resources [Boolean] prefetch any missing api_resources for each api_version
153
+ # @param skip_missing [Boolean] return APIClient without api_resources? if 404
154
+ # @return [Array<APIClient>]
155
+ def apis(api_versions = nil, prefetch_resources: false, skip_missing: false)
156
+ api_versions ||= ['v1'] + api_groups
157
+
158
+ if prefetch_resources
159
+ # api groups that are missing their api_resources
160
+ api_paths = api_versions
161
+ .uniq
162
+ .reject{ |api_version| api(api_version).api_resources? }
163
+ .map{ |api_version| APIClient.path(api_version) }
164
+
165
+ # load into APIClient.api_resources=
166
+ begin
167
+ @transport.gets(*api_paths, response_class: K8s::API::MetaV1::APIResourceList, skip_missing: skip_missing).each do |api_resource_list|
168
+ api(api_resource_list.groupVersion).api_resources = api_resource_list.resources if api_resource_list
169
+ end
170
+ rescue K8s::Error::NotFound, K8s::Error::ServiceUnavailable # rubocop:disable Lint/HandleExceptions
171
+ # kubernetes api is in unstable state
172
+ # because this is only performance optimization, better to skip prefetch and move on
173
+ end
174
+ end
175
+
176
+ api_versions.map{ |api_version| api(api_version) }
177
+ end
178
+
179
+ # @param namespace [String, nil]
180
+ # @return [Array<K8s::ResourceClient>]
181
+ def resources(namespace: nil)
182
+ apis(prefetch_resources: true).map { |api|
183
+ begin
184
+ api.resources(namespace: namespace)
185
+ rescue K8s::Error::ServiceUnavailable, K8s::Error::NotFound
186
+ []
187
+ end
188
+ }.flatten
189
+ end
190
+
191
+ # Pipeline list requests for multiple resource types.
192
+ #
193
+ # Returns flattened array with mixed resource kinds.
194
+ #
195
+ # @param resources [Array<K8s::ResourceClient>] default is all listable resources for api
196
+ # @param options @see K8s::ResourceClient#list
197
+ # @return [Array<K8s::Resource>]
198
+ def list_resources(resources = nil, **options)
199
+ cached_clients = @api_clients.size.positive?
200
+ resources ||= self.resources.select(&:list?)
201
+
202
+ begin
203
+ ResourceClient.list(resources, @transport, **options)
204
+ rescue K8s::Error::NotFound
205
+ raise unless cached_clients
206
+
207
+ cached_clients = false
208
+ api_groups!
209
+ retry
210
+ end
211
+ end
212
+
213
+ # @param resource [K8s::Resource]
214
+ # @param namespace [String, nil] default if resource is missing namespace
215
+ # @raise [K8s::Error::NotFound] API Group does not exist
216
+ # @raise [K8s::Error::UndefinedResource]
217
+ # @return [K8s::ResourceClient]
218
+ def client_for_resource(resource, namespace: nil)
219
+ api(resource.apiVersion).client_for_resource(resource, namespace: namespace)
220
+ end
221
+
222
+ # @param resource [K8s::Resource]
223
+ # @return [K8s::Resource]
224
+ def create_resource(resource)
225
+ client_for_resource(resource).create_resource(resource)
226
+ end
227
+
228
+ # @param resource [K8s::Resource]
229
+ # @return [K8s::Resource]
230
+ def get_resource(resource)
231
+ client_for_resource(resource).get_resource(resource)
232
+ end
233
+
234
+ # Returns nils for any resources that do not exist.
235
+ # This includes custom resources that were not yet defined.
236
+ #
237
+ # @param resources [Array<K8s::Resource>]
238
+ # @return [Array<K8s::Resource, nil>] matching resources array 1:1
239
+ def get_resources(resources)
240
+ # prefetch api resources, skip missing APIs
241
+ resource_apis = apis(resources.map(&:apiVersion), prefetch_resources: true, skip_missing: true)
242
+
243
+ # map each resource to excon request options, or nil if resource is not (yet) defined
244
+ requests = resources.zip(resource_apis).map{ |resource, api_client|
245
+ next nil unless api_client.api_resources?
246
+
247
+ resource_client = api_client.client_for_resource(resource)
248
+
249
+ {
250
+ method: 'GET',
251
+ path: resource_client.path(resource.metadata.name, namespace: resource.metadata.namespace),
252
+ response_class: resource_client.resource_class
253
+ }
254
+ }
255
+
256
+ # map non-nil requests to response objects, or nil for nil request options
257
+ Util.compact_map(requests) { |reqs|
258
+ @transport.requests(*reqs, skip_missing: true)
259
+ }
260
+ end
261
+
262
+ # @param resource [K8s::Resource]
263
+ # @return [K8s::Resource]
264
+ def update_resource(resource)
265
+ client_for_resource(resource).update_resource(resource)
266
+ end
267
+
268
+ # @param resource [K8s::Resource]
269
+ # @param options [Hash]
270
+ # @see ResourceClient#delete for options
271
+ # @return [K8s::Resource]
272
+ def delete_resource(resource, **options)
273
+ client_for_resource(resource).delete_resource(resource, **options)
274
+ end
275
+
276
+ # @param resource [K8s::Resource]
277
+ # @param attrs [Hash]
278
+ # @return [K8s::Client]
279
+ def patch_resource(resource, attrs)
280
+ client_for_resource(resource).json_patch(resource.metadata.name, attrs)
281
+ end
282
+ end
283
+ end