github.com/nginxinc/kubernetes-ingress@v1.12.5/tests/suite/resources_utils.py (about) 1 """Describe methods to utilize the kubernetes-client.""" 2 3 import time 4 import yaml 5 import pytest 6 import requests 7 8 from kubernetes.client import CoreV1Api, ExtensionsV1beta1Api, RbacAuthorizationV1Api, V1Service, AppsV1Api 9 from kubernetes.client.rest import ApiException 10 from kubernetes.stream import stream 11 from kubernetes import client 12 from more_itertools import first 13 14 from settings import TEST_DATA, RECONFIGURATION_DELAY, DEPLOYMENTS 15 16 17 class RBACAuthorization: 18 """ 19 Encapsulate RBAC details. 20 21 Attributes: 22 role (str): cluster role name 23 binding (str): cluster role binding name 24 """ 25 26 def __init__(self, role: str, binding: str): 27 self.role = role 28 self.binding = binding 29 30 31 def configure_rbac(rbac_v1: RbacAuthorizationV1Api) -> RBACAuthorization: 32 """ 33 Create cluster and binding. 34 35 :param rbac_v1: RbacAuthorizationV1Api 36 :return: RBACAuthorization 37 """ 38 with open(f'{DEPLOYMENTS}/rbac/rbac.yaml') as f: 39 docs = yaml.safe_load_all(f) 40 role_name = "" 41 binding_name = "" 42 for dep in docs: 43 if dep["kind"] == "ClusterRole": 44 print("Create cluster role") 45 role_name = dep['metadata']['name'] 46 rbac_v1.create_cluster_role(dep) 47 print(f"Created role '{role_name}'") 48 elif dep["kind"] == "ClusterRoleBinding": 49 print("Create binding") 50 binding_name = dep['metadata']['name'] 51 rbac_v1.create_cluster_role_binding(dep) 52 print(f"Created binding '{binding_name}'") 53 return RBACAuthorization(role_name, binding_name) 54 55 56 def configure_rbac_with_ap(rbac_v1: RbacAuthorizationV1Api) -> RBACAuthorization: 57 """ 58 Create cluster and binding for AppProtect module. 59 :param rbac_v1: RbacAuthorizationV1Api 60 :return: RBACAuthorization 61 """ 62 with open(f"{DEPLOYMENTS}/rbac/ap-rbac.yaml") as f: 63 docs = yaml.safe_load_all(f) 64 role_name = "" 65 binding_name = "" 66 for dep in docs: 67 if dep["kind"] == "ClusterRole": 68 print("Create cluster role for AppProtect") 69 role_name = dep["metadata"]["name"] 70 rbac_v1.create_cluster_role(dep) 71 print(f"Created role '{role_name}'") 72 elif dep["kind"] == "ClusterRoleBinding": 73 print("Create binding for AppProtect") 74 binding_name = dep["metadata"]["name"] 75 rbac_v1.create_cluster_role_binding(dep) 76 print(f"Created binding '{binding_name}'") 77 return RBACAuthorization(role_name, binding_name) 78 79 80 def patch_rbac(rbac_v1: RbacAuthorizationV1Api, yaml_manifest) -> RBACAuthorization: 81 """ 82 Patch a clusterrole and a binding. 83 84 :param rbac_v1: RbacAuthorizationV1Api 85 :param yaml_manifest: an absolute path to yaml manifest 86 :return: RBACAuthorization 87 """ 88 with open(yaml_manifest) as f: 89 docs = yaml.safe_load_all(f) 90 role_name = "" 91 binding_name = "" 92 for dep in docs: 93 if dep["kind"] == "ClusterRole": 94 print("Patch the cluster role") 95 role_name = dep['metadata']['name'] 96 rbac_v1.patch_cluster_role(role_name, dep) 97 print(f"Patched the role '{role_name}'") 98 elif dep["kind"] == "ClusterRoleBinding": 99 print("Patch the binding") 100 binding_name = dep['metadata']['name'] 101 rbac_v1.patch_cluster_role_binding(binding_name, dep) 102 print(f"Patched the binding '{binding_name}'") 103 return RBACAuthorization(role_name, binding_name) 104 105 106 def cleanup_rbac(rbac_v1: RbacAuthorizationV1Api, rbac: RBACAuthorization) -> None: 107 """ 108 Delete binding and cluster role. 109 110 :param rbac_v1: RbacAuthorizationV1Api 111 :param rbac: RBACAuthorization 112 :return: 113 """ 114 print("Delete binding and cluster role") 115 rbac_v1.delete_cluster_role_binding(rbac.binding) 116 rbac_v1.delete_cluster_role(rbac.role) 117 118 119 def create_deployment_from_yaml(apps_v1_api: AppsV1Api, namespace, yaml_manifest) -> str: 120 """ 121 Create a deployment based on yaml file. 122 123 :param apps_v1_api: AppsV1Api 124 :param namespace: namespace name 125 :param yaml_manifest: absolute path to file 126 :return: str 127 """ 128 print(f"Load {yaml_manifest}") 129 with open(yaml_manifest) as f: 130 dep = yaml.safe_load(f) 131 return create_deployment(apps_v1_api, namespace, dep) 132 133 134 def patch_deployment_from_yaml(apps_v1_api: AppsV1Api, namespace, yaml_manifest) -> str: 135 """ 136 Create a deployment based on yaml file. 137 138 :param apps_v1_api: AppsV1Api 139 :param namespace: namespace name 140 :param yaml_manifest: absolute path to file 141 :return: str 142 """ 143 print(f"Load {yaml_manifest}") 144 with open(yaml_manifest) as f: 145 dep = yaml.safe_load(f) 146 return patch_deployment(apps_v1_api, namespace, dep) 147 148 149 def patch_deployment(apps_v1_api: AppsV1Api, namespace, body) -> str: 150 """ 151 Create a deployment based on a dict. 152 153 :param apps_v1_api: AppsV1Api 154 :param namespace: namespace name 155 :param body: dict 156 :return: str 157 """ 158 print("Patch a deployment:") 159 apps_v1_api.patch_namespaced_deployment(body['metadata']['name'], namespace, body) 160 print(f"Deployment patched with name '{body['metadata']['name']}'") 161 return body['metadata']['name'] 162 163 164 def create_deployment(apps_v1_api: AppsV1Api, namespace, body) -> str: 165 """ 166 Create a deployment based on a dict. 167 168 :param apps_v1_api: AppsV1Api 169 :param namespace: namespace name 170 :param body: dict 171 :return: str 172 """ 173 print("Create a deployment:") 174 apps_v1_api.create_namespaced_deployment(namespace, body) 175 print(f"Deployment created with name '{body['metadata']['name']}'") 176 return body['metadata']['name'] 177 178 179 def create_deployment_with_name(apps_v1_api: AppsV1Api, namespace, name) -> str: 180 """ 181 Create a deployment with a specific name based on common yaml file. 182 183 :param apps_v1_api: AppsV1Api 184 :param namespace: namespace name 185 :param name: 186 :return: str 187 """ 188 print(f"Create a Deployment with a specific name") 189 with open(f"{TEST_DATA}/common/backend1.yaml") as f: 190 dep = yaml.safe_load(f) 191 dep['metadata']['name'] = name 192 dep['spec']['selector']['matchLabels']['app'] = name 193 dep['spec']['template']['metadata']['labels']['app'] = name 194 dep['spec']['template']['spec']['containers'][0]['name'] = name 195 return create_deployment(apps_v1_api, namespace, dep) 196 197 198 def scale_deployment(apps_v1_api: AppsV1Api, name, namespace, value) -> int: 199 """ 200 Scale a deployment. 201 202 :param apps_v1_api: AppsV1Api 203 :param namespace: namespace name 204 :param name: deployment name 205 :param value: int 206 :return: original: int the original amount of replicas 207 """ 208 print(f"Scale a deployment '{name}'") 209 body = apps_v1_api.read_namespaced_deployment_scale(name, namespace) 210 original = body.spec.replicas 211 body.spec.replicas = value 212 apps_v1_api.patch_namespaced_deployment_scale(name, namespace, body) 213 print(f"Scale a deployment '{name}': complete") 214 return original 215 216 217 def create_daemon_set(apps_v1_api: AppsV1Api, namespace, body) -> str: 218 """ 219 Create a daemon-set based on a dict. 220 221 :param apps_v1_api: AppsV1Api 222 :param namespace: namespace name 223 :param body: dict 224 :return: str 225 """ 226 print("Create a daemon-set:") 227 apps_v1_api.create_namespaced_daemon_set(namespace, body) 228 print(f"Daemon-Set created with name '{body['metadata']['name']}'") 229 return body['metadata']['name'] 230 231 232 def wait_until_all_pods_are_ready(v1: CoreV1Api, namespace) -> None: 233 """ 234 Wait for all the pods to be 'ContainersReady'. 235 236 :param v1: CoreV1Api 237 :param namespace: namespace of a pod 238 :return: 239 """ 240 print("Start waiting for all pods in a namespace to be ContainersReady") 241 counter = 0 242 while not are_all_pods_in_ready_state(v1, namespace) and counter < 20: 243 print("There are pods that are not ContainersReady. Wait for 4 sec...") 244 time.sleep(4) 245 counter = counter + 1 246 if counter >= 20: 247 pytest.fail("After several seconds the pods aren't ContainersReady. Exiting...") 248 print("All pods are ContainersReady") 249 250 251 def get_first_pod_name(v1: CoreV1Api, namespace) -> str: 252 """ 253 Return 1st pod_name in a list of pods in a namespace. 254 255 :param v1: CoreV1Api 256 :param namespace: 257 :return: str 258 """ 259 resp = v1.list_namespaced_pod(namespace) 260 return resp.items[0].metadata.name 261 262 263 def are_all_pods_in_ready_state(v1: CoreV1Api, namespace) -> bool: 264 """ 265 Check if all the pods have ContainersReady condition. 266 267 :param v1: CoreV1Api 268 :param namespace: namespace 269 :return: bool 270 """ 271 pods = v1.list_namespaced_pod(namespace) 272 if not pods.items: 273 return False 274 pod_ready_amount = 0 275 for pod in pods.items: 276 if pod.status.conditions is None: 277 return False 278 for condition in pod.status.conditions: 279 # wait for 'Ready' state instead of 'ContainersReady' for backwards compatibility with k8s 1.10 280 if condition.type == 'ContainersReady' and condition.status == 'True': 281 pod_ready_amount = pod_ready_amount + 1 282 break 283 return pod_ready_amount == len(pods.items) 284 285 286 def get_pods_amount(v1: CoreV1Api, namespace) -> int: 287 """ 288 Get an amount of pods. 289 290 :param v1: CoreV1Api 291 :param namespace: namespace 292 :return: int 293 """ 294 pods = v1.list_namespaced_pod(namespace) 295 return 0 if not pods.items else len(pods.items) 296 297 298 def create_service_from_yaml(v1: CoreV1Api, namespace, yaml_manifest) -> str: 299 """ 300 Create a service based on yaml file. 301 302 :param v1: CoreV1Api 303 :param namespace: namespace name 304 :param yaml_manifest: absolute path to file 305 :return: str 306 """ 307 print(f"Load {yaml_manifest}") 308 with open(yaml_manifest) as f: 309 dep = yaml.safe_load(f) 310 return create_service(v1, namespace, dep) 311 312 313 def create_service(v1: CoreV1Api, namespace, body) -> str: 314 """ 315 Create a service based on a dict. 316 317 :param v1: CoreV1Api 318 :param namespace: namespace 319 :param body: a dict 320 :return: str 321 """ 322 print("Create a Service:") 323 resp = v1.create_namespaced_service(namespace, body) 324 print(f"Service created with name '{body['metadata']['name']}'") 325 return resp.metadata.name 326 327 328 def create_service_with_name(v1: CoreV1Api, namespace, name) -> str: 329 """ 330 Create a service with a specific name based on a common yaml manifest. 331 332 :param v1: CoreV1Api 333 :param namespace: namespace name 334 :param name: name 335 :return: str 336 """ 337 print(f"Create a Service with a specific name:") 338 with open(f"{TEST_DATA}/common/backend1-svc.yaml") as f: 339 dep = yaml.safe_load(f) 340 dep['metadata']['name'] = name 341 dep['spec']['selector']['app'] = name.replace("-svc", "") 342 return create_service(v1, namespace, dep) 343 344 345 def get_service_node_ports(v1: CoreV1Api, name, namespace) -> (int, int, int, int, int, int): 346 """ 347 Get service allocated node_ports. 348 349 :param v1: CoreV1Api 350 :param name: 351 :param namespace: 352 :return: (plain_port, ssl_port, api_port, exporter_port) 353 """ 354 resp = v1.read_namespaced_service(name, namespace) 355 if len(resp.spec.ports) == 6: 356 print("An unexpected amount of ports in a service. Check the configuration") 357 print(f"Service with an API port: {resp.spec.ports[2].node_port}") 358 print(f"Service with an Exporter port: {resp.spec.ports[3].node_port}") 359 return resp.spec.ports[0].node_port, resp.spec.ports[1].node_port,\ 360 resp.spec.ports[2].node_port, resp.spec.ports[3].node_port, resp.spec.ports[4].node_port,\ 361 resp.spec.ports[5].node_port 362 363 364 def wait_for_public_ip(v1: CoreV1Api, namespace: str) -> str: 365 """ 366 Wait for LoadBalancer to get the public ip. 367 368 :param v1: CoreV1Api 369 :param namespace: namespace 370 :return: str 371 """ 372 resp = v1.list_namespaced_service(namespace) 373 counter = 0 374 svc_item = first(x for x in resp.items if x.metadata.name == "nginx-ingress") 375 while str(svc_item.status.load_balancer.ingress) == "None" and counter < 20: 376 time.sleep(5) 377 resp = v1.list_namespaced_service(namespace) 378 svc_item = first(x for x in resp.items if x.metadata.name == "nginx-ingress") 379 counter = counter + 1 380 if counter == 20: 381 pytest.fail("After 100 seconds the LB still doesn't have a Public IP. Exiting...") 382 print(f"Public IP ='{svc_item.status.load_balancer.ingress[0].ip}'") 383 return str(svc_item.status.load_balancer.ingress[0].ip) 384 385 386 def create_secret_from_yaml(v1: CoreV1Api, namespace, yaml_manifest) -> str: 387 """ 388 Create a secret based on yaml file. 389 390 :param v1: CoreV1Api 391 :param namespace: namespace name 392 :param yaml_manifest: an absolute path to file 393 :return: str 394 """ 395 print(f"Load {yaml_manifest}") 396 with open(yaml_manifest) as f: 397 dep = yaml.safe_load(f) 398 return create_secret(v1, namespace, dep) 399 400 401 def create_secret(v1: CoreV1Api, namespace, body) -> str: 402 """ 403 Create a secret based on a dict. 404 405 :param v1: CoreV1Api 406 :param namespace: namespace 407 :param body: a dict 408 :return: str 409 """ 410 print("Create a secret:") 411 v1.create_namespaced_secret(namespace, body) 412 print(f"Secret created: {body['metadata']['name']}") 413 return body['metadata']['name'] 414 415 416 def replace_secret(v1: CoreV1Api, name, namespace, yaml_manifest) -> str: 417 """ 418 Replace a secret based on yaml file. 419 420 :param v1: CoreV1Api 421 :param name: secret name 422 :param namespace: namespace name 423 :param yaml_manifest: an absolute path to file 424 :return: str 425 """ 426 print(f"Replace a secret: '{name}'' in a namespace: '{namespace}'") 427 with open(yaml_manifest) as f: 428 dep = yaml.safe_load(f) 429 v1.replace_namespaced_secret(name, namespace, dep) 430 print("Secret replaced") 431 return name 432 433 434 def is_secret_present(v1: CoreV1Api, name, namespace) -> bool: 435 """ 436 Check if a namespace has a secret. 437 438 :param v1: CoreV1Api 439 :param name: 440 :param namespace: 441 :return: bool 442 """ 443 try: 444 v1.read_namespaced_secret(name, namespace) 445 except ApiException as ex: 446 if ex.status == 404: 447 print(f"No secret '{name}' found.") 448 return False 449 return True 450 451 452 def delete_secret(v1: CoreV1Api, name, namespace) -> None: 453 """ 454 Delete a secret. 455 456 :param v1: CoreV1Api 457 :param name: secret name 458 :param namespace: namespace name 459 :return: 460 """ 461 delete_options = { 462 "grace_period_seconds": 0, 463 "propagation_policy": "Foreground", 464 } 465 print(f"Delete a secret: {name}") 466 v1.delete_namespaced_secret(name, namespace, **delete_options) 467 ensure_item_removal(v1.read_namespaced_secret, name, namespace) 468 print(f"Secret was removed with name '{name}'") 469 470 471 def ensure_item_removal(get_item, *args, **kwargs) -> None: 472 """ 473 Wait for item to be removed. 474 475 :param get_item: a call to get an item 476 :param args: *args 477 :param kwargs: **kwargs 478 :return: 479 """ 480 try: 481 counter = 0 482 while counter < 120: 483 time.sleep(1) 484 get_item(*args, **kwargs) 485 counter = counter + 1 486 if counter >= 120: 487 # Due to k8s issue with namespaces, they sometimes get stuck in Terminating state, skip such cases 488 if "_namespace " in str(get_item): 489 print(f"Failed to remove namespace '{args}' after 120 seconds, skip removal. Remove manually.") 490 else: 491 pytest.fail("Failed to remove the item after 120 seconds") 492 except ApiException as ex: 493 if ex.status == 404: 494 print("Item was removed") 495 496 497 def create_ingress_from_yaml(extensions_v1_beta1: ExtensionsV1beta1Api, namespace, yaml_manifest) -> str: 498 """ 499 Create an ingress based on yaml file. 500 501 :param extensions_v1_beta1: ExtensionsV1beta1Api 502 :param namespace: namespace name 503 :param yaml_manifest: an absolute path to file 504 :return: str 505 """ 506 print(f"Load {yaml_manifest}") 507 with open(yaml_manifest) as f: 508 dep = yaml.safe_load(f) 509 return create_ingress(extensions_v1_beta1, namespace, dep) 510 511 512 def create_ingress(extensions_v1_beta1: ExtensionsV1beta1Api, namespace, body) -> str: 513 """ 514 Create an ingress based on a dict. 515 516 :param extensions_v1_beta1: ExtensionsV1beta1Api 517 :param namespace: namespace name 518 :param body: a dict 519 :return: str 520 """ 521 print("Create an ingress:") 522 extensions_v1_beta1.create_namespaced_ingress(namespace, body) 523 print(f"Ingress created with name '{body['metadata']['name']}'") 524 return body['metadata']['name'] 525 526 527 def delete_ingress(extensions_v1_beta1: ExtensionsV1beta1Api, name, namespace) -> None: 528 """ 529 Delete an ingress. 530 531 :param extensions_v1_beta1: ExtensionsV1beta1Api 532 :param namespace: namespace 533 :param name: 534 :return: 535 """ 536 print(f"Delete an ingress: {name}") 537 extensions_v1_beta1.delete_namespaced_ingress(name, namespace) 538 ensure_item_removal(extensions_v1_beta1.read_namespaced_ingress, name, namespace) 539 print(f"Ingress was removed with name '{name}'") 540 541 542 def generate_ingresses_with_annotation(yaml_manifest, annotations) -> []: 543 """ 544 Generate an Ingress item with an annotation. 545 546 :param yaml_manifest: an absolute path to a file 547 :param annotations: 548 :return: [] 549 """ 550 res = [] 551 with open(yaml_manifest) as f: 552 docs = yaml.safe_load_all(f) 553 for doc in docs: 554 if doc['kind'] == 'Ingress': 555 doc['metadata']['annotations'].update(annotations) 556 res.append(doc) 557 return res 558 559 560 def replace_ingress(extensions_v1_beta1: ExtensionsV1beta1Api, name, namespace, body) -> str: 561 """ 562 Replace an Ingress based on a dict. 563 564 :param extensions_v1_beta1: ExtensionsV1beta1Api 565 :param name: 566 :param namespace: namespace 567 :param body: dict 568 :return: str 569 """ 570 print(f"Replace a Ingress: {name}") 571 resp = extensions_v1_beta1.replace_namespaced_ingress(name, namespace, body) 572 print(f"Ingress replaced with name '{name}'") 573 return resp.metadata.name 574 575 576 def create_namespace_from_yaml(v1: CoreV1Api, yaml_manifest) -> str: 577 """ 578 Create a namespace based on yaml file. 579 580 :param v1: CoreV1Api 581 :param yaml_manifest: an absolute path to file 582 :return: str 583 """ 584 print(f"Load {yaml_manifest}") 585 with open(yaml_manifest) as f: 586 dep = yaml.safe_load(f) 587 create_namespace(v1, dep) 588 return dep['metadata']['name'] 589 590 591 def create_namespace(v1: CoreV1Api, body) -> str: 592 """ 593 Create an ingress based on a dict. 594 595 :param v1: CoreV1Api 596 :param body: a dict 597 :return: str 598 """ 599 print("Create a namespace:") 600 v1.create_namespace(body) 601 print(f"Namespace created with name '{body['metadata']['name']}'") 602 return body['metadata']['name'] 603 604 605 def create_namespace_with_name_from_yaml(v1: CoreV1Api, name, yaml_manifest) -> str: 606 """ 607 Create a namespace with a specific name based on a yaml manifest. 608 609 :param v1: CoreV1Api 610 :param name: name 611 :param yaml_manifest: an absolute path to file 612 :return: str 613 """ 614 print(f"Create a namespace with specific name:") 615 with open(yaml_manifest) as f: 616 dep = yaml.safe_load(f) 617 dep['metadata']['name'] = name 618 v1.create_namespace(dep) 619 print(f"Namespace created with name '{str(dep['metadata']['name'])}'") 620 return dep['metadata']['name'] 621 622 623 def create_service_account(v1: CoreV1Api, namespace, body) -> None: 624 """ 625 Create a ServiceAccount based on a dict. 626 627 :param v1: CoreV1Api 628 :param namespace: namespace name 629 :param body: a dict 630 :return: 631 """ 632 print("Create a SA:") 633 v1.create_namespaced_service_account(namespace, body) 634 print(f"Service account created with name '{body['metadata']['name']}'") 635 636 637 def create_configmap_from_yaml(v1: CoreV1Api, namespace, yaml_manifest) -> str: 638 """ 639 Create a config-map based on yaml file. 640 641 :param v1: CoreV1Api 642 :param namespace: namespace name 643 :param yaml_manifest: an absolute path to file 644 :return: str 645 """ 646 print(f"Load {yaml_manifest}") 647 with open(yaml_manifest) as f: 648 dep = yaml.safe_load(f) 649 return create_configmap(v1, namespace, dep) 650 651 652 def create_configmap(v1: CoreV1Api, namespace, body) -> str: 653 """ 654 Create a config-map based on a dict. 655 656 :param v1: CoreV1Api 657 :param namespace: namespace name 658 :param body: a dict 659 :return: str 660 """ 661 print("Create a configMap:") 662 v1.create_namespaced_config_map(namespace, body) 663 print(f"Config map created with name '{body['metadata']['name']}'") 664 return body["metadata"]["name"] 665 666 667 def replace_configmap_from_yaml(v1: CoreV1Api, name, namespace, yaml_manifest) -> None: 668 """ 669 Replace a config-map based on a yaml file. 670 671 :param v1: CoreV1Api 672 :param name: 673 :param namespace: namespace name 674 :param yaml_manifest: an absolute path to file 675 :return: 676 """ 677 print(f"Replace a configMap: '{name}'") 678 with open(yaml_manifest) as f: 679 dep = yaml.safe_load(f) 680 v1.replace_namespaced_config_map(name, namespace, dep) 681 print("ConfigMap replaced") 682 683 684 def replace_configmap(v1: CoreV1Api, name, namespace, body) -> None: 685 """ 686 Replace a config-map based on a dict. 687 688 :param v1: CoreV1Api 689 :param name: 690 :param namespace: 691 :param body: a dict 692 :return: 693 """ 694 print(f"Replace a configMap: '{name}'") 695 v1.replace_namespaced_config_map(name, namespace, body) 696 print("ConfigMap replaced") 697 698 699 def delete_configmap(v1: CoreV1Api, name, namespace) -> None: 700 """ 701 Delete a ConfigMap. 702 703 :param v1: CoreV1Api 704 :param name: ConfigMap name 705 :param namespace: namespace name 706 :return: 707 """ 708 delete_options = { 709 "grace_period_seconds": 0, 710 "propagation_policy": "Foreground", 711 } 712 print(f"Delete a ConfigMap: {name}") 713 v1.delete_namespaced_config_map(name, namespace, **delete_options) 714 ensure_item_removal(v1.read_namespaced_config_map, name, namespace) 715 print(f"ConfigMap was removed with name '{name}'") 716 717 718 def delete_namespace(v1: CoreV1Api, namespace) -> None: 719 """ 720 Delete a namespace. 721 722 :param v1: CoreV1Api 723 :param namespace: namespace name 724 :return: 725 """ 726 delete_options = { 727 "grace_period_seconds": 0, 728 "propagation_policy": "Foreground", 729 } 730 print(f"Delete a namespace: {namespace}") 731 v1.delete_namespace(namespace, **delete_options) 732 ensure_item_removal(v1.read_namespace, namespace) 733 print(f"Namespace was removed with name '{namespace}'") 734 735 736 def delete_testing_namespaces(v1: CoreV1Api) -> []: 737 """ 738 List and remove all the testing namespaces. 739 740 Testing namespaces are the ones starting with "test-namespace-" 741 742 :param v1: CoreV1Api 743 :return: 744 """ 745 namespaces_list = v1.list_namespace() 746 for namespace in list(filter(lambda ns: ns.metadata.name.startswith("test-namespace-"), namespaces_list.items)): 747 delete_namespace(v1, namespace.metadata.name) 748 749 750 def get_file_contents(v1: CoreV1Api, file_path, pod_name, pod_namespace) -> str: 751 """ 752 Execute 'cat file_path' command in a pod. 753 754 :param v1: CoreV1Api 755 :param pod_name: pod name 756 :param pod_namespace: pod namespace 757 :param file_path: an absolute path to a file in the pod 758 :return: str 759 """ 760 command = ["cat", file_path] 761 resp = stream( 762 v1.connect_get_namespaced_pod_exec, 763 pod_name, 764 pod_namespace, 765 command=command, 766 stderr=True, stdin=False, stdout=True, tty=False) 767 result_conf = str(resp) 768 print("\nFile contents:\n" + result_conf) 769 return result_conf 770 771 772 def get_ingress_nginx_template_conf(v1: CoreV1Api, ingress_namespace, ingress_name, pod_name, pod_namespace) -> str: 773 """ 774 Get contents of /etc/nginx/conf.d/{namespace}-{ingress_name}.conf in the pod. 775 776 :param v1: CoreV1Api 777 :param ingress_namespace: 778 :param ingress_name: 779 :param pod_name: 780 :param pod_namespace: 781 :return: str 782 """ 783 file_path = f"/etc/nginx/conf.d/{ingress_namespace}-{ingress_name}.conf" 784 return get_file_contents(v1, file_path, pod_name, pod_namespace) 785 786 787 def get_ts_nginx_template_conf(v1: CoreV1Api, resource_namespace, resource_name, pod_name, pod_namespace) -> str: 788 """ 789 Get contents of /etc/nginx/stream-conf.d/ts_{namespace}-{resource_name}.conf in the pod. 790 791 :param v1: CoreV1Api 792 :param resource_namespace: 793 :param resource_name: 794 :param pod_name: 795 :param pod_namespace: 796 :return: str 797 """ 798 file_path = f"/etc/nginx/stream-conf.d/ts_{resource_namespace}_{resource_name}.conf" 799 return get_file_contents(v1, file_path, pod_name, pod_namespace) 800 801 802 def create_example_app(kube_apis, app_type, namespace) -> None: 803 """ 804 Create a backend application. 805 806 An application consists of 3 backend services. 807 808 :param kube_apis: client apis 809 :param app_type: type of the application (simple|split) 810 :param namespace: namespace name 811 :return: 812 """ 813 create_items_from_yaml(kube_apis, f"{TEST_DATA}/common/app/{app_type}/app.yaml", namespace) 814 815 816 def delete_common_app(kube_apis, app_type, namespace) -> None: 817 """ 818 Delete a common simple application. 819 820 :param kube_apis: 821 :param app_type: 822 :param namespace: namespace name 823 :return: 824 """ 825 delete_items_from_yaml(kube_apis, f"{TEST_DATA}/common/app/{app_type}/app.yaml", namespace) 826 827 828 def delete_service(v1: CoreV1Api, name, namespace) -> None: 829 """ 830 Delete a service. 831 832 :param v1: CoreV1Api 833 :param name: 834 :param namespace: 835 :return: 836 """ 837 print(f"Delete a service: {name}") 838 v1.delete_namespaced_service(name, namespace) 839 ensure_item_removal(v1.read_namespaced_service_status, name, namespace) 840 print(f"Service was removed with name '{name}'") 841 842 843 def delete_deployment(apps_v1_api: AppsV1Api, name, namespace) -> None: 844 """ 845 Delete a deployment. 846 847 :param apps_v1_api: AppsV1Api 848 :param name: 849 :param namespace: 850 :return: 851 """ 852 delete_options = { 853 "grace_period_seconds": 0, 854 "propagation_policy": "Foreground", 855 } 856 print(f"Delete a deployment: {name}") 857 apps_v1_api.delete_namespaced_deployment(name, namespace, **delete_options) 858 ensure_item_removal(apps_v1_api.read_namespaced_deployment_status, name, namespace) 859 print(f"Deployment was removed with name '{name}'") 860 861 862 def delete_daemon_set(apps_v1_api: AppsV1Api, name, namespace) -> None: 863 """ 864 Delete a daemon-set. 865 866 :param apps_v1_api: AppsV1Api 867 :param name: 868 :param namespace: 869 :return: 870 """ 871 delete_options = { 872 "grace_period_seconds": 0, 873 "propagation_policy": "Foreground", 874 } 875 print(f"Delete a daemon-set: {name}") 876 apps_v1_api.delete_namespaced_daemon_set(name, namespace, **delete_options) 877 ensure_item_removal(apps_v1_api.read_namespaced_daemon_set_status, name, namespace) 878 print(f"Daemon-set was removed with name '{name}'") 879 880 881 def wait_before_test(delay=RECONFIGURATION_DELAY) -> None: 882 """ 883 Wait for a time in seconds. 884 885 :param delay: a delay in seconds 886 :return: 887 """ 888 time.sleep(delay) 889 890 891 def wait_for_event_increment(kube_apis, namespace, event_count, offset) -> bool: 892 """ 893 Wait for event count to increase. 894 895 :param kube_apis: Kubernates API 896 :param namespace: event namespace 897 :param event_count: Current even count 898 :param offset: Number of events generated by last operation 899 :return: 900 """ 901 print(f"Current count: {event_count}") 902 updated_event_count = len(get_events(kube_apis.v1, namespace)) 903 retry = 0 904 while(updated_event_count != (event_count+offset) and retry < 30 ): 905 time.sleep(1) 906 retry += 1 907 updated_event_count = len(get_events(kube_apis.v1, namespace)) 908 print(f"Updated count: {updated_event_count}") 909 print(f"Event not registered, Retry #{retry}..") 910 if (updated_event_count == (event_count+offset)): 911 return True 912 else: 913 print(f"Event was not registered after {retry} retries, exiting...") 914 return False 915 916 917 918 919 def create_ingress_controller(v1: CoreV1Api, apps_v1_api: AppsV1Api, cli_arguments, 920 namespace, args=None) -> str: 921 """ 922 Create an Ingress Controller according to the params. 923 924 :param v1: CoreV1Api 925 :param apps_v1_api: AppsV1Api 926 :param cli_arguments: context name as in kubeconfig 927 :param namespace: namespace name 928 :param args: a list of any extra cli arguments to start IC with 929 :return: str 930 """ 931 print(f"Create an Ingress Controller as {cli_arguments['ic-type']}") 932 yaml_manifest = f"{DEPLOYMENTS}/{cli_arguments['deployment-type']}/{cli_arguments['ic-type']}.yaml" 933 with open(yaml_manifest) as f: 934 dep = yaml.safe_load(f) 935 dep['spec']['template']['spec']['containers'][0]['image'] = cli_arguments["image"] 936 dep['spec']['template']['spec']['containers'][0]['imagePullPolicy'] = cli_arguments["image-pull-policy"] 937 if args is not None: 938 dep['spec']['template']['spec']['containers'][0]['args'].extend(args) 939 if cli_arguments['deployment-type'] == 'deployment': 940 name = create_deployment(apps_v1_api, namespace, dep) 941 else: 942 name = create_daemon_set(apps_v1_api, namespace, dep) 943 wait_until_all_pods_are_ready(v1, namespace) 944 print(f"Ingress Controller was created with name '{name}'") 945 return name 946 947 948 def delete_ingress_controller(apps_v1_api: AppsV1Api, name, dep_type, namespace) -> None: 949 """ 950 Delete IC according to its type. 951 952 :param apps_v1_api: ExtensionsV1beta1Api 953 :param name: name 954 :param dep_type: IC deployment type 'deployment' or 'daemon-set' 955 :param namespace: namespace name 956 :return: 957 """ 958 if dep_type == 'deployment': 959 delete_deployment(apps_v1_api, name, namespace) 960 elif dep_type == 'daemon-set': 961 delete_daemon_set(apps_v1_api, name, namespace) 962 963 964 def create_ns_and_sa_from_yaml(v1: CoreV1Api, yaml_manifest) -> str: 965 """ 966 Create a namespace and a service account in that namespace. 967 968 :param v1: 969 :param yaml_manifest: an absolute path to a file 970 :return: str 971 """ 972 print("Load yaml:") 973 res = {} 974 with open(yaml_manifest) as f: 975 docs = yaml.safe_load_all(f) 976 for doc in docs: 977 if doc["kind"] == "Namespace": 978 res['namespace'] = create_namespace(v1, doc) 979 elif doc["kind"] == "ServiceAccount": 980 assert res['namespace'] is not None, "Ensure 'Namespace' is above 'SA' in the yaml manifest" 981 create_service_account(v1, res['namespace'], doc) 982 return res["namespace"] 983 984 985 def create_items_from_yaml(kube_apis, yaml_manifest, namespace) -> None: 986 """ 987 Apply yaml manifest with multiple items. 988 989 :param kube_apis: KubeApis 990 :param yaml_manifest: an absolute path to a file 991 :param namespace: 992 :return: 993 """ 994 print("Load yaml:") 995 with open(yaml_manifest) as f: 996 docs = yaml.safe_load_all(f) 997 for doc in docs: 998 if doc["kind"] == "Secret": 999 create_secret(kube_apis.v1, namespace, doc) 1000 elif doc["kind"] == "ConfigMap": 1001 create_configmap(kube_apis.v1, namespace, doc) 1002 elif doc["kind"] == "Ingress": 1003 create_ingress(kube_apis.extensions_v1_beta1, namespace, doc) 1004 elif doc["kind"] == "Service": 1005 create_service(kube_apis.v1, namespace, doc) 1006 elif doc["kind"] == "Deployment": 1007 create_deployment(kube_apis.apps_v1_api, namespace, doc) 1008 elif doc["kind"] == "DaemonSet": 1009 create_daemon_set(kube_apis.apps_v1_api, namespace, doc) 1010 1011 1012 def create_ingress_with_ap_annotations( 1013 kube_apis, yaml_manifest, namespace, policy_name, ap_pol_st, ap_log_st, syslog_ep 1014 ) -> None: 1015 """ 1016 Create an ingress with AppProtect annotations 1017 :param kube_apis: KubeApis 1018 :param yaml_manifest: an absolute path to ingress yaml 1019 :param namespace: namespace 1020 :param policy_name: AppProtect policy 1021 :param ap_log_st: True/False for enabling/disabling AppProtect security logging 1022 :param ap_pol_st: True/False for enabling/disabling AppProtect module for particular ingress 1023 :param syslog_ep: Destination endpoint for security logs 1024 :return: 1025 """ 1026 print("Load ingress yaml and set AppProtect annotations") 1027 policy = f"{namespace}/{policy_name}" 1028 logconf = f"{namespace}/logconf" 1029 1030 with open(yaml_manifest) as f: 1031 doc = yaml.safe_load(f) 1032 1033 doc["metadata"]["annotations"]["appprotect.f5.com/app-protect-policy"] = policy 1034 doc["metadata"]["annotations"]["appprotect.f5.com/app-protect-enable"] = ap_pol_st 1035 doc["metadata"]["annotations"][ 1036 "appprotect.f5.com/app-protect-security-log-enable" 1037 ] = ap_log_st 1038 doc["metadata"]["annotations"]["appprotect.f5.com/app-protect-security-log"] = logconf 1039 doc["metadata"]["annotations"]["appprotect.f5.com/app-protect-security-log-destination"] = f"syslog:server={syslog_ep}" 1040 create_ingress(kube_apis.extensions_v1_beta1, namespace, doc) 1041 1042 1043 def replace_ingress_with_ap_annotations( 1044 kube_apis, yaml_manifest, name, namespace, policy_name, ap_pol_st, ap_log_st, syslog_ep 1045 ) -> None: 1046 """ 1047 Replace an ingress with AppProtect annotations 1048 :param kube_apis: KubeApis 1049 :param yaml_manifest: an absolute path to ingress yaml 1050 :param namespace: namespace 1051 :param policy_name: AppProtect policy 1052 :param ap_log_st: True/False for enabling/disabling AppProtect security logging 1053 :param ap_pol_st: True/False for enabling/disabling AppProtect module for particular ingress 1054 :param syslog_ep: Destination endpoint for security logs 1055 :return: 1056 """ 1057 print("Load ingress yaml and set AppProtect annotations") 1058 policy = f"{namespace}/{policy_name}" 1059 logconf = f"{namespace}/logconf" 1060 1061 with open(yaml_manifest) as f: 1062 doc = yaml.safe_load(f) 1063 1064 doc["metadata"]["annotations"]["appprotect.f5.com/app-protect-policy"] = policy 1065 doc["metadata"]["annotations"]["appprotect.f5.com/app-protect-enable"] = ap_pol_st 1066 doc["metadata"]["annotations"][ 1067 "appprotect.f5.com/app-protect-security-log-enable" 1068 ] = ap_log_st 1069 doc["metadata"]["annotations"]["appprotect.f5.com/app-protect-security-log"] = logconf 1070 doc["metadata"]["annotations"]["appprotect.f5.com/app-protect-security-log-destination"] = f"syslog:server={syslog_ep}" 1071 replace_ingress(kube_apis.extensions_v1_beta1, name, namespace, doc) 1072 1073 1074 def delete_items_from_yaml(kube_apis, yaml_manifest, namespace) -> None: 1075 """ 1076 Delete all the items found in the yaml file. 1077 1078 :param kube_apis: KubeApis 1079 :param yaml_manifest: an absolute path to a file 1080 :param namespace: namespace 1081 :return: 1082 """ 1083 print("Load yaml:") 1084 with open(yaml_manifest) as f: 1085 docs = yaml.safe_load_all(f) 1086 for doc in docs: 1087 if doc["kind"] == "Namespace": 1088 delete_namespace(kube_apis.v1, doc['metadata']['name']) 1089 elif doc["kind"] == "Secret": 1090 delete_secret(kube_apis.v1, doc['metadata']['name'], namespace) 1091 elif doc["kind"] == "Ingress": 1092 delete_ingress(kube_apis.extensions_v1_beta1, doc['metadata']['name'], namespace) 1093 elif doc["kind"] == "Service": 1094 delete_service(kube_apis.v1, doc['metadata']['name'], namespace) 1095 elif doc["kind"] == "Deployment": 1096 delete_deployment(kube_apis.apps_v1_api, doc['metadata']['name'], namespace) 1097 elif doc["kind"] == "DaemonSet": 1098 delete_daemon_set(kube_apis.apps_v1_api, doc['metadata']['name'], namespace) 1099 elif doc["kind"] == "ConfigMap": 1100 delete_configmap(kube_apis.v1, doc['metadata']['name'], namespace) 1101 1102 1103 def ensure_connection(request_url, expected_code=404) -> None: 1104 """ 1105 Wait for connection. 1106 1107 :param request_url: url to request 1108 :param expected_code: response code 1109 :return: 1110 """ 1111 for _ in range(10): 1112 try: 1113 resp = requests.get(request_url, verify=False, timeout=5) 1114 if resp.status_code == expected_code: 1115 return 1116 except Exception as ex: 1117 print(f"Warning: there was an exception {str(ex)}") 1118 time.sleep(3) 1119 pytest.fail("Connection failed after several attempts") 1120 1121 1122 def ensure_connection_to_public_endpoint(ip_address, port, port_ssl) -> None: 1123 """ 1124 Ensure the public endpoint doesn't refuse connections. 1125 1126 :param ip_address: 1127 :param port: 1128 :param port_ssl: 1129 :return: 1130 """ 1131 ensure_connection(f"http://{ip_address}:{port}/") 1132 ensure_connection(f"https://{ip_address}:{port_ssl}/") 1133 1134 1135 def read_service(v1: CoreV1Api, name, namespace) -> V1Service: 1136 """ 1137 Get details of a Service. 1138 1139 :param v1: CoreV1Api 1140 :param name: service name 1141 :param namespace: namespace name 1142 :return: V1Service 1143 """ 1144 print(f"Read a service named '{name}'") 1145 return v1.read_namespaced_service(name, namespace) 1146 1147 1148 def replace_service(v1: CoreV1Api, name, namespace, body) -> str: 1149 """ 1150 Patch a service based on a dict. 1151 1152 :param v1: CoreV1Api 1153 :param name: 1154 :param namespace: namespace 1155 :param body: a dict 1156 :return: str 1157 """ 1158 print(f"Replace a Service: {name}") 1159 resp = v1.replace_namespaced_service(name, namespace, body) 1160 print(f"Service updated with name '{name}'") 1161 return resp.metadata.name 1162 1163 1164 def get_events(v1: CoreV1Api, namespace) -> []: 1165 """ 1166 Get the list of events in a namespace. 1167 1168 :param v1: CoreV1Api 1169 :param namespace: 1170 :return: [] 1171 """ 1172 print(f"Get the events in the namespace: {namespace}") 1173 res = v1.list_namespaced_event(namespace) 1174 return res.items 1175 1176 1177 def ensure_response_from_backend(req_url, host, additional_headers=None, check404=False) -> None: 1178 """ 1179 Wait for 502|504|404 to disappear. 1180 1181 :param req_url: url to request 1182 :param host: 1183 :param additional_headers: 1184 :return: 1185 """ 1186 headers = {"host": host} 1187 if additional_headers: 1188 headers.update(additional_headers) 1189 1190 if check404: 1191 for _ in range(60): 1192 resp = requests.get(req_url, headers=headers, verify=False) 1193 if resp.status_code != 502 and resp.status_code != 504 and resp.status_code != 404: 1194 print(f"After {_} retries at 1 second interval, got {resp.status_code} response. Continue with tests...") 1195 return 1196 time.sleep(1) 1197 pytest.fail(f"Keep getting {resp.status_code} from {req_url} after 60 seconds. Exiting...") 1198 1199 else: 1200 for _ in range(30): 1201 resp = requests.get(req_url, headers=headers, verify=False) 1202 if resp.status_code != 502 and resp.status_code != 504: 1203 print(f"After {_} retries at 1 second interval, got non 502|504 response. Continue with tests...") 1204 return 1205 time.sleep(1) 1206 pytest.fail(f"Keep getting 502|504 from {req_url} after 60 seconds. Exiting...") 1207 1208 def get_service_endpoint(kube_apis, service_name, namespace): 1209 """ 1210 Wait for endpoint resource to spin up. 1211 :param kube_apis: Kubernates API object 1212 :param service_name: Service resource name 1213 :param namespace: test namespace 1214 :return: endpoint ip 1215 """ 1216 found = False 1217 retry = 0 1218 ep = "" 1219 while(not found and retry<40): 1220 time.sleep(1) 1221 try: 1222 ep = ( 1223 kube_apis.v1.read_namespaced_endpoints(service_name, namespace) 1224 .subsets[0] 1225 .addresses[0] 1226 .ip 1227 ) 1228 found = True 1229 print(f"Endpoint IP for {service_name} is {ep}") 1230 except TypeError as err: 1231 retry += 1 1232 return ep