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