k8s-client 0.6.4 → 0.7.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
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