Feature 10239: Distributed VCA
[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://charts.helm.sh/stable")
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.kdu_instance = "stable-openldap-0005399828"
138 self.helm_conn.generate_kdu_instance_name = Mock(return_value=self.kdu_instance)
139 self.helm_conn._get_namespaces = asynctest.CoroutineMock(return_value=[])
140 self.helm_conn._create_namespace = asynctest.CoroutineMock()
141
142 await self.helm_conn.install(
143 self.cluster_uuid,
144 kdu_model,
145 self.kdu_instance,
146 atomic=True,
147 namespace=self.namespace,
148 db_dict=db_dict
149 )
150
151 self.helm_conn._get_namespaces.assert_called_once()
152 self.helm_conn._create_namespace.assert_called_once_with(self.cluster_id, self.namespace)
153 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
154 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
155 self.helm_conn._store_status.assert_called_with(cluster_id=self.cluster_id,
156 kdu_instance=kdu_instance,
157 namespace=self.namespace,
158 db_dict=db_dict,
159 operation="install",
160 run_once=True,
161 check_every=0)
162 command = "/usr/bin/helm3 install stable-openldap-0005399828 --atomic --output yaml " \
163 "--timeout 300s --namespace testk8s stable/openldap --version 1.2.2"
164 self.helm_conn._local_async_exec.assert_called_once_with(command=command,
165 env=self.env,
166 raise_exception_on_error=False)
167
168 @asynctest.fail_on(active_handles=True)
169 async def test_upgrade(self):
170 kdu_model = "stable/openldap:1.2.3"
171 kdu_instance = "stable-openldap-0005399828"
172 db_dict = {}
173 instance_info = {
174 "chart": "openldap-1.2.2",
175 "name": kdu_instance,
176 "namespace": self.namespace,
177 "revision": 1,
178 "status": "DEPLOYED"
179 }
180 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
181 self.helm_conn._store_status = asynctest.CoroutineMock()
182 self.helm_conn.get_instance_info = asynctest.CoroutineMock(return_value=instance_info)
183
184 await self.helm_conn.upgrade(self.cluster_uuid,
185 kdu_instance,
186 kdu_model,
187 atomic=True,
188 db_dict=db_dict)
189 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
190 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
191 self.helm_conn._store_status.assert_called_with(cluster_id=self.cluster_id,
192 kdu_instance=kdu_instance,
193 namespace=self.namespace,
194 db_dict=db_dict,
195 operation="upgrade",
196 run_once=True,
197 check_every=0)
198 command = "/usr/bin/helm3 upgrade stable-openldap-0005399828 stable/openldap " \
199 "--namespace testk8s --atomic --output yaml --timeout 300s " \
200 "--version 1.2.3"
201 self.helm_conn._local_async_exec.assert_called_once_with(command=command,
202 env=self.env,
203 raise_exception_on_error=False)
204
205 @asynctest.fail_on(active_handles=True)
206 async def test_rollback(self):
207 kdu_instance = "stable-openldap-0005399828"
208 db_dict = {}
209 instance_info = {
210 "chart": "openldap-1.2.3",
211 "name": kdu_instance,
212 "namespace": self.namespace,
213 "revision": 2,
214 "status": "DEPLOYED"
215 }
216 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
217 self.helm_conn._store_status = asynctest.CoroutineMock()
218 self.helm_conn.get_instance_info = asynctest.CoroutineMock(return_value=instance_info)
219
220 await self.helm_conn.rollback(self.cluster_uuid,
221 kdu_instance=kdu_instance,
222 revision=1,
223 db_dict=db_dict)
224 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
225 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
226 self.helm_conn._store_status.assert_called_with(cluster_id=self.cluster_id,
227 kdu_instance=kdu_instance,
228 namespace=self.namespace,
229 db_dict=db_dict,
230 operation="rollback",
231 run_once=True,
232 check_every=0)
233 command = "/usr/bin/helm3 rollback stable-openldap-0005399828 1 --namespace=testk8s --wait"
234 self.helm_conn._local_async_exec.assert_called_once_with(command=command,
235 env=self.env,
236 raise_exception_on_error=False)
237
238 @asynctest.fail_on(active_handles=True)
239 async def test_uninstall(self):
240 kdu_instance = "stable-openldap-0005399828"
241 instance_info = {
242 "chart": "openldap-1.2.2",
243 "name": kdu_instance,
244 "namespace": self.namespace,
245 "revision": 3,
246 "status": "DEPLOYED"
247 }
248 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
249 self.helm_conn._store_status = asynctest.CoroutineMock()
250 self.helm_conn.get_instance_info = asynctest.CoroutineMock(return_value=instance_info)
251
252 await self.helm_conn.uninstall(self.cluster_uuid, kdu_instance)
253 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
254 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
255 command = "/usr/bin/helm3 uninstall {} --namespace={}".format(
256 kdu_instance, self.namespace)
257 self.helm_conn._local_async_exec.assert_called_once_with(command=command,
258 env=self.env,
259 raise_exception_on_error=True)
260
261 @asynctest.fail_on(active_handles=True)
262 async def test_get_services(self):
263 kdu_instance = "test_services_1"
264 service = {
265 "name": "testservice",
266 "type": "LoadBalancer"
267 }
268 self.helm_conn._local_async_exec_pipe = asynctest.CoroutineMock(return_value=("", 0))
269 self.helm_conn._parse_services = Mock(return_value=["testservice"])
270 self.helm_conn._get_service = asynctest.CoroutineMock(return_value=service)
271
272 services = await self.helm_conn.get_services(self.cluster_uuid, kdu_instance,
273 self.namespace)
274 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
275 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
276 self.helm_conn._parse_services.assert_called_once()
277 command1 = "/usr/bin/helm3 get manifest {} --namespace=testk8s".format(kdu_instance)
278 command2 = "/usr/bin/kubectl get --namespace={} -f -".format(self.namespace)
279 self.helm_conn._local_async_exec_pipe.assert_called_once_with(command1, command2,
280 env=self.env,
281 raise_exception_on_error=True)
282 self.assertEqual(services, [service], "Invalid service returned from get_service")
283
284 @asynctest.fail_on(active_handles=True)
285 async def test_get_service(self):
286 service_name = "service1"
287
288 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
289 await self.helm_conn.get_service(self.cluster_uuid, service_name, self.namespace)
290
291 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
292 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
293 command = "/usr/bin/kubectl --kubeconfig=./tmp/helm3_cluster_id/.kube/config " \
294 "--namespace=testk8s get service service1 -o=yaml"
295 self.helm_conn._local_async_exec.assert_called_once_with(command=command,
296 env=self.env,
297 raise_exception_on_error=True)
298
299 @asynctest.fail_on(active_handles=True)
300 async def test_inspect_kdu(self):
301 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
302
303 kdu_model = "stable/openldap:1.2.4"
304 repo_url = "https://kubernetes-charts.storage.googleapis.com/"
305 await self.helm_conn.inspect_kdu(kdu_model, repo_url)
306
307 command = "/usr/bin/helm3 show all openldap --repo " \
308 "https://kubernetes-charts.storage.googleapis.com/ " \
309 "--version 1.2.4"
310 self.helm_conn._local_async_exec.assert_called_with(command=command, encode_utf8=True)
311
312 @asynctest.fail_on(active_handles=True)
313 async def test_help_kdu(self):
314 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
315
316 kdu_model = "stable/openldap:1.2.4"
317 repo_url = "https://kubernetes-charts.storage.googleapis.com/"
318 await self.helm_conn.help_kdu(kdu_model, repo_url)
319
320 command = "/usr/bin/helm3 show readme openldap --repo " \
321 "https://kubernetes-charts.storage.googleapis.com/ " \
322 "--version 1.2.4"
323 self.helm_conn._local_async_exec.assert_called_with(command=command, encode_utf8=True)
324
325 @asynctest.fail_on(active_handles=True)
326 async def test_values_kdu(self):
327 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
328
329 kdu_model = "stable/openldap:1.2.4"
330 repo_url = "https://kubernetes-charts.storage.googleapis.com/"
331 await self.helm_conn.values_kdu(kdu_model, repo_url)
332
333 command = "/usr/bin/helm3 show values openldap --repo " \
334 "https://kubernetes-charts.storage.googleapis.com/ " \
335 "--version 1.2.4"
336 self.helm_conn._local_async_exec.assert_called_with(command=command, encode_utf8=True)
337
338 @asynctest.fail_on(active_handles=True)
339 async def test_instances_list(self):
340 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
341
342 await self.helm_conn.instances_list(self.cluster_uuid)
343 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
344 self.helm_conn.fs.reverse_sync.assert_called_once_with(from_path=self.cluster_id)
345 command = "/usr/bin/helm3 list --all-namespaces --output yaml"
346 self.helm_conn._local_async_exec.assert_called_once_with(command=command,
347 env=self.env,
348 raise_exception_on_error=True)
349
350 @asynctest.fail_on(active_handles=True)
351 async def test_status_kdu(self):
352 kdu_instance = "stable-openldap-0005399828"
353 self.helm_conn._local_async_exec = asynctest.CoroutineMock(return_value=("", 0))
354
355 await self.helm_conn._status_kdu(self.cluster_id, kdu_instance,
356 self.namespace, return_text=True)
357 command = "/usr/bin/helm3 status {} --namespace={} --output yaml".format(
358 kdu_instance, self.namespace
359 )
360 self.helm_conn._local_async_exec.assert_called_once_with(command=command,
361 env=self.env,
362 raise_exception_on_error=True,
363 show_error_log=False)
364
365 @asynctest.fail_on(active_handles=True)
366 async def test_store_status(self):
367 kdu_instance = "stable-openldap-0005399828"
368 db_dict = {}
369 status = {
370 "info": {
371 "description": "Install complete",
372 "status": {
373 "code": "1",
374 "notes": "The openldap helm chart has been installed"
375 }
376 }
377 }
378 self.helm_conn._status_kdu = asynctest.CoroutineMock(return_value=status)
379 self.helm_conn.write_app_status_to_db = asynctest.CoroutineMock(return_value=status)
380
381 await self.helm_conn._store_status(cluster_id=self.cluster_id,
382 kdu_instance=kdu_instance,
383 namespace=self.namespace,
384 db_dict=db_dict,
385 operation="install",
386 run_once=True,
387 check_every=0)
388 self.helm_conn._status_kdu.assert_called_once_with(cluster_id=self.cluster_id,
389 kdu_instance=kdu_instance,
390 namespace=self.namespace,
391 return_text=False)
392 self.helm_conn.write_app_status_to_db.assert_called_once_with(db_dict=db_dict,
393 status="Install complete",
394 detailed_status=str(status),
395 operation="install")
396
397 @asynctest.fail_on(active_handles=True)
398 async def test_reset_uninstall_false(self):
399 self.helm_conn._uninstall_sw = asynctest.CoroutineMock()
400
401 await self.helm_conn.reset(self.cluster_uuid, force=False, uninstall_sw=False)
402 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
403 self.helm_conn.fs.file_delete.assert_called_once_with(self.cluster_id,
404 ignore_non_exist=True)
405 self.helm_conn._uninstall_sw.assert_not_called()
406
407 @asynctest.fail_on(active_handles=True)
408 async def test_reset_uninstall(self):
409 kdu_instance = 'stable-openldap-0021099429'
410 instances = [
411 {
412 'app_version': '2.4.48',
413 'chart': 'openldap-1.2.3',
414 'name': kdu_instance,
415 'namespace': self.namespace,
416 'revision': '1',
417 'status': 'deployed',
418 'updated': '2020-10-30 11:11:20.376744191 +0000 UTC'
419 }
420 ]
421 self.helm_conn._uninstall_sw = asynctest.CoroutineMock()
422 self.helm_conn.instances_list = asynctest.CoroutineMock(return_value=instances)
423 self.helm_conn.uninstall = asynctest.CoroutineMock()
424
425 await self.helm_conn.reset(self.cluster_uuid, force=True, uninstall_sw=True)
426 self.helm_conn.fs.sync.assert_called_once_with(from_path=self.cluster_id)
427 self.helm_conn.fs.file_delete.assert_called_once_with(self.cluster_id,
428 ignore_non_exist=True)
429 self.helm_conn.instances_list.assert_called_once_with(cluster_uuid=self.cluster_uuid)
430 self.helm_conn.uninstall.assert_called_once_with(cluster_uuid=self.cluster_uuid,
431 kdu_instance=kdu_instance)
432 self.helm_conn._uninstall_sw.assert_called_once_with(self.cluster_id, self.namespace)
433
434 @asynctest.fail_on(active_handles=True)
435 async def test_sync_repos_add(self):
436 repo_list = [
437 {
438 "name": "stable",
439 "url": "https://kubernetes-charts.storage.googleapis.com/"
440 }
441 ]
442 self.helm_conn.repo_list = asynctest.CoroutineMock(return_value=repo_list)
443
444 def get_one_result(*args, **kwargs):
445 if args[0] == "k8sclusters":
446 return {
447 "_admin": {
448 "helm_chart_repos": [
449 "4b5550a9-990d-4d95-8a48-1f4614d6ac9c"
450 ]
451 }
452 }
453 elif args[0] == "k8srepos":
454 return {
455 "_id": "4b5550a9-990d-4d95-8a48-1f4614d6ac9c",
456 "type": "helm-chart",
457 "name": "bitnami",
458 "url": "https://charts.bitnami.com/bitnami"
459 }
460 self.helm_conn.db.get_one = asynctest.Mock()
461 self.helm_conn.db.get_one.side_effect = get_one_result
462
463 self.helm_conn.repo_add = asynctest.CoroutineMock()
464 self.helm_conn.repo_remove = asynctest.CoroutineMock()
465
466 deleted_repo_list, added_repo_dict = await self.helm_conn.synchronize_repos(
467 self.cluster_uuid)
468 self.helm_conn.repo_remove.assert_not_called()
469 self.helm_conn.repo_add.assert_called_once_with(self.cluster_uuid, "bitnami",
470 "https://charts.bitnami.com/bitnami")
471 self.assertEqual(deleted_repo_list, [], "Deleted repo list should be empty")
472 self.assertEqual(added_repo_dict,
473 {"4b5550a9-990d-4d95-8a48-1f4614d6ac9c": "bitnami"},
474 "Repos added should include only one bitnami")
475
476 @asynctest.fail_on(active_handles=True)
477 async def test_sync_repos_delete(self):
478 repo_list = [
479 {
480 "name": "stable",
481 "url": "https://kubernetes-charts.storage.googleapis.com/"
482 },
483 {
484 "name": "bitnami",
485 "url": "https://charts.bitnami.com/bitnami"
486 }
487 ]
488 self.helm_conn.repo_list = asynctest.CoroutineMock(return_value=repo_list)
489
490 def get_one_result(*args, **kwargs):
491 if args[0] == "k8sclusters":
492 return {
493 "_admin": {
494 "helm_chart_repos": []
495 }
496 }
497
498 self.helm_conn.db.get_one = asynctest.Mock()
499 self.helm_conn.db.get_one.side_effect = get_one_result
500
501 self.helm_conn.repo_add = asynctest.CoroutineMock()
502 self.helm_conn.repo_remove = asynctest.CoroutineMock()
503
504 deleted_repo_list, added_repo_dict = await self.helm_conn.synchronize_repos(
505 self.cluster_uuid)
506 self.helm_conn.repo_add.assert_not_called()
507 self.helm_conn.repo_remove.assert_called_once_with(self.cluster_uuid, "bitnami")
508 self.assertEqual(deleted_repo_list, ["bitnami"], "Deleted repo list should be bitnami")
509 self.assertEqual(added_repo_dict, {}, "No repos should be added")