1#!/usr/bin/env bash
2
3# Copyright 2018 The Kubernetes Authors.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17set -o errexit
18set -o nounset
19set -o pipefail
20
21run_crd_tests() {
22 set -o nounset
23 set -o errexit
24
25 create_and_use_new_namespace
26 kube::log::status "Testing kubectl crd"
27 kubectl "${kube_flags_with_token[@]:?}" create -f - << __EOF__
28{
29 "kind": "CustomResourceDefinition",
30 "apiVersion": "apiextensions.k8s.io/v1",
31 "metadata": {
32 "name": "foos.company.com"
33 },
34 "spec": {
35 "group": "company.com",
36 "scope": "Namespaced",
37 "names": {
38 "plural": "foos",
39 "kind": "Foo"
40 },
41 "versions": [
42 {
43 "name": "v1",
44 "served": true,
45 "storage": true,
46 "schema": {
47 "openAPIV3Schema": {
48 "type": "object",
49 "properties": {
50 "metadata": {"type": "object"},
51 "nestedField": {
52 "type": "object",
53 "properties": {
54 "someSubfield": {"type": "string"},
55 "otherSubfield": {"type": "string"},
56 "newSubfield": {"type": "string"}
57 }
58 },
59 "otherField": {"type": "string"},
60 "someField": {"type": "string"},
61 "newField": {"type": "string"},
62 "patched": {"type": "string"}
63 }
64 }
65 }
66 }
67 ]
68 }
69}
70__EOF__
71
72 # Post-Condition: assertion object exist
73 kube::test::get_object_assert customresourcedefinitions "{{range.items}}{{if eq ${id_field:?} \"foos.company.com\"}}{{$id_field}}:{{end}}{{end}}" 'foos.company.com:'
74
75 kubectl "${kube_flags_with_token[@]}" create -f - << __EOF__
76{
77 "kind": "CustomResourceDefinition",
78 "apiVersion": "apiextensions.k8s.io/v1",
79 "metadata": {
80 "name": "bars.company.com"
81 },
82 "spec": {
83 "group": "company.com",
84 "scope": "Namespaced",
85 "names": {
86 "plural": "bars",
87 "kind": "Bar"
88 },
89 "versions": [
90 {
91 "name": "v1",
92 "served": true,
93 "storage": true,
94 "schema": {
95 "openAPIV3Schema": {
96 "x-kubernetes-preserve-unknown-fields": true,
97 "type": "object"
98 }
99 }
100 }
101 ]
102 }
103}
104__EOF__
105
106 # Post-Condition: assertion object exist
107 kube::test::get_object_assert customresourcedefinitions "{{range.items}}{{if eq $id_field \"foos.company.com\" \"bars.company.com\"}}{{$id_field}}:{{end}}{{end}}" 'bars.company.com:foos.company.com:'
108
109 # This test ensures that the name printer is able to output a resource
110 # in the proper "kind.group/resource_name" format, and that the
111 # resource builder is able to resolve a GVK when a kind.group pair is given.
112 kubectl "${kube_flags_with_token[@]}" create -f - << __EOF__
113{
114 "kind": "CustomResourceDefinition",
115 "apiVersion": "apiextensions.k8s.io/v1",
116 "metadata": {
117 "name": "resources.mygroup.example.com"
118 },
119 "spec": {
120 "group": "mygroup.example.com",
121 "scope": "Namespaced",
122 "names": {
123 "plural": "resources",
124 "singular": "resource",
125 "kind": "Kind",
126 "listKind": "KindList"
127 },
128 "versions": [
129 {
130 "name": "v1alpha1",
131 "served": true,
132 "storage": true,
133 "schema": {
134 "openAPIV3Schema": {
135 "x-kubernetes-preserve-unknown-fields": true,
136 "type": "object"
137 }
138 }
139 }
140 ]
141 }
142}
143__EOF__
144
145 # Post-Condition: assertion crd with non-matching kind and resource exists
146 kube::test::get_object_assert customresourcedefinitions "{{range.items}}{{if eq $id_field \"foos.company.com\" \"bars.company.com\" \"resources.mygroup.example.com\"}}{{$id_field}}:{{end}}{{end}}" 'bars.company.com:foos.company.com:resources.mygroup.example.com:'
147
148 # This test ensures that we can create complex validation without client-side validation complaining
149 kubectl "${kube_flags_with_token[@]}" create -f - << __EOF__
150{
151 "kind": "CustomResourceDefinition",
152 "apiVersion": "apiextensions.k8s.io/v1",
153 "metadata": {
154 "name": "validfoos.company.com"
155 },
156 "spec": {
157 "group": "company.com",
158 "scope": "Namespaced",
159 "names": {
160 "plural": "validfoos",
161 "kind": "ValidFoo"
162 },
163 "versions": [
164 {
165 "name": "v1",
166 "served": true,
167 "storage": true,
168 "schema": {
169 "openAPIV3Schema": {
170 "type": "object",
171 "properties": {
172 "spec": {
173 "type": "array",
174 "items": {
175 "type": "number"
176 }
177 }
178 }
179 }
180 }
181 }
182 ]
183 }
184}
185__EOF__
186
187 # Post-Condition: assertion crd with non-matching kind and resource exists
188 kube::test::get_object_assert customresourcedefinitions "{{range.items}}{{if eq $id_field \"foos.company.com\" \"bars.company.com\" \"resources.mygroup.example.com\" \"validfoos.company.com\"}}{{$id_field}}:{{end}}{{end}}" 'bars.company.com:foos.company.com:resources.mygroup.example.com:validfoos.company.com:'
189
190 run_non_native_resource_tests
191
192 # teardown
193 kubectl delete customresourcedefinitions/foos.company.com "${kube_flags_with_token[@]}"
194 kubectl delete customresourcedefinitions/bars.company.com "${kube_flags_with_token[@]}"
195 kubectl delete customresourcedefinitions/resources.mygroup.example.com "${kube_flags_with_token[@]}"
196 kubectl delete customresourcedefinitions/validfoos.company.com "${kube_flags_with_token[@]}"
197
198 set +o nounset
199 set +o errexit
200}
201
202kube::util::non_native_resources() {
203 local times
204 local wait
205 local failed
206 times=30
207 wait=10
208 for _ in $(seq 1 $times); do
209 failed=""
210 kubectl "${kube_flags[@]:?}" get --raw '/apis/company.com/v1' || failed=true
211 kubectl "${kube_flags[@]}" get --raw '/apis/company.com/v1/foos' || failed=true
212 kubectl "${kube_flags[@]}" get --raw '/apis/company.com/v1/bars' || failed=true
213
214 if [ -z "${failed}" ]; then
215 return 0
216 fi
217 sleep ${wait}
218 done
219
220 kube::log::error "Timed out waiting for non-native-resources; tried ${times} waiting ${wait}s between each"
221 return 1
222}
223
224run_non_native_resource_tests() {
225 set -o nounset
226 set -o errexit
227
228 create_and_use_new_namespace
229 kube::log::status "Testing kubectl non-native resources"
230 kube::util::non_native_resources
231
232 # Test that we can list this new CustomResource (foos)
233 kube::test::get_object_assert foos "{{range.items}}{{$id_field}}:{{end}}" ''
234
235 # Test that we can list this new CustomResource (bars)
236 kube::test::get_object_assert bars "{{range.items}}{{$id_field}}:{{end}}" ''
237
238 # Test that we can list this new CustomResource (resources)
239 kube::test::get_object_assert resources "{{range.items}}{{$id_field}}:{{end}}" ''
240
241 # Test that we can create a new resource of type Kind
242 kubectl "${kube_flags[@]}" create -f hack/testdata/CRD/resource.yaml "${kube_flags[@]}"
243
244 # Test that -o name returns kind.group/resourcename
245 output_message=$(kubectl "${kube_flags[@]}" get resource/myobj -o name)
246 kube::test::if_has_string "${output_message}" 'kind.mygroup.example.com/myobj'
247
248 output_message=$(kubectl "${kube_flags[@]}" get resources/myobj -o name)
249 kube::test::if_has_string "${output_message}" 'kind.mygroup.example.com/myobj'
250
251 output_message=$(kubectl "${kube_flags[@]}" get kind.mygroup.example.com/myobj -o name)
252 kube::test::if_has_string "${output_message}" 'kind.mygroup.example.com/myobj'
253
254 # Delete the resource with cascading strategy background.
255 kubectl "${kube_flags[@]}" delete resources myobj --cascade=background
256
257 # Make sure it's gone
258 kube::test::wait_object_assert resources "{{range.items}}{{$id_field}}:{{end}}" ''
259
260 # Test that we can create a new resource of type Foo
261 kubectl "${kube_flags[@]}" create -f hack/testdata/CRD/foo.yaml "${kube_flags[@]}"
262 kubectl "${kube_flags[@]}" create -f hack/testdata/CRD/foo-2.yaml "${kube_flags[@]}"
263
264 # Test that we can list this new custom resource
265 kube::test::get_object_assert foos "{{range.items}}{{$id_field}}:{{end}}" 'test:'
266
267 # Test alternate forms
268 kube::test::get_object_assert foo "{{range.items}}{{$id_field}}:{{end}}" 'test:'
269 kube::test::get_object_assert foos.company.com "{{range.items}}{{$id_field}}:{{end}}" 'test:'
270 kube::test::get_object_assert foos.v1.company.com "{{range.items}}{{$id_field}}:{{end}}" 'test:'
271
272 # Test all printers, with lists and individual items
273 kube::log::status "Testing CustomResource printing"
274 kubectl "${kube_flags[@]}" get foos
275 kubectl "${kube_flags[@]}" get foos/test
276 kubectl "${kube_flags[@]}" get foos -o name
277 kubectl "${kube_flags[@]}" get foos/test -o name
278 kubectl "${kube_flags[@]}" get foos -o wide
279 kubectl "${kube_flags[@]}" get foos/test -o wide
280 kubectl "${kube_flags[@]}" get foos -o json
281 kubectl "${kube_flags[@]}" get foos/test -o json
282 kubectl "${kube_flags[@]}" get foos -o yaml
283 kubectl "${kube_flags[@]}" get foos/test -o yaml
284 kubectl "${kube_flags[@]}" get foos -o "jsonpath={.items[*].someField}" --allow-missing-template-keys=false
285 kubectl "${kube_flags[@]}" get foos/test -o "jsonpath={.someField}" --allow-missing-template-keys=false
286 kubectl "${kube_flags[@]}" get foos -o "go-template={{range .items}}{{.someField}}{{end}}" --allow-missing-template-keys=false
287 kubectl "${kube_flags[@]}" get foos/test -o "go-template={{.someField}}" --allow-missing-template-keys=false
288 output_message=$(kubectl "${kube_flags[@]}" get foos/test -o name)
289 kube::test::if_has_string "${output_message}" 'foo.company.com/test'
290
291 # Test patching
292 kube::log::status "Testing CustomResource patching"
293 kubectl "${kube_flags[@]}" patch foos/test -p '{"patched":"value1"}' --type=merge
294 kube::test::get_object_assert foos/test "{{.patched}}" 'value1'
295 kubectl "${kube_flags[@]}" patch foos/test -p '{"patched":"value2"}' --type=merge --record
296 kube::test::get_object_assert foos/test "{{.patched}}" 'value2'
297 kubectl "${kube_flags[@]}" patch foos/test -p '{"patched":null}' --type=merge --record
298 kube::test::get_object_assert foos/test "{{.patched}}" '<no value>'
299 # Get local version
300 CRD_RESOURCE_FILE="${KUBE_TEMP}/crd-foos-test.json"
301 kubectl "${kube_flags[@]}" get foos/test -o json > "${CRD_RESOURCE_FILE}"
302 # cannot apply strategic patch locally
303 CRD_PATCH_ERROR_FILE="${KUBE_TEMP}/crd-foos-test-error"
304 ! kubectl "${kube_flags[@]}" patch --local -f "${CRD_RESOURCE_FILE}" -p '{"patched":"value3"}' 2> "${CRD_PATCH_ERROR_FILE}" || exit 1
305 if grep -q "try --type merge" "${CRD_PATCH_ERROR_FILE}"; then
306 kube::log::status "\"kubectl patch --local\" returns error as expected for CustomResource: $(cat "${CRD_PATCH_ERROR_FILE}")"
307 else
308 kube::log::status "\"kubectl patch --local\" returns unexpected error or non-error: $(cat "${CRD_PATCH_ERROR_FILE}")"
309 exit 1
310 fi
311 # can apply merge patch locally
312 kubectl "${kube_flags[@]}" patch --local -f "${CRD_RESOURCE_FILE}" -p '{"patched":"value3"}' --type=merge -o json
313 # can apply merge patch remotely
314 kubectl "${kube_flags[@]}" patch --record -f "${CRD_RESOURCE_FILE}" -p '{"patched":"value3"}' --type=merge -o json
315 kube::test::get_object_assert foos/test "{{.patched}}" 'value3'
316 rm "${CRD_RESOURCE_FILE}"
317 rm "${CRD_PATCH_ERROR_FILE}"
318
319 # Test labeling
320 kube::log::status "Testing CustomResource labeling"
321 kubectl "${kube_flags[@]}" label foos --all listlabel=true
322 kubectl "${kube_flags[@]}" label foo/test itemlabel=true
323 kubectl "${kube_flags[@]}" label --all --all-namespaces foo allnsLabel=true
324 # make sure all instances in different namespaces got the annotation
325 kubectl "${kube_flags[@]}" get foo/test -oyaml | grep allnsLabel
326 kubectl "${kube_flags[@]}" get -n default foo -oyaml | grep allnsLabel
327
328 # Test annotating
329 kube::log::status "Testing CustomResource annotating"
330 kubectl "${kube_flags[@]}" annotate foos --all listannotation=true
331 kubectl "${kube_flags[@]}" annotate foo/test itemannotation=true
332 kubectl "${kube_flags[@]}" annotate --all --all-namespaces foo allnsannotation=true
333 # make sure all instances in different namespaces got the annotation
334 kubectl "${kube_flags[@]}" get foo/test -oyaml | grep allnsannotation
335 kubectl "${kube_flags[@]}" get -n default foo -oyaml | grep allnsannotation
336
337 # Test describing
338 kube::log::status "Testing CustomResource describing"
339 kubectl "${kube_flags[@]}" describe foos
340 kubectl "${kube_flags[@]}" describe foos/test
341 kubectl "${kube_flags[@]}" describe foos | grep listlabel=true
342 kubectl "${kube_flags[@]}" describe foos | grep itemlabel=true
343 # Describe command should respect the chunk size parameter
344 kube::test::describe_resource_chunk_size_assert customresourcedefinitions events
345 kube::test::describe_resource_chunk_size_assert foos events
346
347 # Delete the resource with cascading strategy background.
348 kubectl "${kube_flags[@]}" delete foos test --cascade=background
349
350 # Make sure it's gone
351 kube::test::wait_object_assert foos "{{range.items}}{{$id_field}}:{{end}}" ''
352
353 # Test that we can create a new resource of type Bar
354 kubectl "${kube_flags[@]}" create -f hack/testdata/CRD/bar.yaml "${kube_flags[@]}"
355
356 # Test that we can list this new custom resource
357 kube::test::get_object_assert bars "{{range.items}}{{$id_field}}:{{end}}" 'test:'
358
359 # Test that we can watch the resource.
360 # Start watcher in background with process substitution,
361 # so we can read from stdout asynchronously.
362 kube::log::status "Testing CustomResource watching"
363 exec 3< <(kubectl "${kube_flags[@]}" get bars --request-timeout=1m --watch-only -o name & echo $! ; wait)
364 local watch_pid
365 read -r <&3 watch_pid
366
367 # We can't be sure when the watch gets established,
368 # so keep triggering events (in the background) until something comes through.
369 local tries=0
370 while [ ${tries} -lt 10 ]; do
371 tries=$((tries+1))
372 kubectl "${kube_flags[@]}" patch bars/test -p "{\"patched\":\"${tries}\"}" --type=merge
373 sleep 1
374 done &
375 local patch_pid=$!
376
377 # Wait up to 30s for a complete line of output.
378 local watch_output
379 read -r <&3 -t 30 watch_output
380 # Stop the watcher and the patch loop.
381 kill -9 "${watch_pid}"
382 kill -9 "${patch_pid}"
383 kube::test::if_has_string "${watch_output}" 'bar.company.com/test'
384
385 # Delete the resource with cascading strategy orphan.
386 kubectl "${kube_flags[@]}" delete bars test --cascade=orphan
387
388 # Make sure it's gone
389 kube::test::wait_object_assert bars "{{range.items}}{{$id_field}}:{{end}}" ''
390
391 # Test that we can create single item via apply
392 kubectl "${kube_flags[@]}" apply -f hack/testdata/CRD/foo.yaml
393
394 # Test that we have create a foo named test
395 kube::test::get_object_assert foos "{{range.items}}{{$id_field}}:{{end}}" 'test:'
396
397 # Test that the field has the expected value
398 kube::test::get_object_assert foos/test '{{.someField}}' 'field1'
399
400 # Test that apply an empty patch doesn't change fields
401 kubectl "${kube_flags[@]}" apply -f hack/testdata/CRD/foo.yaml
402
403 # Test that the field has the same value after re-apply
404 kube::test::get_object_assert foos/test '{{.someField}}' 'field1'
405
406 # Test that apply has updated the subfield
407 kube::test::get_object_assert foos/test '{{.nestedField.someSubfield}}' 'subfield1'
408
409 # Update a subfield and then apply the change
410 kubectl "${kube_flags[@]}" apply -f hack/testdata/CRD/foo-updated-subfield.yaml
411
412 # Test that apply has updated the subfield
413 kube::test::get_object_assert foos/test '{{.nestedField.someSubfield}}' 'modifiedSubfield'
414
415 # Test that the field has the expected value
416 kube::test::get_object_assert foos/test '{{.nestedField.otherSubfield}}' 'subfield2'
417
418 # Delete a subfield and then apply the change
419 kubectl "${kube_flags[@]}" apply -f hack/testdata/CRD/foo-deleted-subfield.yaml
420
421 # Test that apply has deleted the field
422 kube::test::get_object_assert foos/test '{{.nestedField.otherSubfield}}' '<no value>'
423
424 # Test that the field does not exist
425 kube::test::get_object_assert foos/test '{{.nestedField.newSubfield}}' '<no value>'
426
427 # Add a field and then apply the change
428 kubectl "${kube_flags[@]}" apply -f hack/testdata/CRD/foo-added-subfield.yaml
429
430 # Test that apply has added the field
431 kube::test::get_object_assert foos/test '{{.nestedField.newSubfield}}' 'subfield3'
432
433 # Delete the resource
434 kubectl "${kube_flags[@]}" delete -f hack/testdata/CRD/foo.yaml
435
436 # Make sure it's gone
437 kube::test::get_object_assert foos "{{range.items}}{{$id_field}}:{{end}}" ''
438
439 # Test that we can create list via apply
440 kubectl "${kube_flags[@]}" apply -f hack/testdata/CRD/multi-crd-list.yaml
441
442 # Test that we have create a foo and a bar from a list
443 kube::test::get_object_assert foos "{{range.items}}{{$id_field}}:{{end}}" 'test-list:'
444 kube::test::get_object_assert bars "{{range.items}}{{$id_field}}:{{end}}" 'test-list:'
445
446 # Test that the field has the expected value
447 kube::test::get_object_assert foos/test-list '{{.someField}}' 'field1'
448 kube::test::get_object_assert bars/test-list '{{.someField}}' 'field1'
449
450 # Test that re-apply an list doesn't change anything
451 kubectl "${kube_flags[@]}" apply -f hack/testdata/CRD/multi-crd-list.yaml
452
453 # Test that the field has the same value after re-apply
454 kube::test::get_object_assert foos/test-list '{{.someField}}' 'field1'
455 kube::test::get_object_assert bars/test-list '{{.someField}}' 'field1'
456
457 # Test that the fields have the expected value
458 kube::test::get_object_assert foos/test-list '{{.someField}}' 'field1'
459 kube::test::get_object_assert bars/test-list '{{.someField}}' 'field1'
460
461 # Update fields and then apply the change
462 kubectl "${kube_flags[@]}" apply -f hack/testdata/CRD/multi-crd-list-updated-field.yaml
463
464 # Test that apply has updated the fields
465 kube::test::get_object_assert foos/test-list '{{.someField}}' 'modifiedField'
466 kube::test::get_object_assert bars/test-list '{{.someField}}' 'modifiedField'
467
468 # Test that the field has the expected value
469 kube::test::get_object_assert foos/test-list '{{.otherField}}' 'field2'
470 kube::test::get_object_assert bars/test-list '{{.otherField}}' 'field2'
471
472 # Delete fields and then apply the change
473 kubectl "${kube_flags[@]}" apply -f hack/testdata/CRD/multi-crd-list-deleted-field.yaml
474
475 # Test that apply has deleted the fields
476 kube::test::get_object_assert foos/test-list '{{.otherField}}' '<no value>'
477 kube::test::get_object_assert bars/test-list '{{.otherField}}' '<no value>'
478
479 # Test that the fields does not exist
480 kube::test::get_object_assert foos/test-list '{{.newField}}' '<no value>'
481 kube::test::get_object_assert bars/test-list '{{.newField}}' '<no value>'
482
483 # Add a field and then apply the change
484 kubectl "${kube_flags[@]}" apply -f hack/testdata/CRD/multi-crd-list-added-field.yaml
485
486 # Test that apply has added the field
487 kube::test::get_object_assert foos/test-list '{{.newField}}' 'field3'
488 kube::test::get_object_assert bars/test-list '{{.newField}}' 'field3'
489
490 # Delete the resource
491 kubectl "${kube_flags[@]}" delete -f hack/testdata/CRD/multi-crd-list.yaml
492
493 # Make sure it's gone
494 kube::test::get_object_assert foos "{{range.items}}{{$id_field}}:{{end}}" ''
495 kube::test::get_object_assert bars "{{range.items}}{{$id_field}}:{{end}}" ''
496
497 ## kubectl apply --prune
498 # Test that no foo or bar exist
499 kube::test::get_object_assert foos "{{range.items}}{{$id_field}}:{{end}}" ''
500 kube::test::get_object_assert bars "{{range.items}}{{$id_field}}:{{end}}" ''
501
502 # apply --prune on foo.yaml that has foo/test
503 kubectl apply --prune -l pruneGroup=true -f hack/testdata/CRD/foo.yaml "${kube_flags[@]}" --prune-allowlist=company.com/v1/Foo --prune-allowlist=company.com/v1/Bar
504 # check right crds exist
505 kube::test::get_object_assert foos "{{range.items}}{{$id_field}}:{{end}}" 'test:'
506 kube::test::get_object_assert bars "{{range.items}}{{$id_field}}:{{end}}" ''
507
508 # apply --prune on bar.yaml that has bar/test
509 kubectl apply --prune -l pruneGroup=true -f hack/testdata/CRD/bar.yaml "${kube_flags[@]}" --prune-allowlist=company.com/v1/Foo --prune-allowlist=company.com/v1/Bar
510 # check right crds exist
511 kube::test::wait_object_assert foos "{{range.items}}{{$id_field}}:{{end}}" ''
512 kube::test::get_object_assert bars "{{range.items}}{{$id_field}}:{{end}}" 'test:'
513
514 # Delete the resource
515 kubectl "${kube_flags[@]}" delete -f hack/testdata/CRD/bar.yaml
516
517 # Make sure it's gone
518 kube::test::get_object_assert foos "{{range.items}}{{$id_field}}:{{end}}" ''
519 kube::test::get_object_assert bars "{{range.items}}{{$id_field}}:{{end}}" ''
520
521 # Test 'kubectl create' with namespace, and namespace cleanup.
522 kubectl "${kube_flags[@]}" create namespace non-native-resources
523 kubectl "${kube_flags[@]}" create -f hack/testdata/CRD/bar.yaml --namespace=non-native-resources
524 kube::test::get_object_assert bars '{{len .items}}' '1' --namespace=non-native-resources
525 kubectl "${kube_flags[@]}" delete namespace non-native-resources
526 # Make sure objects go away.
527 kube::test::wait_object_assert bars '{{len .items}}' '0' --namespace=non-native-resources
528 # Make sure namespace goes away.
529 local tries=0
530 while kubectl "${kube_flags[@]}" get namespace non-native-resources && [ ${tries} -lt 10 ]; do
531 tries=$((tries+1))
532 sleep ${tries}
533 done
534
535 set +o nounset
536 set +o errexit
537}
View as plain text