k8s-ruby 0.10.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,214 @@
1
+ # K8s::Client provided by k8s-ruby
2
+
3
+ Ruby client library for the Kubernetes (1.9+) API
4
+
5
+ ## Highlights
6
+
7
+ * Clean API for dynamic Kubernetes API Groups / Resources
8
+ * Fast API requests using HTTP connection keepalive
9
+ * Fast API discovery and resource listings using pipelined HTTP requests
10
+ * Typed errors with useful debugging information
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'k8s-ruby'
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install k8s-ruby
27
+
28
+ And then load the code using:
29
+
30
+ ```ruby
31
+ require 'k8s-ruby'
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### Overview
37
+ The top-level `K8s::Client` provides access to separate `APIClient` instances for each Kubernetes API Group (`v1`, `apps/v1`, etc.), which in turns provides access to separate `ResourceClient` instances for each API resource type (`nodes`, `pods`, `deployments`, etc.).
38
+
39
+ Individual resources are returned as `K8s::Resource` instances, which are `RecursiveOpenStruct` instances providing attribute access (`resource.metadata.name`). The resource instances are returned by methods such as `client.api('v1').resource('nodes').get('foo')`, and passed as arguments for `client.api('v1').resource('nodes').create_resource(res)`. Resources can also be loaded from disk using `K8s::Resource.from_files(path)`, and passed to the top-level methods such as `client.create_resource(res)`, which lookup the correct API/Resource client from the resource `apiVersion` and `kind`.
40
+
41
+ The different `K8s::Error::API` subclasses represent different HTTP response codes, such as `K8s::Error::NotFound` or `K8s::Error::Conflict`.
42
+
43
+ ### Creating a client
44
+
45
+ #### Unauthenticated client
46
+
47
+ ```ruby
48
+ client = K8s.client('https://localhost:6443', ssl_verify_peer: false)
49
+ ```
50
+
51
+ The keyword options are [Excon](https://github.com/excon/excon/) options.
52
+
53
+ #### Client from kubeconfig
54
+
55
+ ```ruby
56
+ client = K8s::Client.config(
57
+ K8s::Config.load_file(
58
+ File.expand_path '~/.kube/config'
59
+ )
60
+ )
61
+ ```
62
+
63
+ #### Supported kubeconfig options
64
+
65
+ Not all kubeconfig options are supported, only the following kubeconfig options work:
66
+
67
+ * `current-context`
68
+ * `context.cluster`
69
+ * `context.user`
70
+ * `cluster.server`
71
+ * `cluster.insecure_skip_tls_verify`
72
+ * `cluster.certificate_authority`
73
+ * `cluster.certificate_authority_data`
74
+ * `user.client_certificate` + `user.client_key`
75
+ * `user.client_certificate_data` + `user.client_key_data`
76
+ * `user.token`
77
+
78
+ ##### With overrides
79
+
80
+ ```ruby
81
+ client = K8s::Client.config(K8s::Config.load_file('~/.kube/config'),
82
+ server: 'http://localhost:8001',
83
+ )
84
+ ```
85
+
86
+ #### In-cluster client from pod envs/secrets
87
+
88
+ ```ruby
89
+ client = K8s::Client.in_cluster_config
90
+ ```
91
+
92
+ ### Logging
93
+
94
+ #### Quiet
95
+
96
+ To supress any warning messages:
97
+
98
+ ```ruby
99
+ K8s::Logging.quiet!
100
+ K8s::Transport.quiet!
101
+ ```
102
+
103
+ The `K8s::Transport` is quiet by default, but other components may log warnings in the future.
104
+
105
+ #### Debugging
106
+
107
+ Log all API requests
108
+
109
+ ```ruby
110
+ K8s::Logging.debug!
111
+ K8s::Transport.verbose!
112
+ ```
113
+
114
+ ```
115
+ I, [2018-08-09T14:19:50.404739 #1] INFO -- K8s::Transport: Using config with server=https://167.99.39.233:6443
116
+ I, [2018-08-09T14:19:50.629521 #1] INFO -- K8s::Transport<https://167.99.39.233:6443>: GET /version => HTTP 200: <K8s::API::Version> in 0.224s
117
+ I, [2018-08-09T14:19:50.681367 #1] INFO -- K8s::Transport<https://167.99.39.233:6443>: GET /api/v1 => HTTP 200: <K8s::API::MetaV1::APIResourceList> in 0.046s
118
+ I, [2018-08-09T14:19:51.018740 #1] INFO -- K8s::Transport<https://167.99.39.233:6443>: GET /api/v1/pods => HTTP 200: <K8s::API::MetaV1::List> in 0.316s
119
+ ```
120
+
121
+ Using `K8s::Transport.debug!` will also log request/response bodies. The `EXCON_DEBUG=true` env will log all request/response attributes, including headers.
122
+
123
+ ### Prefetching API resources
124
+
125
+ Operations like mapping a resource `kind` to an API resource URL require knowledge of the API resource lists for the API group. Mapping resources for multiple API groups would require fetching the API resource lists for each API group in turn, leading to additional request latency. This can be optimized using resource prefetching:
126
+
127
+ ```ruby
128
+ client.apis(prefetch_resources: true)
129
+ ```
130
+
131
+ This will fetch the API resource lists for all API groups in a single pipelined request.
132
+
133
+ ### Listing resources
134
+
135
+ ```ruby
136
+ client.api('v1').resource('pods', namespace: 'default').list(labelSelector: {'role' => 'test'}).each do |pod|
137
+ puts "namespace=#{pod.metadata.namespace} pod: #{pod.metadata.name} node=#{pod.spec.nodeName}"
138
+ end
139
+ ```
140
+
141
+ ### Updating resources
142
+
143
+ ```ruby
144
+ node = client.api('v1').resource('nodes').get('test-node')
145
+
146
+ node[:spec][:unschedulable] = true
147
+
148
+ client.api('v1').resource('nodes').update_resource(node)
149
+ ```
150
+
151
+ ### Deleting resources
152
+
153
+ ```ruby
154
+ pod = client.api('v1').resource('pods', namespace: 'default').delete('test-pod')
155
+ ```
156
+
157
+ ```ruby
158
+ pods = client.api('v1').resource('pods', namespace: 'default').delete_collection(labelSelector: {'role' => 'test'})
159
+ ```
160
+
161
+ ### Creating resources
162
+
163
+ #### Programmatically defined resources
164
+ ```ruby
165
+ service = K8s::Resource.new(
166
+ apiVersion: 'v1',
167
+ kind: 'Service',
168
+ metadata: {
169
+ namespace: 'default',
170
+ name: 'test',
171
+ },
172
+ spec: {
173
+ type: 'ClusterIP',
174
+ ports: [
175
+ { port: 80 },
176
+ ],
177
+ selector: {'app' => 'test'},
178
+ },
179
+ )
180
+
181
+ logger.info "Create service=#{service.metadata.name} in namespace=#{service.metadata.namespace}"
182
+
183
+ service = client.api('v1').resource('services').create_resource(service)
184
+ ```
185
+
186
+ #### From file(s)
187
+
188
+ ```ruby
189
+ resources = K8s::Resource.from_files('./test.yaml')
190
+
191
+ for resource in resources
192
+ resource = client.create_resource(resource)
193
+ end
194
+ ```
195
+
196
+ ### Patching resources
197
+
198
+ ```ruby
199
+ client.api('apps/v1').resource('deployments', namespace: 'default').merge_patch('test', {
200
+ spec: { replicas: 3 },
201
+ })
202
+ ```
203
+
204
+ ### Watching resources
205
+
206
+ ```ruby
207
+ client.api('v1').resource('pods', namespace: 'default').watch(labelSelector: {'role' => 'test'}) do |watch_event|
208
+ puts "type=#{watch_event.type} pod=#{watch_event.resource.metadata.name}"
209
+ end
210
+ ```
211
+
212
+ ## Contributing
213
+
214
+ Bug reports and pull requests are welcome on GitHub at [rdxnet/k8s-ruby](https://github.com/rdxnet/k8s-ruby).
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require 'rubocop/rake_task'
6
+
7
+ RSpec::Core::RakeTask.new(:spec)
8
+
9
+ RuboCop::RakeTask.new(:rubocop).tap do |task|
10
+ task.options = %w(--fail-level A -S --format c)
11
+ if RUBY_ENGINE == 'ruby' && RbConfig::CONFIG['host_os'] !~ /mswin|msys|mingw|cygwin|bccwin|wince|emc/
12
+ task.options << '--parallel'
13
+ end
14
+ end
15
+
16
+ task default: [:spec, :rubocop]
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'optparse'
5
+ require 'uri'
6
+ require 'k8s-ruby'
7
+
8
+ Options = Struct.new(
9
+ :in_cluster_config,
10
+ :config,
11
+ :server,
12
+ :insecure_skip_tls_verify,
13
+ :prefetch_resources,
14
+ :list_resource_kinds,
15
+ :namespace,
16
+ :all_namespaces,
17
+ :label_selector,
18
+ :pipeline_list,
19
+ :list_resources,
20
+ :list_pods,
21
+ :update_node,
22
+ :node_unschedulable,
23
+ :delete_pod,
24
+ :delete_pods,
25
+ :create_service,
26
+ :service_type,
27
+ :service_port,
28
+ :resources,
29
+ :create_resources,
30
+ :update_resources,
31
+ :delete_resources,
32
+ :stack_name,
33
+ :stack,
34
+ :prune_stack,
35
+ :delete_stack,
36
+ :api,
37
+ :list_api_resources,
38
+ :patch_deployment,
39
+ :replicas,
40
+ :version
41
+ )
42
+
43
+ options = Options.new
44
+ options.create_resources = []
45
+ options.update_resources = []
46
+ options.delete_resources = []
47
+
48
+ logger = Logger.new(STDERR)
49
+
50
+ opt_parser = OptionParser.new do |parser|
51
+ parser.on('--debug-api') do
52
+ K8s::Logging.debug!
53
+ K8s::Transport.debug!
54
+ end
55
+ parser.on('--debug') do
56
+ K8s::Logging.debug!
57
+ K8s::Transport.verbose!
58
+ end
59
+ parser.on('--verbose') do
60
+ K8s::Logging.verbose!
61
+ K8s::Transport.quiet!
62
+ end
63
+ parser.on('--quiet') do
64
+ K8s::Logging.quiet!
65
+ K8s::Transport.quiet!
66
+ end
67
+ parser.on('--version') do
68
+ options.version = true
69
+ end
70
+ parser.on('--in-cluster-config') do
71
+ options.in_cluster_config = true
72
+ end
73
+ parser.on('--kubeconfig=PATH') do |path|
74
+ options.config = K8s::Config.load_file(path)
75
+ end
76
+ parser.on('--server=SERVER') do |server|
77
+ options.server = server
78
+ end
79
+ parser.on('--insecure-skip-tls-verify', TrueClass) do |bool|
80
+ options.insecure_skip_tls_verify = bool
81
+ end
82
+ parser.on('--prefetch-resources', TrueClass) do |bool|
83
+ options.prefetch_resources = bool
84
+ end
85
+ parser.on('--list-resource-kinds', TrueClass) do |_bool|
86
+ options.list_resource_kinds = true
87
+ end
88
+ parser.on('-n', '--namespace=NAMESPACE') do |namespace|
89
+ options.namespace = namespace
90
+ end
91
+ parser.on('--all-namespaces') do
92
+ options.all_namespaces = true
93
+ end
94
+ parser.on('-l', '--label-selector=LABEL=VALUE') do |selector|
95
+ options.label_selector = selector
96
+ end
97
+ parser.on('--pipeline-list') do
98
+ options.pipeline_list = true
99
+ end
100
+ parser.on('--list-resources') do
101
+ options.list_resources = true
102
+ end
103
+ parser.on('--list-pods') do
104
+ options.list_pods = true
105
+ end
106
+ parser.on('--update-node=NODE') do |node|
107
+ options.update_node = node
108
+ end
109
+ parser.on('--node-unschedulable=BOOL', TrueClass) do |bool|
110
+ options.node_unschedulable = bool
111
+ end
112
+ parser.on('--delete-pod=POD') do |pod|
113
+ options.delete_pod = pod
114
+ end
115
+ parser.on('--delete-pods') do
116
+ options.delete_pods = true
117
+ end
118
+ parser.on('--create-service=SERVICE') do |service|
119
+ options.create_service = service
120
+ end
121
+ parser.on('--service-type=SERVICE-TYPE') do |type|
122
+ options.service_type = type
123
+ end
124
+ parser.on('--service-port=PORT', Integer) do |port|
125
+ options.service_port = port
126
+ end
127
+ parser.on('--resource-file=path') do |path|
128
+ options.resources = K8s::Resource.from_files(path)
129
+ end
130
+ parser.on('--create') do
131
+ options.create_resources = options.resources
132
+ end
133
+ parser.on('--update') do
134
+ options.update_resources = options.resources
135
+ end
136
+ parser.on('--delete') do
137
+ options.delete_resources = options.resources
138
+ end
139
+ parser.on('--stack-name=NAME') do |name|
140
+ options.stack_name = name
141
+ end
142
+ parser.on('--stack=PATH') do |path|
143
+ options.stack = K8s::Stack.load(options.stack_name || File.basename(path, ".*"), path)
144
+ end
145
+ parser.on('--prune-stack', TrueClass) do |flag|
146
+ options.prune_stack = flag
147
+ end
148
+ parser.on('--delete-stack') do
149
+ options.delete_stack = options.stack
150
+ end
151
+ parser.on('--api=API') do |api|
152
+ options.api = api
153
+ end
154
+ parser.on('--list-api-resources') do
155
+ options.list_api_resources = options.api
156
+ end
157
+ parser.on('--patch-deployment=NAME') do |name|
158
+ options.patch_deployment = name
159
+ end
160
+ parser.on('--replicas=COUNT', Integer) do |count|
161
+ options.replicas = count
162
+ end
163
+ end
164
+
165
+ opt_parser.parse!
166
+
167
+ if options.config
168
+ overrides = {}
169
+ overrides[:ssl_verify_peer] = false if options.insecure_skip_tls_verify
170
+
171
+ client = K8s::Client.config(options.config,
172
+ server: options.server,
173
+ **overrides)
174
+ elsif options.in_cluster_config
175
+ client = K8s::Client.in_cluster_config
176
+ else
177
+ client = K8s.client(options.server,
178
+ ssl_verify_peer: !options.insecure_skip_tls_verify)
179
+ end
180
+
181
+ if options.version
182
+ logger.info client.version
183
+ else
184
+ logger.info "Kube server version: #{client.version.gitVersion}"
185
+ end
186
+
187
+ if options.prefetch_resources
188
+ client.apis(prefetch_resources: true)
189
+ end
190
+
191
+ if options.list_resource_kinds
192
+ client.resources.sort_by(&:kind).each do |resource_client|
193
+ next if resource_client.subresource?
194
+
195
+ puts "#{resource_client.kind} => #{resource_client.api_version} #{resource_client.name}"
196
+ end
197
+ end
198
+
199
+ namespace = if options.all_namespaces
200
+ nil # all
201
+ elsif options.namespace
202
+ options.namespace
203
+ elsif options.config && ns = options.config.context.namespace
204
+ ns
205
+ end
206
+
207
+ if options.list_api_resources
208
+ logger.info "List resource types for api=#{options.list_api_resources}..."
209
+
210
+ client.api(options.list_api_resources).resources do |api_resource|
211
+ logger.info "api=#{api_resource.api_version} resource=#{api_resource.resource} subresource=#{api_resource.subresource}"
212
+ end
213
+ end
214
+
215
+ if options.pipeline_list
216
+ objects = client.list_resources(namespace: namespace, labelSelector: options.label_selector)
217
+ objects.each do |object|
218
+ logger.info "api=#{object.apiVersion} kind=#{object.kind} namespace=#{object.metadata.namespace} name=#{object.metadata.name}"
219
+ end
220
+ elsif options.list_resources
221
+ client.apis(prefetch_resources: true).each do |api|
222
+ logger.info "api=#{api.api_version}"
223
+
224
+ resources = api.resources.select(&:list?)
225
+
226
+ resources.each do |resource|
227
+ logger.info "api=#{api.api_version} resource=#{resource.name}"
228
+
229
+ objects = resource.list(labelSelector: options.label_selector)
230
+ objects.each do |object|
231
+ logger.info "api=#{object.apiVersion} kind=#{object.kind} namespace=#{object.metadata.namespace} name=#{object.metadata.name}"
232
+ end
233
+ end
234
+ end
235
+ end
236
+
237
+ if options.list_pods
238
+ client.api('v1').resource('pods', namespace: namespace).list(labelSelector: options.label_selector).each do |pod|
239
+ puts "namespace=#{pod.metadata.namespace} pod: #{pod.metadata.name} node=#{pod.spec.nodeName}"
240
+ end
241
+ end
242
+
243
+ if options.update_node
244
+ node = client.api('v1').resource('nodes').get(options.update_node)
245
+
246
+ puts "Update node=#{node.metadata.name}..."
247
+
248
+ if !options.node_unschedulable.nil?
249
+ puts "Set node=#{node.metadata.name} unschedulable: #{node.spec.unschedulable} => #{options.node_unschedulable}"
250
+
251
+ node[:spec][:unschedulable] = options.node_unschedulable
252
+ end
253
+
254
+ client.api('v1').resource('nodes').update_resource(node)
255
+ end
256
+
257
+ if options.delete_pod
258
+ logger.info "Delete pod=#{options.delete_pod} in namespace=#{namespace}"
259
+
260
+ pod = client.api('v1').resource('pods', namespace: namespace).delete(options.delete_pod)
261
+
262
+ logger.debug { pod.metadata }
263
+ end
264
+
265
+ if options.delete_pods
266
+ logger.info "Delete pods with labelSelector=#{options.label_selector} in namespace=#{namespace}"
267
+
268
+ pods = client.api('v1').resource('pods', namespace: namespace).delete_collection(labelSelector: options.label_selector)
269
+
270
+ pods.each do |pod_instance|
271
+ logger.info "Deleted pod=#{pod_instance.metadata.name} in namespace=#{pod_instance.metadata.namespace} on node=#{pod_instance.spec.nodeName}"
272
+ end
273
+ end
274
+
275
+ if options.create_service
276
+ service = K8s::Resource.new(
277
+ apiVersion: 'v1',
278
+ kind: 'Service',
279
+ metadata: {
280
+ namespace: namespace,
281
+ name: options.create_service
282
+ },
283
+ spec: {
284
+ type: options.service_type,
285
+ ports: [
286
+ { port: options.service_port }
287
+ ],
288
+ selector: Hash[options.label_selector.split('=', 2)]
289
+ }
290
+ )
291
+
292
+ logger.info "Create service=#{service.metadata.name} in namespace=#{service.metadata.namespace}"
293
+
294
+ service = client.api('v1').resource('services').create_resource(service)
295
+
296
+ logger.debug { service }
297
+ end
298
+
299
+ options.create_resources.each do |resource|
300
+ resource = client.create_resource(resource)
301
+
302
+ logger.info "Created #{resource.apiVersion} resource #{resource.kind} #{resource.metadata.name} in namespace #{resource.metadata.namespace}:\n#{JSON.pretty_generate(resource)}"
303
+ end
304
+
305
+ options.update_resources.each do |resource|
306
+ resource = client.update_resource(resource)
307
+
308
+ logger.info "Updated #{resource.apiVersion} resource #{resource.kind} #{resource.metadata.name} in namespace #{resource.metadata.namespace}:\n#{JSON.pretty_generate(resource)}"
309
+ end
310
+
311
+ options.delete_resources.each do |resource|
312
+ begin
313
+ resource = client.delete_resource(resource)
314
+
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 => e
317
+ logger.info "Skip #{resource.apiVersion} resource #{resource.kind} #{resource.metadata.name} in namespace #{resource.metadata.namespace}: #{e}"
318
+ end
319
+ end
320
+
321
+ if stack = options.delete_stack
322
+ logger.info "Delete stack #{stack.name}..."
323
+
324
+ stack.delete(client)
325
+
326
+ elsif options.stack
327
+ logger.info "Apply stack #{options.stack.name}..."
328
+
329
+ options.stack.apply(client, prune: options.prune_stack)
330
+ end
331
+
332
+ if name = options.patch_deployment
333
+ client.api('apps/v1').resource('deployments', namespace: namespace).merge_patch(
334
+ name,
335
+ spec: { replicas: options.replicas }
336
+ )
337
+ end