k8s-ruby2 0.10.6
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.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/.rubocop.relaxed.yml +197 -0
- data/.rubocop.yml +57 -0
- data/Dockerfile +11 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +109 -0
- data/LICENSE +201 -0
- data/README.md +227 -0
- data/Rakefile +16 -0
- data/docker-compose.yaml +10 -0
- data/k8s-ruby2.gemspec +40 -0
- data/lib/k8s/api/metav1/api_group.rb +26 -0
- data/lib/k8s/api/metav1/api_resource.rb +26 -0
- data/lib/k8s/api/metav1/list.rb +20 -0
- data/lib/k8s/api/metav1/object.rb +53 -0
- data/lib/k8s/api/metav1/status.rb +34 -0
- data/lib/k8s/api/metav1/watch_event.rb +20 -0
- data/lib/k8s/api/metav1.rb +26 -0
- data/lib/k8s/api/version.rb +17 -0
- data/lib/k8s/api.rb +31 -0
- data/lib/k8s/api_client.rb +116 -0
- data/lib/k8s/client.rb +283 -0
- data/lib/k8s/config.rb +212 -0
- data/lib/k8s/error.rb +66 -0
- data/lib/k8s/logging.rb +87 -0
- data/lib/k8s/resource.rb +115 -0
- data/lib/k8s/resource_client.rb +349 -0
- data/lib/k8s/ruby/version.rb +8 -0
- data/lib/k8s/stack.rb +187 -0
- data/lib/k8s/transport.rb +382 -0
- data/lib/k8s/util.rb +139 -0
- data/lib/k8s-ruby.rb +4 -0
- metadata +279 -0
data/lib/k8s/client.rb
ADDED
@@ -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
|
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
|
data/lib/k8s/config.rb
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry-struct'
|
4
|
+
require 'dry-types'
|
5
|
+
require 'base64'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
module K8s
|
9
|
+
# Common struct type for kubeconfigs:
|
10
|
+
#
|
11
|
+
# * converts string keys to symbols
|
12
|
+
# * normalizes foo-bar to foo_bar
|
13
|
+
class ConfigStruct < Dry::Struct
|
14
|
+
transform_keys do |key|
|
15
|
+
case key
|
16
|
+
when String
|
17
|
+
key.tr('-', '_').to_sym
|
18
|
+
else
|
19
|
+
key
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# @see https://godoc.org/k8s.io/client-go/tools/clientcmd/api/v1#Config
|
25
|
+
class Config < ConfigStruct
|
26
|
+
# Common dry-types for config
|
27
|
+
class Types
|
28
|
+
include Dry::Types()
|
29
|
+
end
|
30
|
+
|
31
|
+
# structured cluster
|
32
|
+
class Cluster < ConfigStruct
|
33
|
+
attribute :server, Types::String
|
34
|
+
attribute :insecure_skip_tls_verify,
|
35
|
+
Types::Bool.optional.default(nil, shared: true)
|
36
|
+
attribute :certificate_authority,
|
37
|
+
Types::String.optional.default(nil, shared: true)
|
38
|
+
attribute :certificate_authority_data,
|
39
|
+
Types::String.optional.default(nil, shared: true)
|
40
|
+
attribute :extensions,
|
41
|
+
Types::Strict::Array.optional.default(nil, shared: true)
|
42
|
+
end
|
43
|
+
|
44
|
+
# structured cluster with name
|
45
|
+
class NamedCluster < ConfigStruct
|
46
|
+
attribute :name, Types::String
|
47
|
+
attribute :cluster, Cluster
|
48
|
+
end
|
49
|
+
|
50
|
+
# structured user auth provider
|
51
|
+
class UserAuthProvider < ConfigStruct
|
52
|
+
attribute :name, Types::String
|
53
|
+
attribute :config, Types::Strict::Hash
|
54
|
+
end
|
55
|
+
|
56
|
+
# structured user exec
|
57
|
+
class UserExec < ConfigStruct
|
58
|
+
attribute :command, Types::String
|
59
|
+
attribute :apiVersion, Types::String
|
60
|
+
attribute :env, Types::Strict::Array.of(Types::Hash).optional.default(nil)
|
61
|
+
attribute :args, Types::Strict::Array.of(Types::String).optional.default(nil, shared: true)
|
62
|
+
end
|
63
|
+
|
64
|
+
# structured user
|
65
|
+
class User < ConfigStruct
|
66
|
+
attribute :client_certificate, Types::String.optional.default(nil, shared: true)
|
67
|
+
attribute :client_certificate_data, Types::String.optional.default(nil, shared: true)
|
68
|
+
attribute :client_key, Types::String.optional.default(nil, shared: true)
|
69
|
+
attribute :client_key_data, Types::String.optional.default(nil, shared: true)
|
70
|
+
attribute :token, Types::String.optional.default(nil, shared: true)
|
71
|
+
attribute :tokenFile, Types::String.optional.default(nil, shared: true)
|
72
|
+
attribute :as, Types::String.optional.default(nil, shared: true)
|
73
|
+
attribute :as_groups, Types::Array.of(Types::String).optional.default(nil, shared: true)
|
74
|
+
attribute :as_user_extra, Types::Hash.optional.default(nil, shared: true)
|
75
|
+
attribute :username, Types::String.optional.default(nil, shared: true)
|
76
|
+
attribute :password, Types::String.optional.default(nil, shared: true)
|
77
|
+
attribute :auth_provider, UserAuthProvider.optional.default(nil, shared: true)
|
78
|
+
attribute :exec, UserExec.optional.default(nil, shared: true)
|
79
|
+
attribute :extensions, Types::Strict::Array.optional.default(nil, shared: true)
|
80
|
+
end
|
81
|
+
|
82
|
+
# structured user with name
|
83
|
+
class NamedUser < ConfigStruct
|
84
|
+
attribute :name, Types::String
|
85
|
+
attribute :user, User
|
86
|
+
end
|
87
|
+
|
88
|
+
# structured context
|
89
|
+
#
|
90
|
+
# Referrs to other named User/cluster objects within the same config.
|
91
|
+
class Context < ConfigStruct
|
92
|
+
attribute :cluster, Types::Strict::String
|
93
|
+
attribute :user, Types::Strict::String
|
94
|
+
attribute :namespace, Types::Strict::String.optional.default(nil, shared: true)
|
95
|
+
attribute :extensions, Types::Strict::Array.optional.default(nil, shared: true)
|
96
|
+
end
|
97
|
+
|
98
|
+
# named context
|
99
|
+
class NamedContext < ConfigStruct
|
100
|
+
attribute :name, Types::String
|
101
|
+
attribute :context, Context
|
102
|
+
end
|
103
|
+
|
104
|
+
attribute :kind, Types::Strict::String.optional.default(nil, shared: true)
|
105
|
+
attribute :apiVersion, Types::Strict::String.optional.default(nil, shared: true)
|
106
|
+
attribute :preferences, Types::Strict::Hash.optional.default(proc { {} }, shared: true)
|
107
|
+
attribute :clusters, Types::Strict::Array.of(NamedCluster).optional.default(proc { [] }, shared: true)
|
108
|
+
attribute :users, Types::Strict::Array.of(NamedUser).optional.default(proc { [] }, shared: true)
|
109
|
+
attribute :contexts, Types::Strict::Array.of(NamedContext).optional.default(proc { [] }, shared: true)
|
110
|
+
attribute :current_context, Types::Strict::String.optional.default(nil, shared: true)
|
111
|
+
attribute :extensions, Types::Strict::Array.optional.default(proc { [] }, shared: true)
|
112
|
+
|
113
|
+
# Loads a configuration from a YAML file
|
114
|
+
#
|
115
|
+
# @param path [String]
|
116
|
+
# @return [K8s::Config]
|
117
|
+
def self.load_file(path)
|
118
|
+
new(YAML.safe_load(File.read(File.expand_path(path)), [Time, DateTime, Date], [], true))
|
119
|
+
end
|
120
|
+
|
121
|
+
# Loads configuration files listed in KUBE_CONFIG environment variable and
|
122
|
+
# merge using the configuration merge rules, @see K8s::Config.merge
|
123
|
+
#
|
124
|
+
# @param kubeconfig [String] by default read from ENV['KUBECONFIG']
|
125
|
+
def self.from_kubeconfig_env(kubeconfig = nil)
|
126
|
+
kubeconfig ||= ENV.fetch('KUBECONFIG', '')
|
127
|
+
raise ArgumentError, "KUBECONFIG not set" if kubeconfig.empty?
|
128
|
+
|
129
|
+
paths = kubeconfig.split(/(?!\\):/)
|
130
|
+
|
131
|
+
paths.inject(load_file(paths.shift)) do |memo, other_cfg|
|
132
|
+
memo.merge(load_file(other_cfg))
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Build a minimal configuration from at least a server address, server certificate authority data and an access token.
|
137
|
+
#
|
138
|
+
# @param server [String] kubernetes server address
|
139
|
+
# @param ca [String] server certificate authority data (base64 encoded)
|
140
|
+
# @param token [String] access token
|
141
|
+
# @param cluster_name [String] cluster name
|
142
|
+
# @param user [String] user name
|
143
|
+
# @param context [String] context name
|
144
|
+
# @param options [Hash] (see #initialize)
|
145
|
+
def self.build(server:, ca:, auth_token:, cluster_name: 'kubernetes', user: 'k8s-client', context: 'k8s-client', **options)
|
146
|
+
new(
|
147
|
+
{
|
148
|
+
clusters: [{ name: cluster_name, cluster: { server: server, certificate_authority_data: ca } }],
|
149
|
+
users: [{ name: user, user: { token: auth_token } }],
|
150
|
+
contexts: [{ name: context, context: { cluster: cluster_name, user: user } }],
|
151
|
+
current_context: context
|
152
|
+
}.merge(options)
|
153
|
+
)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Merges configuration according to the rules specified in
|
157
|
+
# https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files
|
158
|
+
#
|
159
|
+
# @param other [Hash, K8s::Config]
|
160
|
+
# @return [K8s::Config]
|
161
|
+
def merge(other)
|
162
|
+
old_attributes = attributes
|
163
|
+
other_attributes = other.is_a?(Hash) ? other : other.attributes
|
164
|
+
|
165
|
+
old_attributes.merge!(other_attributes) do |key, old_value, new_value|
|
166
|
+
case key
|
167
|
+
when :clusters, :contexts, :users
|
168
|
+
old_value + new_value.reject do |new_mapping|
|
169
|
+
old_value.any? { |old_mapping| old_mapping[:name] == new_mapping[:name] }
|
170
|
+
end
|
171
|
+
else
|
172
|
+
case old_value
|
173
|
+
when Array
|
174
|
+
(old_value + new_value).uniq
|
175
|
+
when Hash
|
176
|
+
old_value.merge(new_value) do |_key, inner_old_value, inner_new_value|
|
177
|
+
inner_old_value.nil? ? inner_new_value : inner_old_value
|
178
|
+
end
|
179
|
+
when NilClass
|
180
|
+
new_value
|
181
|
+
else
|
182
|
+
old_value
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
self.class.new(old_attributes)
|
188
|
+
end
|
189
|
+
|
190
|
+
# @param name [String]
|
191
|
+
# @raise [K8s::Error::Configuration]
|
192
|
+
# @return [K8s::Config::Context]
|
193
|
+
def context(name = current_context)
|
194
|
+
found = contexts.find{ |context| context.name == name }
|
195
|
+
raise K8s::Error::Configuration, "context not found: #{name.inspect}" unless found
|
196
|
+
|
197
|
+
found.context
|
198
|
+
end
|
199
|
+
|
200
|
+
# @param name [String]
|
201
|
+
# @return [K8s::Config::Cluster]
|
202
|
+
def cluster(name = context.cluster)
|
203
|
+
clusters.find{ |cluster| cluster.name == name }.cluster
|
204
|
+
end
|
205
|
+
|
206
|
+
# @param name [String]
|
207
|
+
# @return [K8s::Config::User]
|
208
|
+
def user(name = context.user)
|
209
|
+
users.find{ |user| user.name == name }.user
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
data/lib/k8s/error.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module K8s
|
6
|
+
# Top-level class for all errors raised by this gem.
|
7
|
+
class Error < StandardError
|
8
|
+
# Kube API error, related to a HTTP response with a non-2xx code
|
9
|
+
class API < Error
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
attr_reader :method, :path, :code, :reason, :status
|
13
|
+
|
14
|
+
# @param method [Integer] HTTP request method
|
15
|
+
# @param path [Integer] HTTP request path
|
16
|
+
# @param code [Integer] HTTP response code
|
17
|
+
# @param reason [String] HTTP response reason
|
18
|
+
# @param status [K8s::API::MetaV1::Status]
|
19
|
+
def initialize(method, path, code, reason, status = nil)
|
20
|
+
@method = method
|
21
|
+
@path = path
|
22
|
+
@code = code
|
23
|
+
@reason = reason
|
24
|
+
@status = status
|
25
|
+
|
26
|
+
if status
|
27
|
+
super("#{@method} #{@path} => HTTP #{@code} #{@reason}: #{@status.message}")
|
28
|
+
else
|
29
|
+
super("#{@method} #{@path} => HTTP #{@code} #{@reason}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
BadRequest = Class.new(API).freeze
|
35
|
+
Unauthorized = Class.new(API).freeze
|
36
|
+
Forbidden = Class.new(API).freeze
|
37
|
+
NotFound = Class.new(API).freeze
|
38
|
+
MethodNotAllowed = Class.new(API).freeze
|
39
|
+
Conflict = Class.new(API).freeze # XXX: also AlreadyExists?
|
40
|
+
Invalid = Class.new(API).freeze
|
41
|
+
Timeout = Class.new(API).freeze
|
42
|
+
InternalError = Class.new(API).freeze
|
43
|
+
ServiceUnavailable = Class.new(API).freeze
|
44
|
+
ServerTimeout = Class.new(API).freeze
|
45
|
+
|
46
|
+
HTTP_STATUS_ERRORS = {
|
47
|
+
400 => BadRequest,
|
48
|
+
401 => Unauthorized,
|
49
|
+
403 => Forbidden,
|
50
|
+
404 => NotFound,
|
51
|
+
405 => MethodNotAllowed,
|
52
|
+
409 => Conflict,
|
53
|
+
422 => Invalid,
|
54
|
+
429 => Timeout,
|
55
|
+
500 => InternalError,
|
56
|
+
503 => ServiceUnavailable,
|
57
|
+
504 => ServerTimeout
|
58
|
+
}.freeze
|
59
|
+
|
60
|
+
# Attempt to create a ResourceClient for an unknown resource type.
|
61
|
+
# The client cannot construct the correct API URL without having the APIResource definition.
|
62
|
+
UndefinedResource = Class.new(Error)
|
63
|
+
|
64
|
+
Configuration = Class.new(Error)
|
65
|
+
end
|
66
|
+
end
|
data/lib/k8s/logging.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module K8s
|
6
|
+
# Logging utilities
|
7
|
+
#
|
8
|
+
# This provides a per-class Logger that uses the class name as a logging prefix.
|
9
|
+
# Instances can optionally also use logger! to define a per-instance Logger using a custom prefix.
|
10
|
+
module Logging
|
11
|
+
# Default log target
|
12
|
+
LOG_TARGET = $stderr
|
13
|
+
|
14
|
+
# Default log level: show warnings.
|
15
|
+
#
|
16
|
+
# Use K8s::Logging.quiet! to supress warnings.
|
17
|
+
# Note that the K8s::Transport defaults to quiet!
|
18
|
+
LOG_LEVEL = Logger::WARN
|
19
|
+
|
20
|
+
# methods defined on both the global K8s::Logging module, as well as class methods on each class including K8s::Logging
|
21
|
+
module ModuleMethods
|
22
|
+
# global log_level shared across all including classes
|
23
|
+
# @return Logger::*
|
24
|
+
def log_level
|
25
|
+
@log_level
|
26
|
+
end
|
27
|
+
|
28
|
+
# @param level Logger::*
|
29
|
+
def log_level=(level)
|
30
|
+
@log_level = level
|
31
|
+
end
|
32
|
+
|
33
|
+
# Set log_level to Logger::DEBUG
|
34
|
+
def debug!
|
35
|
+
self.log_level = Logger::DEBUG
|
36
|
+
end
|
37
|
+
|
38
|
+
# Set log_level to Logger::INFO
|
39
|
+
def verbose!
|
40
|
+
self.log_level = Logger::INFO
|
41
|
+
end
|
42
|
+
|
43
|
+
# Set log_level to Logger::ERROR, surpressing any warnings logged by default
|
44
|
+
def quiet!
|
45
|
+
self.log_level = Logger::ERROR
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
extend ModuleMethods # global @log_level
|
50
|
+
|
51
|
+
# methods defined on each class including K8s::Logging
|
52
|
+
module ClassMethods
|
53
|
+
# @return [Logger]
|
54
|
+
def logger(target: LOG_TARGET, level: nil)
|
55
|
+
@logger ||= Logger.new(target).tap do |logger|
|
56
|
+
logger.progname = name
|
57
|
+
logger.level = level || log_level || K8s::Logging.log_level || LOG_LEVEL
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# extend class/intance methods for per-class logger
|
63
|
+
def self.included(base)
|
64
|
+
base.extend(ModuleMethods) # per-class @log_level
|
65
|
+
base.extend(ClassMethods)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Use per-instance logger instead of the default per-class logger
|
69
|
+
#
|
70
|
+
# Sets the instance variable returned by #logger
|
71
|
+
#
|
72
|
+
# @return [Logger]
|
73
|
+
def logger!(progname: self.class.name, target: LOG_TARGET, level: nil, debug: false)
|
74
|
+
@logger = Logger.new(target).tap do |logger|
|
75
|
+
level = Logger::DEBUG if debug
|
76
|
+
|
77
|
+
logger.progname = "#{self.class.name}<#{progname}>"
|
78
|
+
logger.level = level || self.class.log_level || K8s::Logging.log_level || LOG_LEVEL
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# @return [Logger]
|
83
|
+
def logger
|
84
|
+
@logger || self.class.logger
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|