Support for helm v3
[osm/N2VC.git] / n2vc / tests / unit / test_k8s_helm3_conn.py
1 ##
2 # Licensed under the Apache License, Version 2.0 (the "License"); you may
3 # not use this file except in compliance with the License. You may obtain
4 # a copy of the License at
5 #
6 # http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11 # License for the specific language governing permissions and limitations
12 # under the License.
13 #
14 # For those usages not covered by the Apache License, Version 2.0 please
15 # contact: alfonso.tiernosepulveda@telefonica.com
16 ##
17
18 import asynctest
19 import logging
20
21 from asynctest.mock import Mock
22 from osm_common.dbmemory import DbMemory
23 from osm_common.fslocal import FsLocal
24 from n2vc.k8s_helm3_conn import K8sHelm3Connector
25
26 __author__ = "Isabel Lloret <illoret@indra.es>"
27
28
29 class TestK8sHelm3Conn(asynctest.TestCase):
30 logging.basicConfig(level=logging.DEBUG)
31 logger = logging.getLogger(__name__)
32 logger.setLevel(logging.DEBUG)
33
34 async def setUp(self):
35 self.db = Mock(DbMemory())
36 self.fs = asynctest.Mock(FsLocal())
37 self.fs.path = "./tmp/"
38 self.namespace = "testk8s"
39 self.cluster_id = "helm3_cluster_id"
40 self.cluster_uuid = "{}:{}".format(self.namespace, self.cluster_id)
41 # pass fake kubectl and helm commands to make sure it does not call actual commands
42 K8sHelm3Connector._check_file_exists = asynctest.Mock(return_value=True)
43 cluster_dir = self.fs.path + self.cluster_id
44 self.env = {
45 "HELM_CACHE_HOME": "{}/.cache/helm".format(cluster_dir),
46 "HELM_CONFIG_HOME": "{}/.config/helm".format(cluster_dir),
47 "HELM_DATA_HOME": "{}/.local/share/helm".format(cluster_dir),
48 "KUBECONFIG": "{}/.kube/config".format(cluster_dir)
49 }
50 self.helm_conn = K8sHelm3Connector(self.fs, self.db,
51 log=self.logger)
52 self.logger.debug("Set up executed")
53
54 @asynctest.fail_on(active_handles=True)
55 async def test_init_env(self):
56 k8s_creds = "false_credentials_string"
57 self.helm_conn._get_namespaces = asynctest.CoroutineMock(return_value=[])
58 self.helm_conn._create_namespace = asynctest.CoroutineMock()
59 self.helm_conn.repo_list = asynctest.CoroutineMock(return_value=[])
60 self.helm_conn.repo_add = asynctest.CoroutineMock()
61
62 k8scluster_uuid, installed = await self.helm_conn.init_env(
63 k8s_creds, namespace=self.namespace, reuse_cluster_uuid=self.cluster_id)
64
65 self.assertEqual(k8scluster_uuid, "{}:{}".format(self.namespace, self.cluster_id),
66 "Check cluster_uuid format: <namespace>.<cluster_id>")
67 self.helm_conn._get_namespaces.assert_called_once_with(self.cluster_id)
68 self.helm_conn._create_namespace.assert_called_once_with(self.cluster_id, self.namespace)
69 self.helm_conn.repo_list.assert_called_once_with(k8scluster_uuid)
70 self.helm_conn.repo_add.assert_called_once_with(
71 k8scluster_uuid, "stable", "https://kubernetes-charts.storage.googleapis.com/")
72 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
73 self.logger.debug(f"cluster_uuid: {k8scluster_uuid}")
74
75 @asynctest.fail_on(active_handles=True)
76 async def test_repo_add(self):
77 repo_name = "bitnami"
78 repo_url = "https://charts.bitnami.com/bitnami"
79 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
80
81 await self.helm_conn.repo_add(self.cluster_uuid, repo_name, repo_url)
82
83 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
84 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
85 self.assertEqual(self.helm_conn._local_async_exec.call_count, 2,
86 "local_async_exec expected 2 calls, called {}".format(
87 self.helm_conn._local_async_exec.call_count))
88
89 repo_update_command = "/usr/bin/helm3 repo update"
90 repo_add_command = "/usr/bin/helm3 repo add {} {}".format(repo_name, repo_url)
91 calls = self.helm_conn._local_async_exec.call_args_list
92 call0_kargs = calls[0][1]
93 self.assertEqual(call0_kargs.get("command"), repo_update_command,
94 "Invalid repo update command: {}".format(call0_kargs.get("command")))
95 self.assertEqual(call0_kargs.get("env"), self.env,
96 "Invalid env for update command: {}".format(call0_kargs.get("env")))
97 call1_kargs = calls[1][1]
98 self.assertEqual(call1_kargs.get("command"), repo_add_command,
99 "Invalid repo add command: {}".format(call1_kargs.get("command")))
100 self.assertEqual(call1_kargs.get("env"), self.env,
101 "Invalid env for add command: {}".format(call1_kargs.get("env")))
102
103 @asynctest.fail_on(active_handles=True)
104 async def test_repo_list(self):
105
106 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
107
108 await self.helm_conn.repo_list(self.cluster_uuid)
109
110 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
111 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
112 command = "/usr/bin/helm3 repo list --output yaml"
113 self.helm_conn._local_async_exec.assert_called_with(command=command, env=self.env,
114 raise_exception_on_error=False)
115
116 @asynctest.fail_on(active_handles=True)
117 async def test_repo_remove(self):
118
119 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
120 repo_name = "bitnami"
121 await self.helm_conn.repo_remove(self.cluster_uuid, repo_name)
122
123 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
124 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
125 command = "/usr/bin/helm3 repo remove {}".format(repo_name)
126 self.helm_conn._local_async_exec.assert_called_with(command=command, env=self.env,
127 raise_exception_on_error=True)
128
129 @asynctest.fail_on(active_handles=True)
130 async def test_install(self):
131 kdu_model = "stable/openldap:1.2.2"
132 kdu_instance = "stable-openldap-0005399828"
133 db_dict = {}
134 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
135 self.helm_conn._status_kdu = asynctest.CoroutineMock(return_value=None)
136 self.helm_conn._store_status = asynctest.CoroutineMock()
137 self.helm_conn._generate_release_name = Mock(return_value="stable-openldap-0005399828")
138
139 kdu_instance = await self.helm_conn.install(self.cluster_uuid,
140 kdu_model,
141 atomic=True,
142 namespace=self.namespace,
143 db_dict=db_dict)
144
145 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
146 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
147 self.helm_conn._generate_release_name.assert_called_once_with("stable/openldap")
148 self.helm_conn._status_kdu.assert_called_once_with(cluster_id=self.cluster_id,
149 kdu_instance=kdu_instance,
150 namespace=self.namespace,
151 show_error_log=False)
152 self.helm_conn._store_status.assert_called_with(cluster_id=self.cluster_id,
153 kdu_instance=kdu_instance,
154 namespace=self.namespace,
155 db_dict=db_dict,
156 operation="install",
157 run_once=True,
158 check_every=0)
159 command = "/usr/bin/helm3 install stable-openldap-0005399828 --atomic --output yaml " \
160 "--timeout 300s --namespace testk8s stable/openldap --version 1.2.2"
161 self.helm_conn._local_async_exec.assert_called_once_with(command=command,
162 env=self.env,
163 raise_exception_on_error=False)
164
165 @asynctest.fail_on(active_handles=True)
166 async def test_upgrade(self):
167 kdu_model = "stable/openldap:1.2.3"
168 kdu_instance = "stable-openldap-0005399828"
169 db_dict = {}
170 instance_info = {
171 "chart": "openldap-1.2.2",
172 "name": kdu_instance,
173 "namespace": self.namespace,
174 "revision": 1,
175 "status": "DEPLOYED"
176 }
177 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
178 self.helm_conn._store_status = asynctest.CoroutineMock()
179 self.helm_conn.get_instance_info = asynctest.CoroutineMock(return_value=instance_info)
180
181 await self.helm_conn.upgrade(self.cluster_uuid,
182 kdu_instance,
183 kdu_model,
184 atomic=True,
185 db_dict=db_dict)
186 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
187 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
188 self.helm_conn._store_status.assert_called_with(cluster_id=self.cluster_id,
189 kdu_instance=kdu_instance,
190 namespace=self.namespace,
191 db_dict=db_dict,
192 operation="upgrade",
193 run_once=True,
194 check_every=0)
195 command = "/usr/bin/helm3 upgrade stable-openldap-0005399828 stable/openldap " \
196 "--namespace testk8s --atomic --output yaml --timeout 300s " \
197 "--version 1.2.3"
198 self.helm_conn._local_async_exec.assert_called_once_with(command=command,
199 env=self.env,
200 raise_exception_on_error=False)
201
202 @asynctest.fail_on(active_handles=True)
203 async def test_rollback(self):
204 kdu_instance = "stable-openldap-0005399828"
205 db_dict = {}
206 instance_info = {
207 "chart": "openldap-1.2.3",
208 "name": kdu_instance,
209 "namespace": self.namespace,
210 "revision": 2,
211 "status": "DEPLOYED"
212 }
213 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
214 self.helm_conn._store_status = asynctest.CoroutineMock()
215 self.helm_conn.get_instance_info = asynctest.CoroutineMock(return_value=instance_info)
216
217 await self.helm_conn.rollback(self.cluster_uuid,
218 kdu_instance=kdu_instance,
219 revision=1,
220 db_dict=db_dict)
221 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
222 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
223 self.helm_conn._store_status.assert_called_with(cluster_id=self.cluster_id,
224 kdu_instance=kdu_instance,
225 namespace=self.namespace,
226 db_dict=db_dict,
227 operation="rollback",
228 run_once=True,
229 check_every=0)
230 command = "/usr/bin/helm3 rollback stable-openldap-0005399828 1 --namespace=testk8s --wait"
231 self.helm_conn._local_async_exec.assert_called_once_with(command=command,
232 env=self.env,
233 raise_exception_on_error=False)
234
235 @asynctest.fail_on(active_handles=True)
236 async def test_uninstall(self):
237 kdu_instance = "stable-openldap-0005399828"
238 instance_info = {
239 "chart": "openldap-1.2.2",
240 "name": kdu_instance,
241 "namespace": self.namespace,
242 "revision": 3,
243 "status": "DEPLOYED"
244 }
245 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
246 self.helm_conn._store_status = asynctest.CoroutineMock()
247 self.helm_conn.get_instance_info = asynctest.CoroutineMock(return_value=instance_info)
248
249 await self.helm_conn.uninstall(self.cluster_uuid, kdu_instance)
250 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
251 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
252 command = "/usr/bin/helm3 uninstall {} --namespace={}".format(
253 kdu_instance, self.namespace)
254 self.helm_conn._local_async_exec.assert_called_once_with(command=command,
255 env=self.env,
256 raise_exception_on_error=True)
257
258 @asynctest.fail_on(active_handles=True)
259 async def test_get_services(self):
260 kdu_instance = "test_services_1"
261 service = {
262 "name": "testservice",
263 "type": "LoadBalancer"
264 }
265 self.helm_conn._local_async_exec_pipe = asynctest.CoroutineMock(return_value=("", 0))
266 self.helm_conn._parse_services = Mock(return_value=["testservice"])
267 self.helm_conn._get_service = asynctest.CoroutineMock(return_value=service)
268
269 services = await self.helm_conn.get_services(self.cluster_uuid, kdu_instance,
270 self.namespace)
271 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
272 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
273 self.helm_conn._parse_services.assert_called_once()
274 command1 = "/usr/bin/helm3 get manifest {} --namespace=testk8s".format(kdu_instance)
275 command2 = "/usr/bin/kubectl get --namespace={} -f -".format(self.namespace)
276 self.helm_conn._local_async_exec_pipe.assert_called_once_with(command1, command2,
277 env=self.env,
278 raise_exception_on_error=True)
279 self.assertEqual(services, [service], "Invalid service returned from get_service")
280
281 @asynctest.fail_on(active_handles=True)
282 async def test_get_service(self):
283 service_name = "service1"
284
285 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
286 await self.helm_conn.get_service(self.cluster_uuid, service_name, self.namespace)
287
288 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
289 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
290 command = "/usr/bin/kubectl --kubeconfig=./tmp/helm3_cluster_id/.kube/config " \
291 "--namespace=testk8s get service service1 -o=yaml"
292 self.helm_conn._local_async_exec.assert_called_once_with(command=command,
293 env=self.env,
294 raise_exception_on_error=True)
295
296 @asynctest.fail_on(active_handles=True)
297 async def test_inspect_kdu(self):
298 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
299
300 kdu_model = "stable/openldap:1.2.4"
301 repo_url = "https://kubernetes-charts.storage.googleapis.com/"
302 await self.helm_conn.inspect_kdu(kdu_model, repo_url)
303
304 command = "/usr/bin/helm3 show all openldap --repo " \
305 "https://kubernetes-charts.storage.googleapis.com/ " \
306 "--version 1.2.4"
307 self.helm_conn._local_async_exec.assert_called_with(command=command, encode_utf8=True)
308
309 @asynctest.fail_on(active_handles=True)
310 async def test_help_kdu(self):
311 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
312
313 kdu_model = "stable/openldap:1.2.4"
314 repo_url = "https://kubernetes-charts.storage.googleapis.com/"
315 await self.helm_conn.help_kdu(kdu_model, repo_url)
316
317 command = "/usr/bin/helm3 show readme openldap --repo " \
318 "https://kubernetes-charts.storage.googleapis.com/ " \
319 "--version 1.2.4"
320 self.helm_conn._local_async_exec.assert_called_with(command=command, encode_utf8=True)
321
322 @asynctest.fail_on(active_handles=True)
323 async def test_values_kdu(self):
324 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
325
326 kdu_model = "stable/openldap:1.2.4"
327 repo_url = "https://kubernetes-charts.storage.googleapis.com/"
328 await self.helm_conn.values_kdu(kdu_model, repo_url)
329
330 command = "/usr/bin/helm3 show values openldap --repo " \
331 "https://kubernetes-charts.storage.googleapis.com/ " \
332 "--version 1.2.4"
333 self.helm_conn._local_async_exec.assert_called_with(command=command, encode_utf8=True)
334
335 @asynctest.fail_on(active_handles=True)
336 async def test_instances_list(self):
337 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
338
339 await self.helm_conn.instances_list(self.cluster_uuid)
340 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
341 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
342 command = "/usr/bin/helm3 list --all-namespaces --output yaml"
343 self.helm_conn._local_async_exec.assert_called_once_with(command=command,
344 env=self.env,
345 raise_exception_on_error=True)
346
347 @asynctest.fail_on(active_handles=True)
348 async def test_status_kdu(self):
349 kdu_instance = "stable-openldap-0005399828"
350 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
351
352 await self.helm_conn._status_kdu(self.cluster_id, kdu_instance,
353 self.namespace, return_text=True)
354 command = "/usr/bin/helm3 status {} --namespace={} --output yaml".format(
355 kdu_instance, self.namespace
356 )
357 self.helm_conn._local_async_exec.assert_called_once_with(command=command,
358 env=self.env,
359 raise_exception_on_error=True,
360 show_error_log=False)
361
362 @asynctest.fail_on(active_handles=True)
363 async def test_store_status(self):
364 kdu_instance = "stable-openldap-0005399828"
365 db_dict = {}
366 status = {
367 "info": {
368 "description": "Install complete",
369 "status": {
370 "code": "1",
371 "notes": "The openldap helm chart has been installed"
372 }
373 }
374 }
375 self.helm_conn._status_kdu = asynctest.CoroutineMock(return_value=status)
376 self.helm_conn.write_app_status_to_db = asynctest.CoroutineMock(return_value=status)
377
378 await self.helm_conn._store_status(cluster_id=self.cluster_id,
379 kdu_instance=kdu_instance,
380 namespace=self.namespace,
381 db_dict=db_dict,
382 operation="install",
383 run_once=True,
384 check_every=0)
385 self.helm_conn._status_kdu.assert_called_once_with(cluster_id=self.cluster_id,
386 kdu_instance=kdu_instance,
387 namespace=self.namespace,
388 return_text=False)
389 self.helm_conn.write_app_status_to_db.assert_called_once_with(db_dict=db_dict,
390 status="Install complete",
391 detailed_status=str(status),
392 operation="install")
393
394 @asynctest.fail_on(active_handles=True)
395 async def test_reset_uninstall_false(self):
396 self.helm_conn._uninstall_sw = asynctest.CoroutineMock()
397
398 await self.helm_conn.reset(self.cluster_uuid, force=False, uninstall_sw=False)
399 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
400 self.helm_conn.fs.file_delete.assert_called_once_with(self.cluster_id,
401 ignore_non_exist=True)
402 self.helm_conn._uninstall_sw.assert_not_called()
403
404 @asynctest.fail_on(active_handles=True)
405 async def test_reset_uninstall(self):
406 kdu_instance = 'stable-openldap-0021099429'
407 instances = [
408 {
409 'app_version': '2.4.48',
410 'chart': 'openldap-1.2.3',
411 'name': kdu_instance,
412 'namespace': self.namespace,
413 'revision': '1',
414 'status': 'deployed',
415 'updated': '2020-10-30 11:11:20.376744191 +0000 UTC'
416 }
417 ]
418 self.helm_conn._uninstall_sw = asynctest.CoroutineMock()
419 self.helm_conn.instances_list = asynctest.CoroutineMock(return_value=instances)
420 self.helm_conn.uninstall = asynctest.CoroutineMock()
421
422 await self.helm_conn.reset(self.cluster_uuid, force=True, uninstall_sw=True)
423 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
424 self.helm_conn.fs.file_delete.assert_called_once_with(self.cluster_id,
425 ignore_non_exist=True)
426 self.helm_conn.instances_list.assert_called_once_with(cluster_uuid=self.cluster_uuid)
427 self.helm_conn.uninstall.assert_called_once_with(cluster_uuid=self.cluster_uuid,
428 kdu_instance=kdu_instance)
429 self.helm_conn._uninstall_sw.assert_called_once_with(self.cluster_id, self.namespace)
430
431 @asynctest.fail_on(active_handles=True)
432 async def test_sync_repos_add(self):
433 repo_list = [
434 {
435 "name": "stable",
436 "url": "https://kubernetes-charts.storage.googleapis.com/"
437 }
438 ]
439 self.helm_conn.repo_list = asynctest.CoroutineMock(return_value=repo_list)
440
441 def get_one_result(*args, **kwargs):
442 if args[0] == "k8sclusters":
443 return {
444 "_admin": {
445 "helm_chart_repos" : [
446 "4b5550a9-990d-4d95-8a48-1f4614d6ac9c"
447 ]
448 }
449 }
450 elif args[0] == "k8srepos":
451 return {
452 "_id": "4b5550a9-990d-4d95-8a48-1f4614d6ac9c",
453 "type": "helm-chart",
454 "name": "bitnami",
455 "url": "https://charts.bitnami.com/bitnami"
456 }
457 self.helm_conn.db.get_one = asynctest.Mock()
458 self.helm_conn.db.get_one.side_effect = get_one_result
459
460 self.helm_conn.repo_add = asynctest.CoroutineMock()
461 self.helm_conn.repo_remove = asynctest.CoroutineMock()
462
463 deleted_repo_list, added_repo_dict = await self.helm_conn.synchronize_repos(
464 self.cluster_uuid)
465 self.helm_conn.repo_remove.assert_not_called()
466 self.helm_conn.repo_add.assert_called_once_with(self.cluster_uuid, "bitnami",
467 "https://charts.bitnami.com/bitnami")
468 self.assertEqual(deleted_repo_list, [], "Deleted repo list should be empty")
469 self.assertEqual(added_repo_dict,
470 {"4b5550a9-990d-4d95-8a48-1f4614d6ac9c": "bitnami"},
471 "Repos added should include only one bitnami")
472
473 @asynctest.fail_on(active_handles=True)
474 async def test_sync_repos_delete(self):
475 repo_list = [
476 {
477 "name": "stable",
478 "url": "https://kubernetes-charts.storage.googleapis.com/"
479 },
480 {
481 "name": "bitnami",
482 "url": "https://charts.bitnami.com/bitnami"
483 }
484 ]
485 self.helm_conn.repo_list = asynctest.CoroutineMock(return_value=repo_list)
486
487 def get_one_result(*args, **kwargs):
488 if args[0] == "k8sclusters":
489 return {
490 "_admin": {
491 "helm_chart_repos": []
492 }
493 }
494
495 self.helm_conn.db.get_one = asynctest.Mock()
496 self.helm_conn.db.get_one.side_effect = get_one_result
497
498 self.helm_conn.repo_add = asynctest.CoroutineMock()
499 self.helm_conn.repo_remove = asynctest.CoroutineMock()
500
501 deleted_repo_list, added_repo_dict = await self.helm_conn.synchronize_repos(
502 self.cluster_uuid)
503 self.helm_conn.repo_add.assert_not_called()
504 self.helm_conn.repo_remove.assert_called_once_with(self.cluster_uuid, "bitnami")
505 self.assertEqual(deleted_repo_list, ["bitnami"], "Deleted repo list should be bitnami")
506 self.assertEqual(added_repo_dict, {}, "No repos should be added")