k8s-client 0.8.3 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
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