kubernetes-cli 0.3.2 → 0.4.0

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