k8s-ruby 0.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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