k8s-client 0.8.3 → 0.9.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA256:
3
- metadata.gz: e30589d8a941cba5fe15409309b397741c358db599d015b1fa9d45050d1d3c11
4
- data.tar.gz: d1d03c0bf3e3c3d14cbe584a0afa21138cb4db74dac9757b7719260fd7b823a3
2
+ SHA1:
3
+ metadata.gz: f0648161976a34f538f314a5f5ac7509779c1df4
4
+ data.tar.gz: e771af2d29bf02f3abaad33cb4ac6be17854bf07
5
5
  SHA512:
6
- metadata.gz: 125c4ac9d5e805c32ea22ab0a69c99fbe731322563c22dbfd9fc36524a525e48032a9ff3fb19c88af26c38d04b197caef9590a1adffd2a74dc1e4b7c19664a3f
7
- data.tar.gz: eb575b7854be11694d5e58458e291f9bbbbc053b5f6a54bbdd4f5f248cfb505f0a843b9ff876d14c8cd72f4c7995b7da98dd7e4f39a55e46ab2a0da6e4ee020e
6
+ metadata.gz: 1ca57fc419c30157b69392a63df5ca8ef5dd64600124ad2c292f2e58ae4b08a0ab89219e5fb732dd5472cb1d70ebbe22309a168051d88b8cb1e676da807db537
7
+ data.tar.gz: 3591b8d176c5423cb5f2ed78f24feb571e2bf9f33e0b295296097edfb41ba8cd4a4942f2603d917afc995c464f115fb3dae690b59ed39bbe3937fe84e83c6362
@@ -8,7 +8,8 @@ rvm:
8
8
  matrix:
9
9
  allow_failures:
10
10
  - rvm: ruby-head
11
- before_install: gem install bundler -v 1.16.2
11
+ before_install: gem install bundler
12
+ script: bundle exec rspec spec/
12
13
  deploy:
13
14
  provider: rubygems
14
15
  gem: k8s-client
@@ -313,8 +313,8 @@ options.delete_resources.each do |resource|
313
313
  resource = client.delete_resource(resource)
314
314
 
315
315
  logger.info "Deleted #{resource.apiVersion} resource #{resource.kind} #{resource.metadata.name} in namespace #{resource.metadata.namespace}:\n#{JSON.pretty_generate(resource)}"
316
- rescue K8s::Error::NotFound => exc
317
- logger.info "Skip #{resource.apiVersion} resource #{resource.kind} #{resource.metadata.name} in namespace #{resource.metadata.namespace}: #{exc}"
316
+ rescue K8s::Error::NotFound => e
317
+ logger.info "Skip #{resource.apiVersion} resource #{resource.kind} #{resource.metadata.name} in namespace #{resource.metadata.namespace}: #{e}"
318
318
  end
319
319
  end
320
320
 
@@ -25,13 +25,14 @@ Gem::Specification.new do |spec|
25
25
 
26
26
  spec.add_runtime_dependency "excon", "~> 0.62.0"
27
27
  spec.add_runtime_dependency "dry-struct", "~> 0.5.0"
28
+ spec.add_runtime_dependency "dry-types", "~> 0.13.0"
28
29
  spec.add_runtime_dependency "recursive-open-struct", "~> 1.1.0"
29
30
  spec.add_runtime_dependency 'hashdiff', '~> 0.3.7'
30
31
  spec.add_runtime_dependency 'jsonpath', '~> 0.9.5'
31
32
  spec.add_runtime_dependency 'yajl-ruby', '~> 1.4.0'
32
33
  spec.add_runtime_dependency "yaml-safe_load_stream", "~> 0.1"
33
34
 
34
- spec.add_development_dependency "bundler", "~> 1.16"
35
+ spec.add_development_dependency "bundler", ">= 1.17", "< 3.0"
35
36
  spec.add_development_dependency "rake", "~> 10.0"
36
37
  spec.add_development_dependency "rspec", "~> 3.7"
37
38
  spec.add_development_dependency "webmock", "~> 3.4.2"
@@ -23,7 +23,7 @@ module K8s
23
23
  end
24
24
 
25
25
  # @return [String]
26
- def to_json
26
+ def to_json(*_args)
27
27
  to_hash.to_json
28
28
  end
29
29
  end
@@ -12,8 +12,8 @@ module K8s
12
12
  attribute :version, Types::Strict::String.optional.default(nil)
13
13
  attribute :kind, Types::Strict::String
14
14
  attribute :verbs, Types::Strict::Array.of(Types::Strict::String)
15
- attribute :shortNames, Types::Strict::Array.of(Types::Strict::String).optional.default([])
16
- attribute :categories, Types::Strict::Array.of(Types::Strict::String).optional.default([])
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
17
  end
18
18
 
19
19
  # @see https://godoc.org/k8s.io/apimachinery/pkg/apis/meta/v1#APIResourceList
@@ -68,7 +68,17 @@ module K8s
68
68
  # @return [K8s::Client]
69
69
  def self.autoconfig(namespace: nil, **options)
70
70
  if ENV.values_at('KUBE_TOKEN', 'KUBE_CA', 'KUBE_SERVER').none? { |v| v.nil? || v.empty? }
71
- configuration = K8s::Config.build(server: ENV['KUBE_SERVER'], ca: ENV['KUBE_CA'], auth_token: options[:auth_token] || ENV['KUBE_TOKEN'])
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)
72
82
  elsif !ENV['KUBECONFIG'].to_s.empty?
73
83
  configuration = K8s::Config.from_kubeconfig_env(ENV['KUBECONFIG'])
74
84
  elsif File.exist?(File.join(Dir.home, '.kube', 'config'))
@@ -3,6 +3,6 @@
3
3
  module K8s
4
4
  class Client
5
5
  # Updated on releases using semver.
6
- VERSION = "0.8.3"
6
+ VERSION = "0.9.0"
7
7
  end
8
8
  end
@@ -49,6 +49,14 @@ module K8s
49
49
  attribute :config, Types::Strict::Hash
50
50
  end
51
51
 
52
+ # structured user exec
53
+ class UserExec < ConfigStruct
54
+ attribute :command, Types::String
55
+ attribute :apiVersion, Types::String
56
+ attribute :env, Types::Strict::Array.of(Types::Hash).optional.default(nil)
57
+ attribute :args, Types::Strict::Array.of(Types::String).optional.default(nil)
58
+ end
59
+
52
60
  # structured user
53
61
  class User < ConfigStruct
54
62
  attribute :client_certificate, Types::String.optional.default(nil)
@@ -63,7 +71,7 @@ module K8s
63
71
  attribute :username, Types::String.optional.default(nil)
64
72
  attribute :password, Types::String.optional.default(nil)
65
73
  attribute :auth_provider, UserAuthProvider.optional.default(nil)
66
- attribute :exec, Types::Strict::Hash.optional.default(nil)
74
+ attribute :exec, UserExec.optional.default(nil)
67
75
  attribute :extensions, Types::Strict::Array.optional.default(nil)
68
76
  end
69
77
 
@@ -103,19 +111,18 @@ module K8s
103
111
  # @param path [String]
104
112
  # @return [K8s::Config]
105
113
  def self.load_file(path)
106
- new(YAML.safe_load(File.read(path), [Time, DateTime, Date]))
114
+ new(YAML.safe_load(File.read(File.expand_path(path)), [Time, DateTime, Date], [], true))
107
115
  end
108
116
 
109
117
  # Loads configuration files listed in KUBE_CONFIG environment variable and
110
- # merged using the configuration merge rules, @see K8s::Config.merge
118
+ # merge using the configuration merge rules, @see K8s::Config.merge
111
119
  #
112
120
  # @param kubeconfig [String] by default read from ENV['KUBECONFIG']
113
121
  def self.from_kubeconfig_env(kubeconfig = nil)
114
122
  kubeconfig ||= ENV.fetch('KUBECONFIG', '')
115
- return if kubeconfig.empty?
123
+ raise ArgumentError, "KUBECONFIG not set" if kubeconfig.empty?
116
124
 
117
125
  paths = kubeconfig.split(/(?!\\):/)
118
- return load_file(paths.first) if paths.size == 1
119
126
 
120
127
  paths.inject(load_file(paths.shift)) do |memo, other_cfg|
121
128
  memo.merge(load_file(other_cfg))
@@ -125,23 +132,17 @@ module K8s
125
132
  # Build a minimal configuration from at least a server address, server certificate authority data and an access token.
126
133
  #
127
134
  # @param server [String] kubernetes server address
128
- # @param ca [String] server certificate authority data
129
- # @param token [String] access token (optionally base64 encoded)
135
+ # @param ca [String] server certificate authority data (base64 encoded)
136
+ # @param token [String] access token
130
137
  # @param cluster_name [String] cluster name
131
138
  # @param user [String] user name
132
139
  # @param context [String] context name
133
140
  # @param options [Hash] (see #initialize)
134
141
  def self.build(server:, ca:, auth_token:, cluster_name: 'kubernetes', user: 'k8s-client', context: 'k8s-client', **options)
135
- begin
136
- decoded_token = Base64.strict_decode64(auth_token)
137
- rescue ArgumentError
138
- decoded_token = nil
139
- end
140
-
141
142
  new(
142
143
  {
143
144
  clusters: [{ name: cluster_name, cluster: { server: server, certificate_authority_data: ca } }],
144
- users: [{ name: user, user: { token: decoded_token || auth_token } }],
145
+ users: [{ name: user, user: { token: auth_token } }],
145
146
  contexts: [{ name: context, context: { cluster: cluster_name, user: user } }],
146
147
  current_context: context
147
148
  }.merge(options)
@@ -174,7 +175,7 @@ module K8s
174
175
  when NilClass
175
176
  new_value
176
177
  else
177
- STDERR.puts "key is #{key} old val is #{old_value.inspect} and new val is #{new_value.inspect}"
178
+ warn "key is #{key} old val is #{old_value.inspect} and new val is #{new_value.inspect}"
178
179
  old_value
179
180
  end
180
181
  end
@@ -184,10 +185,13 @@ module K8s
184
185
  end
185
186
 
186
187
  # @param name [String]
187
- # TODO: raise error if not found
188
+ # @raise [K8s::Error::Configuration]
188
189
  # @return [K8s::Config::Context]
189
190
  def context(name = current_context)
190
- contexts.find{ |context| context.name == name }.context
191
+ found = contexts.find{ |context| context.name == name }
192
+ raise K8s::Error::Configuration, "context not found: #{name.inspect}" unless found
193
+
194
+ found.context
191
195
  end
192
196
 
193
197
  # @param name [String]
@@ -163,10 +163,10 @@ module K8s
163
163
  logger.info "Delete resource #{resource.apiVersion}:#{resource.kind}/#{resource.metadata.name} in namespace #{resource.metadata.namespace}"
164
164
  begin
165
165
  client.delete_resource(resource, propagationPolicy: 'Background')
166
- rescue K8s::Error::NotFound => ex
166
+ rescue K8s::Error::NotFound => e
167
167
  # assume aliased objects in multiple API groups, like for Deployments
168
168
  # alternatively, a custom resource whose definition was already deleted earlier
169
- logger.debug { "Ignoring #{ex} : #{ex.message}" }
169
+ logger.debug { "Ignoring #{e} : #{e.message}" }
170
170
  end
171
171
  end
172
172
  end
@@ -75,14 +75,15 @@ module K8s
75
75
  options[:auth_token] = token
76
76
  elsif config.user.auth_provider && auth_provider = config.user.auth_provider.config
77
77
  logger.debug "Using config with .user.auth-provider.name=#{config.user.auth_provider.name}"
78
-
79
- auth_data = `#{auth_provider['cmd-path']} #{auth_provider['cmd-args']}`.strip
80
- if auth_provider['token-key']
81
- json_path = JsonPath.new(auth_provider['token-key'][1...-1])
82
- options[:auth_token] = json_path.first(auth_data)
83
- else
84
- options[:auth_token] = auth_data
85
- end
78
+ options[:auth_token] = token_from_auth_provider(auth_provider)
79
+ elsif exec_conf = config.user.exec
80
+ logger.debug "Using config with .user.exec.command=#{exec_conf.command}"
81
+ options[:auth_token] = token_from_exec(exec_conf)
82
+ elsif config.user.username && config.user.password
83
+ logger.debug "Using config with .user.password=..."
84
+
85
+ options[:auth_username] = config.user.username
86
+ options[:auth_password] = config.user.password
86
87
  end
87
88
 
88
89
  logger.info "Using config with server=#{server}"
@@ -90,6 +91,35 @@ module K8s
90
91
  new(server, **options, **overrides)
91
92
  end
92
93
 
94
+ # @param auth_provider [K8s::Config::UserAuthProvider]
95
+ # @return [String]
96
+ def self.token_from_auth_provider(auth_provider)
97
+ auth_data = `#{auth_provider['cmd-path']} #{auth_provider['cmd-args']}`.strip
98
+ if auth_provider['token-key']
99
+ json_path = JsonPath.new(auth_provider['token-key'][1...-1])
100
+ json_path.first(auth_data)
101
+ else
102
+ auth_data
103
+ end
104
+ end
105
+
106
+ # @param exec_conf [K8s::Config::UserExec]
107
+ # @return [String]
108
+ def self.token_from_exec(exec_conf)
109
+ cmd = [exec_conf.command]
110
+ cmd += exec_conf.args if exec_conf.args
111
+ orig_env = ENV.to_h
112
+ if envs = exec_conf.env
113
+ envs.each do |env|
114
+ ENV[env['name']] = env['value']
115
+ end
116
+ end
117
+ auth_json = `#{cmd.join(' ')}`.strip
118
+ ENV.replace(orig_env)
119
+
120
+ JSON.parse(auth_json).dig('status', 'token')
121
+ end
122
+
93
123
  # In-cluster config within a kube pod, using the kubernetes service envs and serviceaccount secrets
94
124
  #
95
125
  # @param options [Hash] see #new
@@ -116,10 +146,14 @@ module K8s
116
146
 
117
147
  # @param server [String] URL with protocol://host:port - any /path is ignored
118
148
  # @param auth_token [String] optional Authorization: Bearer token
149
+ # @param auth_username [String] optional Basic authentication username
150
+ # @param auth_password [String] optional Basic authentication password
119
151
  # @param options [Hash] @see Excon.new
120
- def initialize(server, auth_token: nil, **options)
152
+ def initialize(server, auth_token: nil, auth_username: nil, auth_password: nil, **options)
121
153
  @server = server
122
154
  @auth_token = auth_token
155
+ @auth_username = auth_username
156
+ @auth_password = auth_password
123
157
  @options = options
124
158
 
125
159
  logger! progname: @server
@@ -127,7 +161,12 @@ module K8s
127
161
 
128
162
  # @return [Excon::Connection]
129
163
  def excon
130
- @excon ||= Excon.new(
164
+ @excon ||= build_excon
165
+ end
166
+
167
+ # @return [Excon::Connection]
168
+ def build_excon
169
+ Excon.new(
131
170
  @server,
132
171
  persistent: true,
133
172
  middlewares: EXCON_MIDDLEWARES,
@@ -151,6 +190,8 @@ module K8s
151
190
 
152
191
  if @auth_token
153
192
  options[:headers]['Authorization'] = "Bearer #{@auth_token}"
193
+ elsif @auth_username && @auth_password
194
+ options[:headers]['Authorization'] = "Basic #{Base64.strict_encode64("#{@auth_username}:#{@auth_password}")}"
154
195
  end
155
196
 
156
197
  if request_object
@@ -236,12 +277,13 @@ module K8s
236
277
  excon_options = request_options(**options)
237
278
 
238
279
  start = Time.now
239
- response = excon.request(**excon_options)
280
+ excon_client = options[:response_block] ? build_excon : excon
281
+ response = excon_client.request(**excon_options)
240
282
  t = Time.now - start
241
283
 
242
284
  obj = options[:response_block] ? {} : parse_response(response, options, response_class: response_class)
243
- rescue K8s::Error::API => exc
244
- logger.warn { "#{format_request(options)} => HTTP #{exc.code} #{exc.reason} in #{'%.3f' % t}s" }
285
+ rescue K8s::Error::API => e
286
+ logger.warn { "#{format_request(options)} => HTTP #{e.code} #{e.reason} in #{'%.3f' % t}s" }
245
287
  logger.debug { "Request: #{excon_options[:body]}" } if excon_options[:body]
246
288
  logger.debug { "Response: #{response.body}" }
247
289
  raise
@@ -281,17 +323,17 @@ module K8s
281
323
  raise unless skip_forbidden
282
324
 
283
325
  nil
284
- rescue K8s::Error::ServiceUnavailable => exc
326
+ rescue K8s::Error::ServiceUnavailable => e
285
327
  raise unless retry_errors
286
328
 
287
- logger.warn { "Retry #{format_request(request_options)} => HTTP #{exc.code} #{exc.reason} in #{'%.3f' % t}s" }
329
+ logger.warn { "Retry #{format_request(request_options)} => HTTP #{e.code} #{e.reason} in #{'%.3f' % t}s" }
288
330
 
289
331
  # only retry the failed request, not the entire pipeline
290
332
  request(response_class: response_class, **common_options.merge(request_options))
291
333
  end
292
334
  }
293
- rescue K8s::Error => exc
294
- logger.warn { "[#{options.map{ |o| format_request(o) }.join ', '}] => HTTP #{exc.code} #{exc.reason} in #{'%.3f' % t}s" }
335
+ rescue K8s::Error => e
336
+ logger.warn { "[#{options.map{ |o| format_request(o) }.join ', '}] => HTTP #{e.code} #{e.reason} in #{'%.3f' % t}s" }
295
337
  raise
296
338
  else
297
339
  logger.info { "[#{options.map{ |o| format_request(o) }.join ', '}] => HTTP [#{responses.map(&:status).join ', '}] in #{'%.3f' % t}s" }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: k8s-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.3
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kontena, Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-02-14 00:00:00.000000000 Z
11
+ date: 2019-05-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: excon
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: 0.5.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: dry-types
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.13.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.13.0
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: recursive-open-struct
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -112,16 +126,22 @@ dependencies:
112
126
  name: bundler
113
127
  requirement: !ruby/object:Gem::Requirement
114
128
  requirements:
115
- - - "~>"
129
+ - - ">="
116
130
  - !ruby/object:Gem::Version
117
- version: '1.16'
131
+ version: '1.17'
132
+ - - "<"
133
+ - !ruby/object:Gem::Version
134
+ version: '3.0'
118
135
  type: :development
119
136
  prerelease: false
120
137
  version_requirements: !ruby/object:Gem::Requirement
121
138
  requirements:
122
- - - "~>"
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: '1.17'
142
+ - - "<"
123
143
  - !ruby/object:Gem::Version
124
- version: '1.16'
144
+ version: '3.0'
125
145
  - !ruby/object:Gem::Dependency
126
146
  name: rake
127
147
  requirement: !ruby/object:Gem::Requirement
@@ -239,7 +259,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
239
259
  version: '0'
240
260
  requirements: []
241
261
  rubyforge_project:
242
- rubygems_version: 2.7.7
262
+ rubygems_version: 2.6.14.4
243
263
  signing_key:
244
264
  specification_version: 4
245
265
  summary: Kubernetes client library