k8s-client 0.6.4 → 0.7.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
2
  SHA256:
3
- metadata.gz: eba459f35445e6a9712b9c13f4aae1a1e64ee54671c2e79312961814e128a8a0
4
- data.tar.gz: 645af48b165e0d3993f76dee2f4e0cbf81a9df2175ba6b7faabecf88eb22fc11
3
+ metadata.gz: 15012c4385d7320b4026e0da41c3092623ff3b157e3115e4d6a908e56a5af926
4
+ data.tar.gz: 2a8b4a339cff5223c5c7a601b01d60af5313abde8ba42ad9cace8cda0fd6b510
5
5
  SHA512:
6
- metadata.gz: 198809fd3961246fe35ac9f8b31bf986573978e1aa60281b38ba53e610f1620c8cefee834f8e12a6a782ea30c0a2ba61d9b660f10d7f3dd0ca28ae6d81f714f6
7
- data.tar.gz: 244edac8e01d0896b3b6770f9e414393418aaeb3bc3152154c505f6d5288250b46aef9f0323ece7e74c79738ebb05546f2010bbc4ebfa467cee2847499e9f382
6
+ metadata.gz: ce43ce55808589458ce1f6e002fd3687e78adc015c57f200791109fd88e664a9a11c80284cd60514a7822fc89662f7b23617b2802c8b90a99be31c8355ad62cc
7
+ data.tar.gz: bbf4c9f9b66412bf179555a4d8ba3ae807f24a2629709efe8ed2ecbd466eeeca7597571685518afc246cec98e05602a050ba148409f8955114b3de373b1c0e1f
data/.travis.yml CHANGED
@@ -3,6 +3,7 @@ language: ruby
3
3
  rvm:
4
4
  - '2.4'
5
5
  - '2.5'
6
+ - '2.6'
6
7
  - ruby-head
7
8
  matrix:
8
9
  allow_failures:
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/github/kontena/k8s-client/master)
6
6
 
7
7
 
8
- Ruby client library for the Kubernetes (1.11) API
8
+ Ruby client library for the Kubernetes (1.9+) API
9
9
 
10
10
  ## Highlights
11
11
 
@@ -60,7 +60,11 @@ The keyword options are [Excon](https://github.com/excon/excon/) options.
60
60
  #### Client from kubeconfig
61
61
 
62
62
  ```ruby
63
- client = K8s::Client.config(K8s::Config.load_file('~/.kube/config'))
63
+ client = K8s::Client.config(
64
+ K8s::Config.load_file(
65
+ File.expand_path '~/.kube/config'
66
+ )
67
+ )
64
68
  ```
65
69
 
66
70
  #### Supported kubeconfig options
data/k8s-client.gemspec CHANGED
@@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.add_runtime_dependency 'hashdiff', '~> 0.3.7'
31
31
  spec.add_runtime_dependency 'jsonpath', '~> 0.9.5'
32
32
  spec.add_runtime_dependency 'yajl-ruby', '~> 1.4.0'
33
+ spec.add_runtime_dependency "yaml-safe_load_stream", "~> 0.1"
33
34
 
34
35
  spec.add_development_dependency "bundler", "~> 1.16"
35
36
  spec.add_development_dependency "rake", "~> 10.0"
data/lib/k8s/api.rb CHANGED
@@ -19,7 +19,7 @@ module K8s
19
19
  # @param data [Hash]
20
20
  # @return [self]
21
21
  def self.from_json(data)
22
- new(data)
22
+ new(Yajl::Parser.parse(data))
23
23
  end
24
24
 
25
25
  # @return [String]
@@ -55,8 +55,11 @@ module K8s
55
55
  end
56
56
 
57
57
  # @param resource_name [String]
58
+ # @raise [K8s::Error::UndefinedResource]
59
+ # @return [K8s::API::MetaV1::APIResource]
58
60
  def find_api_resource(resource_name)
59
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 }
60
63
  raise K8s::Error::UndefinedResource, "Unknown resource #{resource_name} for #{@api_version}" unless found_resource
61
64
 
62
65
  found_resource
@@ -64,7 +67,7 @@ module K8s
64
67
 
65
68
  # @param resource_name [String]
66
69
  # @param namespace [String, nil]
67
- # @raise [K8s::Error] unknown resource
70
+ # @raise [K8s::Error::UndefinedResource]
68
71
  # @return [K8s::ResourceClient]
69
72
  def resource(resource_name, namespace: nil)
70
73
  ResourceClient.new(@transport, self, find_api_resource(resource_name), namespace: namespace)
@@ -72,7 +75,6 @@ module K8s
72
75
 
73
76
  # @param resource [K8s::Resource]
74
77
  # @param namespace [String, nil] default if resource is missing namespace
75
- # @raise [K8s::Error::NotFound] API Group does not exist
76
78
  # @raise [K8s::Error::UndefinedResource]
77
79
  # @return [K8s::ResourceClient]
78
80
  def client_for_resource(resource, namespace: nil)
@@ -81,10 +83,10 @@ module K8s
81
83
  end
82
84
 
83
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 }
84
87
  raise K8s::Error::UndefinedResource, "Unknown resource kind=#{resource.kind} for #{@api_version}" unless found_resource
85
88
 
86
- ResourceClient.new(@transport, self, found_resource,
87
- namespace: resource.metadata.namespace || namespace)
89
+ ResourceClient.new(@transport, self, found_resource, namespace: resource.metadata.namespace || namespace)
88
90
  end
89
91
 
90
92
  # TODO: skip non-namespaced resources if namespace is given, or ignore namespace?
data/lib/k8s/client.rb CHANGED
@@ -41,15 +41,50 @@ module K8s
41
41
  )
42
42
  end
43
43
 
44
+ # An K8s::Client instance from in-cluster config within a kube pod, using the kubernetes service envs and serviceaccount secrets
45
+ # @see K8s::Transport#in_cluster_config
46
+ #
47
+ # @param namespace [String] default namespace for all operations
48
+ # @return [K8s::Client]
49
+ # @raise [K8s::Error::Config,Errno::ENOENT,Errno::EACCES]
50
+ def self.in_cluster_config(namespace: nil)
51
+ new(Transport.in_cluster_config, namespace: namespace)
52
+ end
53
+
54
+ # Attempts to create a K8s::Client instance automatically using environment variables, existing configuration
55
+ # files or in cluster configuration.
56
+ #
57
+ # Look-up order:
58
+ # - KUBE_TOKEN, KUBE_CA, KUBE_SERVER environment variables
59
+ # - KUBECONFIG environment variable
60
+ # - $HOME/.kube/config file
61
+ # - In cluster configuration
62
+ #
63
+ # Will raise when no means of configuration is available
64
+ #
65
+ # @param options [Hash] default namespace for all operations
66
+ # @raise [K8s::Error::Config,Errno::ENOENT,Errno::EACCES]
44
67
  # @return [K8s::Client]
45
- def self.in_cluster_config
46
- new(Transport.in_cluster_config)
68
+ def self.autoconfig(namespace: nil, **options)
69
+ if ENV.values_at('KUBE_TOKEN', 'KUBE_CA', 'KUBE_SERVER').none? { |v| v.nil? || v.empty? }
70
+ configuration = K8s::Config.build(server: ENV['KUBE_SERVER'], ca: ENV['KUBE_CA'], auth_token: options[:auth_token] || ENV['KUBE_TOKEN'])
71
+ elsif !ENV['KUBECONFIG'].to_s.empty?
72
+ configuration = K8s::Config.from_kubeconfig_env(ENV['KUBECONFIG'], auth_token: options[:auth_token])
73
+ elsif File.exist?(File.join(Dir.home, '.kube', 'config'))
74
+ configuration = K8s::Config.load_file(File.join(Dir.home, '.kube', 'config'))
75
+ end
76
+
77
+ if configuration
78
+ config(configuration, namespace: namespace, **options)
79
+ else
80
+ in_cluster_config(namespace: namespace, **options)
81
+ end
47
82
  end
48
83
 
49
84
  attr_reader :transport
50
85
 
51
86
  # @param transport [K8s::Transport]
52
- # @param namespace [String] default namespace for all operations
87
+ # @param namespace [String, nil] default namespace for all operations
53
88
  def initialize(transport, namespace: nil)
54
89
  @transport = transport
55
90
  @namespace = namespace
@@ -191,6 +226,9 @@ module K8s
191
226
  client_for_resource(resource).delete_resource(resource, **options)
192
227
  end
193
228
 
229
+ # @param resource [K8s::Resource]
230
+ # @param attrs [Hash]
231
+ # @return [K8s::Client]
194
232
  def patch_resource(resource, attrs)
195
233
  client_for_resource(resource).json_patch(resource.metadata.name, attrs)
196
234
  end
@@ -3,6 +3,6 @@
3
3
  module K8s
4
4
  class Client
5
5
  # Updated on releases using semver.
6
- VERSION = "0.6.4"
6
+ VERSION = "0.7.0"
7
7
  end
8
8
  end
data/lib/k8s/config.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'dry-struct'
4
4
  require 'dry-types'
5
+ require 'base64'
5
6
  require 'yaml'
6
7
 
7
8
  module K8s
@@ -90,30 +91,112 @@ module K8s
90
91
 
91
92
  attribute :kind, Types::Strict::String.optional.default(nil)
92
93
  attribute :apiVersion, Types::Strict::String.optional.default(nil)
93
- attribute :preferences, Types::Strict::Hash.optional.default(nil)
94
- attribute :clusters, Types::Strict::Array.of(NamedCluster)
95
- attribute :users, Types::Strict::Array.of(NamedUser)
96
- attribute :contexts, Types::Strict::Array.of(NamedContext)
97
- attribute :current_context, Types::Strict::String
98
- attribute :extensions, Types::Strict::Array.optional.default(nil)
94
+ attribute :preferences, Types::Strict::Hash.optional.default(proc { {} })
95
+ attribute :clusters, Types::Strict::Array.of(NamedCluster).optional.default(proc { [] })
96
+ attribute :users, Types::Strict::Array.of(NamedUser).optional.default(proc { [] })
97
+ attribute :contexts, Types::Strict::Array.of(NamedContext).optional.default(proc { [] })
98
+ attribute :current_context, Types::Strict::String.optional.default(nil)
99
+ attribute :extensions, Types::Strict::Array.optional.default(proc { [] })
99
100
 
101
+ # Loads a configuration from a YAML file
102
+ #
100
103
  # @param path [String]
101
104
  # @return [K8s::Config]
102
105
  def self.load_file(path)
103
- new(YAML.load_file(path))
106
+ new(YAML.safe_load(File.read(path)))
107
+ end
108
+
109
+ # Loads configuration files listed in KUBE_CONFIG environment variable and
110
+ # merged using the configuration merge rules, @see K8s::Config.merge
111
+ #
112
+ # @param kubeconfig [String] by default read from ENV['KUBECONFIG']
113
+ def self.from_kubeconfig_env(kubeconfig = nil)
114
+ kubeconfig ||= ENV.fetch('KUBECONFIG', '')
115
+ return if kubeconfig.empty?
116
+
117
+ paths = kubeconfig.split(/(?!\\):/)
118
+ return load_file(paths.first) if paths.size == 1
119
+
120
+ paths.inject(load_file(paths.shift)) do |memo, other_cfg|
121
+ memo.merge(load_file(other_cfg))
122
+ end
123
+ end
124
+
125
+ # Build a minimal configuration from at least a server address, server certificate authority data and an access token.
126
+ #
127
+ # @param server [String] kubernetes server address
128
+ # @param ca [String] server certificate authority data
129
+ # @param token [String] access token (optionally base64 encoded)
130
+ # @param cluster_name [String] cluster name
131
+ # @param user [String] user name
132
+ # @param context [String] context name
133
+ # @param options [Hash] (see #initialize)
134
+ 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
+ new(
142
+ {
143
+ clusters: [{ name: cluster_name, cluster: { server: server, certificate_authority_data: ca } }],
144
+ users: [{ name: user, user: { token: decoded_token || auth_token } }],
145
+ contexts: [{ name: context, context: { cluster: cluster_name, user: user } }],
146
+ current_context: context
147
+ }.merge(options)
148
+ )
149
+ end
150
+
151
+ # Merges configuration according to the rules specified in
152
+ # https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#merging-kubeconfig-files
153
+ #
154
+ # @param other [Hash, K8s::Config]
155
+ # @return [K8s::Config]
156
+ def merge(other)
157
+ old_attributes = attributes
158
+ other_attributes = other.is_a?(Hash) ? other : other.attributes
159
+
160
+ old_attributes.merge!(other_attributes) do |key, old_value, new_value|
161
+ case key
162
+ when :clusters, :contexts, :users
163
+ old_value + new_value.reject do |new_mapping|
164
+ old_value.any? { |old_mapping| old_mapping[:name] == new_mapping[:name] }
165
+ end
166
+ else
167
+ case old_value
168
+ when Array
169
+ (old_value + new_value).uniq
170
+ when Hash
171
+ old_value.merge(new_value) do |_key, inner_old_value, inner_new_value|
172
+ inner_old_value.nil? ? inner_new_value : inner_old_value
173
+ end
174
+ when NilClass
175
+ new_value
176
+ else
177
+ STDERR.puts "key is #{key} old val is #{old_value.inspect} and new val is #{new_value.inspect}"
178
+ old_value
179
+ end
180
+ end
181
+ end
182
+
183
+ self.class.new(old_attributes)
104
184
  end
105
185
 
186
+ # @param name [String]
106
187
  # TODO: raise error if not found
107
188
  # @return [K8s::Config::Context]
108
189
  def context(name = current_context)
109
190
  contexts.find{ |context| context.name == name }.context
110
191
  end
111
192
 
193
+ # @param name [String]
112
194
  # @return [K8s::Config::Cluster]
113
195
  def cluster(name = context.cluster)
114
196
  clusters.find{ |cluster| cluster.name == name }.cluster
115
197
  end
116
198
 
199
+ # @param name [String]
117
200
  # @return [K8s::Config::User]
118
201
  def user(name = context.user)
119
202
  users.find{ |user| user.name == name }.user
data/lib/k8s/error.rb CHANGED
@@ -60,5 +60,7 @@ module K8s
60
60
  # Attempt to create a ResourceClient for an unknown resource type.
61
61
  # The client cannot construct the correct API URL without having the APIResource definition.
62
62
  UndefinedResource = Class.new(Error)
63
+
64
+ Configuration = Class.new(Error)
63
65
  end
64
66
  end
data/lib/k8s/resource.rb CHANGED
@@ -4,6 +4,7 @@ require 'deep_merge'
4
4
  require 'recursive-open-struct'
5
5
  require 'hashdiff'
6
6
  require 'forwardable'
7
+ require 'yaml/safe_load_stream'
7
8
 
8
9
  module K8s
9
10
  # generic untyped resource
@@ -11,16 +12,18 @@ module K8s
11
12
  extend Forwardable
12
13
  include Comparable
13
14
 
14
- # @param data [Hash]
15
+ using YAMLSafeLoadStream
16
+
17
+ # @param data [String]
15
18
  # @return [self]
16
19
  def self.from_json(data)
17
- new(data)
20
+ new(Yajl::Parser.parse(data))
18
21
  end
19
22
 
20
23
  # @param filename [String] file path
21
24
  # @return [K8s::Resource]
22
25
  def self.from_file(filename)
23
- new(YAML.load_file(filename))
26
+ new(YAML.safe_load(File.read(filename), [], [], true, filename))
24
27
  end
25
28
 
26
29
  # @param path [String] file path
@@ -32,11 +35,13 @@ module K8s
32
35
  # recurse
33
36
  Dir.glob("#{path}/*.{yml,yaml}").sort.map { |dir| from_files(dir) }.flatten
34
37
  else
35
- ::YAML.load_stream(File.read(path), path).map{ |doc| new(doc) }
38
+ YAML.safe_load_stream(File.read(path), path).map{ |doc| new(doc) }
36
39
  end
37
40
  end
38
41
 
39
42
  # @param hash [Hash]
43
+ # @param recurse_over_arrays [Boolean]
44
+ # @param options [Hash] see RecursiveOpenStruct#initialize
40
45
  def initialize(hash, recurse_over_arrays: true, **options)
41
46
  super(hash,
42
47
  recurse_over_arrays: recurse_over_arrays,
@@ -44,6 +49,7 @@ module K8s
44
49
  )
45
50
  end
46
51
 
52
+ # @param options [Hash] see Hash#to_json
47
53
  # @return [String]
48
54
  def to_json(**options)
49
55
  to_hash.to_json(**options)
@@ -69,34 +75,43 @@ module K8s
69
75
  self.class.new(h)
70
76
  end
71
77
 
78
+ # @return [String]
72
79
  def checksum
73
80
  @checksum ||= Digest::MD5.hexdigest(Marshal.dump(to_hash))
74
81
  end
75
82
 
83
+ # @param attrs [Hash]
84
+ # @param config_annotation [String]
85
+ # @return [Hash]
76
86
  def merge_patch_ops(attrs, config_annotation)
77
87
  Util.json_patch(current_config(config_annotation), stringify_hash(attrs))
78
88
  end
79
89
 
80
90
  # Gets the existing resources (on kube api) configuration, an empty hash if not present
81
91
  #
92
+ # @param config_annotation [String]
82
93
  # @return [Hash]
83
94
  def current_config(config_annotation)
84
95
  current_cfg = metadata.annotations&.dig(config_annotation)
85
96
  return {} unless current_cfg
86
97
 
87
- current_hash = JSON.parse(current_cfg)
98
+ current_hash = Yajl::Parser.parse(current_cfg)
88
99
  # kubectl adds empty metadata.namespace, let's fix it
89
100
  current_hash['metadata'].delete('namespace') if current_hash.dig('metadata', 'namespace').to_s.empty?
90
101
 
91
102
  current_hash
92
103
  end
93
104
 
105
+ # @param config_annotation [String]
106
+ # @return [Boolean]
94
107
  def can_patch?(config_annotation)
95
108
  !!metadata.annotations&.dig(config_annotation)
96
109
  end
97
110
 
111
+ # @param hash [Hash]
112
+ # @return [Hash]
98
113
  def stringify_hash(hash)
99
- JSON.parse(JSON.dump(hash))
114
+ Yajl::Parser.parse(JSON.dump(hash))
100
115
  end
101
116
  end
102
117
  end
@@ -7,8 +7,8 @@ module K8s
7
7
  class ResourceClient
8
8
  # Common helpers used in both class/instance methods
9
9
  module Utils
10
- # @param selector [nil, String, Hash{String => String}]
11
- # @return [nil, String]
10
+ # @param selector [NilClass, String, Hash{String => String}]
11
+ # @return [NilClass, String]
12
12
  def selector_query(selector)
13
13
  case selector
14
14
  when nil
@@ -23,7 +23,7 @@ module K8s
23
23
  end
24
24
 
25
25
  # @param options [Hash]
26
- # @return [Hash, nil]
26
+ # @return [Hash, NilClass]
27
27
  def make_query(options)
28
28
  query = options.compact
29
29
 
@@ -66,6 +66,7 @@ module K8s
66
66
  # @param api_client [K8s::APIClient]
67
67
  # @param api_resource [K8s::API::MetaV1::APIResource]
68
68
  # @param namespace [String]
69
+ # @param resource_class [Class]
69
70
  def initialize(transport, api_client, api_resource, namespace: nil, resource_class: K8s::Resource)
70
71
  @transport = transport
71
72
  @api_client = api_client
@@ -115,11 +116,14 @@ module K8s
115
116
  # @return [class] K8s::Resource
116
117
  attr_reader :resource_class
117
118
 
118
- # @return [Bool]
119
+ # @return [Boolean]
119
120
  def namespaced?
120
121
  !!@api_resource.namespaced
121
122
  end
122
123
 
124
+ # @param name [NilClass, String]
125
+ # @param subresource [String, NilClass]
126
+ # @param namespace [String, NilClass]
123
127
  # @return [String]
124
128
  def path(name = nil, subresource: @subresource, namespace: @namespace)
125
129
  namespace_part = namespace ? ['namespaces', namespace] : []
@@ -138,8 +142,8 @@ module K8s
138
142
  @api_resource.verbs.include? 'create'
139
143
  end
140
144
 
141
- # @param resource [resource_class] with metadata.namespace and metadata.name set
142
- # @return [resource_class]
145
+ # @param resource [#metadata] with metadata.namespace and metadata.name set
146
+ # @return [Object] instance of resource_class
143
147
  def create_resource(resource)
144
148
  @transport.request(
145
149
  method: 'POST',
@@ -154,7 +158,9 @@ module K8s
154
158
  @api_resource.verbs.include? 'get'
155
159
  end
156
160
 
157
- # @return [resource_class]
161
+ # @param name [String]
162
+ # @param namespace [String, NilClass]
163
+ # @return [Object] instance of resource_class
158
164
  def get(name, namespace: @namespace)
159
165
  @transport.request(
160
166
  method: 'GET',
@@ -164,7 +170,7 @@ module K8s
164
170
  end
165
171
 
166
172
  # @param resource [resource_class]
167
- # @return [resource_class]
173
+ # @return [Object] instance of resource_class
168
174
  def get_resource(resource)
169
175
  @transport.request(
170
176
  method: 'GET',
@@ -179,7 +185,7 @@ module K8s
179
185
  end
180
186
 
181
187
  # @param list [K8s::API::MetaV1::List]
182
- # @return [Array<resource_class>]
188
+ # @return [Array<Object>] array of instances of resource_class
183
189
  def process_list(list)
184
190
  list.items.map { |item|
185
191
  # list items omit kind/apiVersion
@@ -190,7 +196,7 @@ module K8s
190
196
  # @param labelSelector [nil, String, Hash{String => String}]
191
197
  # @param fieldSelector [nil, String, Hash{String => String}]
192
198
  # @param namespace [nil, String]
193
- # @return [Array<resource_class>]
199
+ # @return [Array<Object>] array of instances of resource_class
194
200
  def list(labelSelector: nil, fieldSelector: nil, namespace: @namespace)
195
201
  list = meta_list(labelSelector: labelSelector, fieldSelector: fieldSelector, namespace: namespace)
196
202
  process_list(list)
@@ -243,13 +249,13 @@ module K8s
243
249
  )
244
250
  end
245
251
 
246
- # @return [Bool]
252
+ # @return [Boolean]
247
253
  def update?
248
254
  @api_resource.verbs.include? 'update'
249
255
  end
250
256
 
251
- # @param resource [resource_class] with metadata.resourceVersion set
252
- # @return [resource_class]
257
+ # @param resource [#metadata] with metadata.resourceVersion set
258
+ # @return [Object] instance of resource_class
253
259
  def update_resource(resource)
254
260
  @transport.request(
255
261
  method: 'PUT',
@@ -259,16 +265,16 @@ module K8s
259
265
  )
260
266
  end
261
267
 
262
- # @return [Bool]
268
+ # @return [Boolean]
263
269
  def patch?
264
270
  @api_resource.verbs.include? 'patch'
265
271
  end
266
272
 
267
273
  # @param name [String]
268
- # @param obj [Object] supporting to_json
269
- # @param namespace [String]
274
+ # @param obj [#to_json]
275
+ # @param namespace [String, nil]
270
276
  # @param strategic_merge [Boolean] use kube Strategic Merge Patch instead of standard Merge Patch (arrays of objects are merged by name)
271
- # @return [resource_class]
277
+ # @return [Object] instance of resource_class
272
278
  def merge_patch(name, obj, namespace: @namespace, strategic_merge: true)
273
279
  @transport.request(
274
280
  method: 'PATCH',
@@ -281,8 +287,8 @@ module K8s
281
287
 
282
288
  # @param name [String]
283
289
  # @param ops [Hash] json-patch operations
284
- # @param namespace [String]
285
- # @return [resource_class]
290
+ # @param namespace [String, nil]
291
+ # @return [Object] instance of resource_class
286
292
  def json_patch(name, ops, namespace: @namespace)
287
293
  @transport.request(
288
294
  method: 'PATCH',
@@ -293,14 +299,14 @@ module K8s
293
299
  )
294
300
  end
295
301
 
296
- # @return [Bool]
302
+ # @return [Boolean]
297
303
  def delete?
298
304
  @api_resource.verbs.include? 'delete'
299
305
  end
300
306
 
301
307
  # @param name [String]
302
- # @param namespace [String]
303
- # @param propagationPolicy [String] The propagationPolicy to use for the API call. Possible values include “Orphan”, “Foreground”, or “Background”
308
+ # @param namespace [String, nil]
309
+ # @param propagationPolicy [String, nil] The propagationPolicy to use for the API call. Possible values include “Orphan”, “Foreground”, or “Background”
304
310
  # @return [K8s::API::MetaV1::Status]
305
311
  def delete(name, namespace: @namespace, propagationPolicy: nil)
306
312
  @transport.request(
@@ -313,10 +319,11 @@ module K8s
313
319
  )
314
320
  end
315
321
 
316
- # @param namespace [String]
322
+ # @param namespace [String, nil]
317
323
  # @param labelSelector [nil, String, Hash{String => String}]
318
324
  # @param fieldSelector [nil, String, Hash{String => String}]
319
- # @return [K8s::API::MetaV1::Status]
325
+ # @param propagationPolicy [String, nil]
326
+ # @return [Array<Object>] array of instances of resource_class
320
327
  def delete_collection(namespace: @namespace, labelSelector: nil, fieldSelector: nil, propagationPolicy: nil)
321
328
  list = @transport.request(
322
329
  method: 'DELETE',
data/lib/k8s/stack.rb CHANGED
@@ -25,6 +25,7 @@ module K8s
25
25
 
26
26
  # @param name [String] unique name for stack
27
27
  # @param path [String] load resources from YAML files
28
+ # @param options [Hash] see Stack#initialize
28
29
  # @return [K8s::Stack]
29
30
  def self.load(name, path, **options)
30
31
  resources = K8s::Resource.from_files(path)
@@ -35,6 +36,8 @@ module K8s
35
36
  # @param path [String] load resources from YAML files
36
37
  # @param client [K8s::Client] apply using client
37
38
  # @param prune [Boolean] delete old resources
39
+ # @param options [Hash] see Stack#initialize
40
+ # @return [K8s::Stack]
38
41
  def self.apply(name, path, client, prune: true, **options)
39
42
  load(name, path, **options).apply(client, prune: prune)
40
43
  end
@@ -49,6 +52,12 @@ module K8s
49
52
 
50
53
  attr_reader :name, :resources
51
54
 
55
+ # @param name [String]
56
+ # @param resources [Array<K8s::Resource>]
57
+ # @param debug [Boolean]
58
+ # @param label [String]
59
+ # @param checksum_annotation [String]
60
+ # @param last_config_annotation [String]
52
61
  def initialize(name, resources = [], debug: false, label: self.class::LABEL, checksum_annotation: self.class::CHECKSUM_ANNOTATION, last_configuration_annotation: self.class::LAST_CONFIG_ANNOTATION)
53
62
  @name = name
54
63
  @resources = resources
@@ -83,6 +92,7 @@ module K8s
83
92
  end
84
93
  # rubocop:enable Lint/UnusedMethodArgument
85
94
 
95
+ # @param client [K8s::Client]
86
96
  # @return [Array<K8s::Resource>]
87
97
  def apply(client, prune: true)
88
98
  server_resources = client.get_resources(resources)
@@ -110,15 +120,22 @@ module K8s
110
120
  end
111
121
 
112
122
  # key MUST NOT include resource.apiVersion: the same kind can be aliased in different APIs
123
+ # @param resource [K8s::Resource]
124
+ # @return [K8s::Resource]
113
125
  def keep_resource!(resource)
114
126
  @keep_resources["#{resource.kind}:#{resource.metadata.name}@#{resource.metadata.namespace}"] = resource.metadata.annotations[@checksum_annotation]
115
127
  end
116
128
 
129
+ # @param resource [K8s::Resource]
130
+ # @return [Boolean]
117
131
  def keep_resource?(resource)
118
132
  @keep_resources["#{resource.kind}:#{resource.metadata.name}@#{resource.metadata.namespace}"] == resource.metadata.annotations[@checksum_annotation]
119
133
  end
120
134
 
121
135
  # Delete all stack resources that were not applied
136
+ # @param client [K8s::Client]
137
+ # @param keep_resources [NilClass, Boolean]
138
+ # @param skip_forbidden [Boolean]
122
139
  def prune(client, keep_resources:, skip_forbidden: true)
123
140
  # using skip_forbidden: assume we can't create resource types that we are forbidden to list, so we don't need to prune them either
124
141
  client.list_resources(labelSelector: { @label => name }, skip_forbidden: skip_forbidden).sort do |a, b|
@@ -156,6 +173,7 @@ module K8s
156
173
  end
157
174
 
158
175
  # Delete all stack resources
176
+ # @param client [K8s::Client]
159
177
  def delete(client)
160
178
  prune(client, keep_resources: false)
161
179
  end
data/lib/k8s/transport.rb CHANGED
@@ -92,16 +92,23 @@ module K8s
92
92
 
93
93
  # In-cluster config within a kube pod, using the kubernetes service envs and serviceaccount secrets
94
94
  #
95
+ # @param options [Hash] see #new
95
96
  # @return [K8s::Transport]
96
- def self.in_cluster_config
97
- host = ENV['KUBERNETES_SERVICE_HOST']
98
- port = ENV['KUBERNETES_SERVICE_PORT_HTTPS']
97
+ # @raise [K8s::Error::Config] when the environment variables KUBERNETES_SEVICE_HOST and KUBERNETES_SERVICE_PORT_HTTPS are not set
98
+ # @raise [Errno::ENOENT,Errno::EACCES] when /var/run/secrets/kubernetes.io/serviceaccount/ca.crt or /var/run/secrets/kubernetes.io/serviceaccount/token can not be read
99
+ def self.in_cluster_config(**options)
100
+ host = ENV['KUBERNETES_SERVICE_HOST'].to_s
101
+ raise(K8s::Error::Config, "in_cluster_config failed: KUBERNETES_SERVICE_HOST environment not set") if host.empty?
102
+
103
+ port = ENV['KUBERNETES_SERVICE_PORT_HTTPS'].to_s
104
+ raise(K8s::Error::Config, "in_cluster_config failed: KUBERNETES_SERVICE_HOST environment not set") if port.empty?
99
105
 
100
106
  new(
101
107
  "https://#{host}:#{port}",
102
- ssl_verify_peer: true,
103
- ssl_ca_file: '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt',
104
- auth_token: File.read('/var/run/secrets/kubernetes.io/serviceaccount/token')
108
+ ssl_verify_peer: options.key?(:ssl_verify_peer) ? options.delete(:ssl_verify_peer) : true,
109
+ ssl_ca_file: options.delete(:ssl_ca_file) || '/var/run/secrets/kubernetes.io/serviceaccount/ca.crt',
110
+ auth_token: options.delete(:auth_token) || File.read('/var/run/secrets/kubernetes.io/serviceaccount/token'),
111
+ **options
105
112
  )
106
113
  end
107
114
 
@@ -155,6 +162,7 @@ module K8s
155
162
  end
156
163
 
157
164
  # @param options [Hash] as passed to Excon#request
165
+ # @return [String]
158
166
  def format_request(options)
159
167
  method = options[:method]
160
168
  path = options[:path]
@@ -173,7 +181,7 @@ module K8s
173
181
 
174
182
  # @param response [Hash] as returned by Excon#request
175
183
  # @param request_options [Hash] as passed to Excon#request
176
- # @param response_class [Class] decode response body using #from_json
184
+ # @param response_class [Class] coerce into response body using #new
177
185
  # @raise [K8s::Error]
178
186
  # @raise [Excon::Error] TODO: wrap
179
187
  # @return [response_class, Hash]
@@ -201,7 +209,7 @@ module K8s
201
209
 
202
210
  return response_data unless response_class
203
211
 
204
- response_class.from_json(response_data)
212
+ response_class.new(response_data)
205
213
  else
206
214
  error_class = K8s::Error::HTTP_STATUS_ERRORS[response.status] || K8s::Error::API
207
215
 
@@ -217,8 +225,9 @@ module K8s
217
225
  end
218
226
  end
219
227
 
220
- # @param response_class [Class] decode response body using #from_json
228
+ # @param response_class [Class] coerce into response class using #new
221
229
  # @param options [Hash] @see Excon#request
230
+ # @return [response_class, Hash]
222
231
  def request(response_class: nil, **options)
223
232
  if options[:method] == 'DELETE' && need_delete_body?
224
233
  options[:request_object] = options.delete(:query)
@@ -248,7 +257,7 @@ module K8s
248
257
  # @param skip_forbidden [Boolean] return nil for HTTP 403 responses
249
258
  # @param retry_errors [Boolean] retry with non-pipelined request for HTTP 503 responses
250
259
  # @param common_options [Hash] @see #request, merged with the per-request options
251
- # @return [Array<response_class, Hash, nil>]
260
+ # @return [Array<response_class, Hash, NilClass>]
252
261
  def requests(*options, skip_missing: false, skip_forbidden: false, retry_errors: true, **common_options)
253
262
  return [] if options.empty? # excon chokes
254
263
 
@@ -297,13 +306,14 @@ module K8s
297
306
  )
298
307
  end
299
308
 
300
- # @return [true, false] true if delete options should be sent as bode of the DELETE request
309
+ # @return [Boolean] true if delete options should be sent as bode of the DELETE request
301
310
  def need_delete_body?
302
311
  @need_delete_body ||= Gem::Version.new(version.gitVersion.match(/v*(.*)/)[1]) < DELETE_OPTS_BODY_VERSION_MIN
303
312
  end
304
313
 
305
314
  # @param path [Array<String>] @see #path
306
315
  # @param options [Hash] @see #request
316
+ # @return [Array<response_class, Hash, NilClass>]
307
317
  def get(*path, **options)
308
318
  request(
309
319
  method: 'GET',
@@ -314,6 +324,7 @@ module K8s
314
324
 
315
325
  # @param paths [Array<String>]
316
326
  # @param options [Hash] @see #request
327
+ # @return [Array<response_class, Hash, NilClass>]
317
328
  def gets(*paths, **options)
318
329
  requests(
319
330
  *paths.map do |path|
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.6.4
4
+ version: 0.7.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: 2018-12-12 00:00:00.000000000 Z
11
+ date: 2019-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: excon
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
110
  version: 1.4.0
111
+ - !ruby/object:Gem::Dependency
112
+ name: yaml-safe_load_stream
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.1'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.1'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: bundler
113
127
  requirement: !ruby/object:Gem::Requirement