kubernetes-cli 0.3.2 → 0.4.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.
data/spec/cli_spec.rb ADDED
@@ -0,0 +1,597 @@
1
+ # typed: ignore
2
+
3
+ require 'spec_helper'
4
+ require 'stringio'
5
+
6
+ describe KubernetesCLI do
7
+ let(:kubeconfig_path) { File.join(ENV['HOME'], '.kube', 'config') }
8
+ let(:cli) { described_class.new(kubeconfig_path) }
9
+ let(:fake_cli) { TestCLI.new(kubeconfig_path) }
10
+
11
+ let(:deployment) do
12
+ KubeDSL.deployment do
13
+ metadata do
14
+ name 'test-deployment'
15
+ namespace 'test'
16
+ end
17
+
18
+ spec do
19
+ replicas 1
20
+
21
+ selector do
22
+ match_labels do
23
+ add :foo, 'bar'
24
+ end
25
+ end
26
+
27
+ template do
28
+ metadata do
29
+ labels do
30
+ add :foo, 'bar'
31
+ end
32
+ end
33
+
34
+ spec do
35
+ container(:ruby) do
36
+ image 'ruby:3.0'
37
+ image_pull_policy 'IfNotPresent'
38
+ name 'ruby'
39
+ command ['ruby', '-e', 'STDOUT.sync = true; loop { puts "alive"; sleep 5 }']
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ around do |example|
48
+ if ENV.fetch('SHOW_STDOUT', 'false') == 'true'
49
+ example.run
50
+ else
51
+ File.open(File::NULL, 'w') do |f|
52
+ cli.with_pipes(f, f) { example.run }
53
+ end
54
+ end
55
+ end
56
+
57
+ describe '#run_cmd' do
58
+ it 'includes the kubeconfig' do
59
+ fake_cli.run_cmd(%w(ls))
60
+ expect(fake_cli).to run_exec.with_args(['--kubeconfig', kubeconfig_path])
61
+ end
62
+
63
+ it 'runs a kubectl command' do
64
+ fake_cli.run_cmd(%w(ls))
65
+ expect(fake_cli).to run_exec.with_args(['ls'])
66
+ end
67
+ end
68
+
69
+ describe '#system_cmd' do
70
+ it 'runs a command in a pod' do
71
+ cli.apply(deployment)
72
+ wait_for_deployment(deployment)
73
+
74
+ pods = cli.get_objects(
75
+ 'Pod',
76
+ deployment.metadata.namespace,
77
+ deployment.spec.selector.match_labels.kv_pairs
78
+ )
79
+
80
+ stdout = StringIO.new
81
+
82
+ cli.with_pipes(stdout) do
83
+ cli.system_cmd(
84
+ ['ruby', '-e', '"STDOUT.sync = true; puts 1 + 1"'],
85
+ deployment.metadata.namespace,
86
+ pods.first.dig(*%w(metadata name)),
87
+ false,
88
+ deployment.spec.template.spec.container(:ruby).name
89
+ )
90
+ end
91
+
92
+ expect(stdout.string.to_i).to eq(2)
93
+ ensure
94
+ safely_delete_res(deployment)
95
+ end
96
+ end
97
+
98
+ describe '#exec_cmd' do
99
+ let(:container_cmd) { %w(ls) }
100
+ let(:namespace) { 'namespace' }
101
+ let(:pod) { 'pod' }
102
+ let(:container) { 'container' }
103
+ let(:out_file) { '/path/to/file' }
104
+
105
+ it 'includes the path to the kubeconfig' do
106
+ fake_cli.exec_cmd(container_cmd, namespace, pod)
107
+ expect(fake_cli).to run_exec.with_args(['--kubeconfig', kubeconfig_path])
108
+ end
109
+
110
+ it 'includes the container command, namespace, and pod' do
111
+ fake_cli.exec_cmd(container_cmd, namespace, pod)
112
+ expect(fake_cli).to run_exec.with_args(['-n', namespace], [pod], ['--', 'ls'])
113
+ end
114
+
115
+ it 'starts a TTY by default' do
116
+ fake_cli.exec_cmd(container_cmd, namespace, pod)
117
+ expect(fake_cli).to run_exec.with_args(['-it'])
118
+ end
119
+
120
+ it 'does not start a TTY when asked not to' do
121
+ fake_cli.exec_cmd(container_cmd, namespace, pod, false)
122
+ expect(fake_cli).to run_exec.without_args(['-it'])
123
+ end
124
+
125
+ it 'selects the container when given' do
126
+ fake_cli.exec_cmd(container_cmd, namespace, pod, true, container)
127
+ expect(fake_cli).to run_exec.with_args(['-c', container])
128
+ end
129
+
130
+ it 'redirects standard output to a file when a file is given' do
131
+ fake_cli.exec_cmd(container_cmd, namespace, pod, true, nil, out_file)
132
+ expect(fake_cli).to run_exec.and_redirect_to(out_file)
133
+ end
134
+ end
135
+
136
+ describe '#apply' do
137
+ let(:res) do
138
+ KubeDSL.config_map do
139
+ metadata do
140
+ namespace 'test'
141
+ name 'test-config'
142
+ end
143
+
144
+ data do
145
+ add 'key', 'value'
146
+ end
147
+ end
148
+ end
149
+
150
+ let(:bad_res) do
151
+ klass = Class.new(KubeDSL::DSL::V1::ConfigMap) do
152
+ value_field :non_existent
153
+
154
+ def serialize
155
+ super.merge(nonExistent: non_existent)
156
+ end
157
+ end
158
+
159
+ klass.new do
160
+ metadata do
161
+ namespace 'test'
162
+ name 'test-config'
163
+ end
164
+
165
+ non_existent 'bad'
166
+ end
167
+ end
168
+
169
+ it 'applies the resource' do
170
+ cli.apply(res)
171
+ obj = get_object(res)
172
+ expect(obj['data']).to eq({ 'key' => 'value' })
173
+ ensure
174
+ safely_delete_res(res)
175
+ end
176
+
177
+ it 'raises an error if the resource is malformed' do
178
+ expect { cli.apply(bad_res) }.to raise_error do |error|
179
+ expect(error).to be_a(KubernetesCLI::InvalidResourceError)
180
+ expect(error.resource).to eq(bad_res)
181
+ end
182
+ end
183
+
184
+ it "doesn't apply the resource if asked to perform a dry run" do
185
+ cli.apply(res, dry_run: true)
186
+ expect { get_object(res) }.to raise_error(KubernetesCLI::GetResourceError)
187
+ end
188
+ end
189
+
190
+ describe '#apply_uri' do
191
+ let(:url) { 'https://raw.githubusercontent.com/getkuby/kubernetes-cli/master/spec/support/test_config_map.yaml' }
192
+ let(:bad_url) { 'https://raw.githubusercontent.com/getkuby/kubernetes-cli/master/spec/support/test_config_map_bad.yaml' }
193
+ let(:kind) { 'ConfigMap' }
194
+ let(:name) { 'test' }
195
+ let(:namespace) { 'test-config-external' }
196
+
197
+ it 'applies the external file' do
198
+ cli.apply_uri(url)
199
+ obj = cli.get_object(kind, name, namespace)
200
+ expect(obj['data']).to eq({ 'key' => 'external value' })
201
+ ensure
202
+ begin
203
+ cli.delete_object(kind, name, namespace)
204
+ rescue KubernetesCLI::DeleteResourceError
205
+ end
206
+ end
207
+
208
+ it 'raises an error if the resource is malformed' do
209
+ expect { cli.apply_uri(bad_url) }.to raise_error do |error|
210
+ expect(error).to be_a(KubernetesCLI::InvalidResourceUriError)
211
+ expect(error.resource_uri).to eq(bad_url)
212
+ end
213
+ end
214
+
215
+ it "doesn't apply the resource if asked to perform a dry run" do
216
+ cli.apply_uri(url, dry_run: true)
217
+ expect { cli.get_object(kind, name, namespace) }.to raise_error(KubernetesCLI::GetResourceError)
218
+ end
219
+ end
220
+
221
+ describe '#get_object' do
222
+ let(:res) do
223
+ KubeDSL.config_map do
224
+ metadata do
225
+ namespace 'test'
226
+ name 'test-config'
227
+ labels do
228
+ add :foo, 'bar'
229
+ end
230
+ end
231
+
232
+ data do
233
+ add 'key', 'value'
234
+ end
235
+ end
236
+ end
237
+
238
+ it 'gets the object by name' do
239
+ cli.apply(res)
240
+ obj = cli.get_object('ConfigMap', res.metadata.namespace, res.metadata.name)
241
+ expect(obj['data']).to eq({ 'key' => 'value' })
242
+ ensure
243
+ safely_delete_res(res)
244
+ end
245
+ end
246
+
247
+ describe '#get_objects' do
248
+ let(:res1) do
249
+ KubeDSL.config_map do
250
+ metadata do
251
+ namespace 'test'
252
+ name 'test-config1'
253
+ labels do
254
+ add :foo, 'bar'
255
+ end
256
+ end
257
+
258
+ data do
259
+ add 'key1', 'value1'
260
+ end
261
+ end
262
+ end
263
+
264
+ let(:res2) do
265
+ KubeDSL.config_map do
266
+ metadata do
267
+ namespace 'test'
268
+ name 'test-config2'
269
+ labels do
270
+ add :baz, 'boo'
271
+ end
272
+ end
273
+
274
+ data do
275
+ add 'key2', 'value2'
276
+ end
277
+ end
278
+ end
279
+
280
+ let(:res3) do
281
+ KubeDSL.config_map do
282
+ metadata do
283
+ namespace 'test'
284
+ name 'test-config3'
285
+ labels do
286
+ add :foo, 'bar'
287
+ end
288
+ end
289
+
290
+ data do
291
+ add 'key3', 'value3'
292
+ end
293
+ end
294
+ end
295
+
296
+ it 'gets the object by its labels' do
297
+ cli.apply(res1)
298
+ cli.apply(res2)
299
+ cli.apply(res3)
300
+ obj = cli.get_objects('ConfigMap', res1.metadata.namespace, foo: 'bar')
301
+ expect(obj.size).to eq(2)
302
+ expect(obj.map { |o| o['metadata']['name'] }.sort).to eq(
303
+ ['test-config1', 'test-config3']
304
+ )
305
+ ensure
306
+ safely_delete_res(res1)
307
+ safely_delete_res(res2)
308
+ safely_delete_res(res3)
309
+ end
310
+ end
311
+
312
+ describe '#delete_object' do
313
+ let(:res) do
314
+ KubeDSL.config_map do
315
+ metadata do
316
+ namespace 'test'
317
+ name 'test-config'
318
+ end
319
+
320
+ data do
321
+ add 'key', 'value'
322
+ end
323
+ end
324
+ end
325
+
326
+ it 'deletes the resource' do
327
+ cli.apply(res)
328
+ cli.delete_object('ConfigMap', res.metadata.namespace, res.metadata.name)
329
+ expect { get_object(res) }.to raise_error(KubernetesCLI::GetResourceError)
330
+ ensure
331
+ safely_delete_res(res)
332
+ end
333
+
334
+ it "raises an error if the resource doesn't exist" do
335
+ expect { cli.delete_object('ConfigMap', res.metadata.namespace, res.metadata.name) }.to(
336
+ raise_error(KubernetesCLI::DeleteResourceError)
337
+ )
338
+ end
339
+ end
340
+
341
+ describe '#delete_objects' do
342
+ let(:res1) do
343
+ KubeDSL.config_map do
344
+ metadata do
345
+ namespace 'test'
346
+ name 'test-config1'
347
+ labels do
348
+ add :foo, 'bar'
349
+ end
350
+ end
351
+
352
+ data do
353
+ add 'key1', 'value1'
354
+ end
355
+ end
356
+ end
357
+
358
+ let(:res2) do
359
+ KubeDSL.config_map do
360
+ metadata do
361
+ namespace 'test'
362
+ name 'test-config2'
363
+ labels do
364
+ add :baz, 'boo'
365
+ end
366
+ end
367
+
368
+ data do
369
+ add 'key2', 'value2'
370
+ end
371
+ end
372
+ end
373
+
374
+ let(:res3) do
375
+ KubeDSL.config_map do
376
+ metadata do
377
+ namespace 'test'
378
+ name 'test-config3'
379
+ labels do
380
+ add :foo, 'bar'
381
+ end
382
+ end
383
+
384
+ data do
385
+ add 'key3', 'value3'
386
+ end
387
+ end
388
+ end
389
+
390
+ it 'deletes objects by their labels' do
391
+ cli.apply(res1)
392
+ cli.apply(res2)
393
+ cli.apply(res3)
394
+ cli.delete_objects('ConfigMap', res1.metadata.namespace, foo: 'bar')
395
+ expect { get_object(res1) }.to raise_error(KubernetesCLI::GetResourceError)
396
+ expect { get_object(res3) }.to raise_error(KubernetesCLI::GetResourceError)
397
+ ensure
398
+ safely_delete_res(res1)
399
+ safely_delete_res(res2)
400
+ safely_delete_res(res3)
401
+ end
402
+ end
403
+
404
+ describe '#patch_object' do
405
+ let(:res) do
406
+ KubeDSL.config_map do
407
+ metadata do
408
+ namespace 'test'
409
+ name 'test-config'
410
+ end
411
+
412
+ data do
413
+ add 'key', 'value'
414
+ end
415
+ end
416
+ end
417
+
418
+ it 'patches the object' do
419
+ cli.apply(res)
420
+ cli.patch_object('ConfigMap', res.metadata.namespace, res.metadata.name, '{"data":{"key":"patched"}}')
421
+ obj = get_object(res)
422
+ expect(obj['data']).to eq({ "key" => "patched" })
423
+ ensure
424
+ safely_delete_res(res)
425
+ end
426
+
427
+ it 'raises an error when patching fails' do
428
+ cli.apply(res)
429
+ expect {
430
+ cli.patch_object('ConfigMap', res.metadata.namespace, res.metadata.name, '{"data":{"key":}}')
431
+ }.to raise_error(KubernetesCLI::PatchResourceError)
432
+ ensure
433
+ safely_delete_res(res)
434
+ end
435
+ end
436
+
437
+ describe '#annotate' do
438
+ let(:res) do
439
+ KubeDSL.config_map do
440
+ metadata do
441
+ namespace 'test'
442
+ name 'test-config'
443
+ end
444
+
445
+ data do
446
+ add 'key', 'value'
447
+ end
448
+ end
449
+ end
450
+
451
+ it 'annotates the resource' do
452
+ cli.apply(res)
453
+ cli.annotate('ConfigMap', res.metadata.namespace, res.metadata.name, { foo: 'bar' })
454
+ obj = get_object(res)
455
+ annotations = obj.dig(*%w(metadata annotations))
456
+ expect(annotations).to include('foo' => 'bar')
457
+ ensure
458
+ safely_delete_res(res)
459
+ end
460
+
461
+ it 'does not overwrite annotations when option is given' do
462
+ cli.apply(res)
463
+ cli.annotate('ConfigMap', res.metadata.namespace, res.metadata.name, { foo: 'bar' })
464
+ expect {
465
+ cli.annotate('ConfigMap', res.metadata.namespace, res.metadata.name, { foo: 'baz' }, overwrite: false)
466
+ }.to raise_error(KubernetesCLI::AnnotateResourceError)
467
+ obj = get_object(res)
468
+ annotations = obj.dig(*%w(metadata annotations))
469
+ expect(annotations).to include('foo' => 'bar')
470
+ ensure
471
+ safely_delete_res(res)
472
+ end
473
+ end
474
+
475
+ describe '#logtail' do
476
+ let(:namespace) { 'namespace' }
477
+ let(:selector) { { role: 'web' } }
478
+
479
+ it 'includes the path to the kubeconfig' do
480
+ fake_cli.logtail(namespace, selector)
481
+ expect(fake_cli).to run_exec.with_args(['--kubeconfig', kubeconfig_path])
482
+ end
483
+
484
+ it 'includes the selector' do
485
+ fake_cli.logtail(namespace, selector)
486
+ expect(fake_cli).to run_exec.with_args(['--selector', 'role=web'])
487
+ end
488
+
489
+ it 'follows log output by default' do
490
+ fake_cli.logtail(namespace, selector)
491
+ expect(fake_cli).to run_exec.with_args(['-f'])
492
+ end
493
+
494
+ it "doesn't follows log output when asked" do
495
+ fake_cli.logtail(namespace, selector, follow: false)
496
+ expect(fake_cli).to run_exec.without_args(['-f'])
497
+ end
498
+ end
499
+
500
+ describe '#current_context' do
501
+ it 'fetches the current context' do
502
+ expect(cli.current_context).to eq('kind-kubernetes-cli-tests')
503
+ end
504
+ end
505
+
506
+ describe '#api_resources' do
507
+ it 'gets the set of available API resources' do
508
+ api_resources = cli.api_resources
509
+ pairs = api_resources.split("\n").map { |line| line.split(/[\s]+/)[0...2] }
510
+ expect(pairs).to include(['namespaces', 'ns'])
511
+ expect(pairs).to include(['configmaps', 'cm'])
512
+ end
513
+ end
514
+
515
+ describe '#restart_deployment' do
516
+ it 'restarts the deployment' do
517
+ cli.apply(deployment)
518
+ wait_for_deployment(deployment)
519
+
520
+ obj_v1 = get_object(deployment)
521
+ expect(obj_v1.dig(*%w(status observedGeneration))).to eq(1)
522
+
523
+ cli.restart_deployment(
524
+ deployment.metadata.namespace,
525
+ deployment.metadata.name
526
+ )
527
+
528
+ obj_v2 = get_object(deployment)
529
+ expect(obj_v2.dig(*%w(status observedGeneration))).to eq(2)
530
+ ensure
531
+ safely_delete_res(deployment)
532
+ end
533
+ end
534
+
535
+ def wait_for_deployment(depl)
536
+ start = Time.now
537
+
538
+ loop do
539
+ obj = get_object(depl)
540
+ desired = obj.dig(*%w(status replicas))
541
+ updated = obj.dig(*%w(status updatedReplicas))
542
+ available = obj.dig(*%w(status availableReplicas))
543
+
544
+ if updated == desired && updated == available
545
+ break
546
+ else
547
+ if (Time.now - start) > 60
548
+ raise 'timed out waiting for deployment'
549
+ end
550
+
551
+ sleep 1
552
+ end
553
+ end
554
+
555
+ start = Time.now
556
+
557
+ loop do
558
+ obj = get_object(depl)
559
+ pods = begin
560
+ cli.get_objects(
561
+ 'Pod',
562
+ deployment.metadata.namespace,
563
+ deployment.spec.selector.match_labels.kv_pairs
564
+ )
565
+ rescue KubernetesCLI::GetResourceError
566
+ []
567
+ end
568
+
569
+ pod_phases = pods.map { |p| p.dig(*%w(status phase)) }
570
+
571
+ if pods.size == obj.dig(*%w(status replicas)) && pod_phases.all?('Running')
572
+ break
573
+ else
574
+ if (Time.now - start) > 60
575
+ raise 'timed out waiting for pods'
576
+ end
577
+
578
+ sleep 1
579
+ end
580
+ end
581
+ end
582
+
583
+ def delete_res(res)
584
+ kind = res.to_resource.contents[:kind]
585
+ cli.delete_object(kind, res.metadata.namespace, res.metadata.name)
586
+ end
587
+
588
+ def safely_delete_res(res)
589
+ delete_res(res)
590
+ rescue KubernetesCLI::DeleteResourceError
591
+ end
592
+
593
+ def get_object(res)
594
+ kind = res.to_resource.contents[:kind]
595
+ cli.get_object(kind, res.metadata.namespace, res.metadata.name)
596
+ end
597
+ end
@@ -0,0 +1,25 @@
1
+ # typed: ignore
2
+
3
+ $:.push(File.expand_path('.', __dir__))
4
+
5
+ require 'sorbet-runtime'
6
+ require 'kubernetes-cli'
7
+ require 'pry-byebug'
8
+ require 'kind-rb'
9
+ require 'kube-dsl'
10
+
11
+ require 'support/matchers'
12
+ require 'support/test_cli'
13
+ require 'support/test_resource'
14
+
15
+ RSpec.configure do |config|
16
+ config.before(:suite) do
17
+ system("#{KindRb.executable} create cluster --name kubernetes-cli-tests")
18
+ system("#{KubectlRb.executable} create namespace test")
19
+ end
20
+
21
+ config.after(:suite) do
22
+ puts # newline to separate test output from kind output
23
+ system("#{KindRb.executable} delete cluster --name kubernetes-cli-tests")
24
+ end
25
+ end