From e2db1c7a4032d316e5b655b858b023ace8435cd8 Mon Sep 17 00:00:00 2001 From: Yi Feng Date: Fri, 8 Apr 2022 11:43:34 +0900 Subject: [PATCH] Fix validate error when k8s resource init This patch fixes the issue that k8s failed to initialize object due to version upgrade. Skip initial validation by setting the `client_side_validation` parameter and remove the must_param variable that was previously set in order to pass the validation. In kubernetes v23.3.0, 'available_replicas' must be set into status of `StatefulSet`, so this patch also add a kuryr-kubernetes versioned parameter temporarily to .zuul.yaml to make the response returned by kubernetes correct. Closes-Bug: #1968103 Change-Id: I9495ce0f0893e5f9a1d6c52b98c3db3928bd95a3 --- .zuul.yaml | 6 + .../infra_drivers/kubernetes/fakes.py | 7 +- .../kubernetes/test_kubernetes.py | 2 +- .../vnfm/infra_drivers/kubernetes/fakes.py | 22 +- .../kubernetes/test_translate_outputs.py | 1 - .../kubernetes/k8s/translate_outputs.py | 207 +++--------------- 6 files changed, 55 insertions(+), 190 deletions(-) diff --git a/.zuul.yaml b/.zuul.yaml index 7770fb3a0..57319ffd2 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -439,6 +439,12 @@ KURYR_K8S_API_URL: "https://{{ hostvars['controller-k8s']['nodepool']['private_ipv4'] }}:${KURYR_K8S_API_PORT}" KURYR_K8S_CONTAINERIZED_DEPLOYMENT: false KURYR_NEUTRON_DEFAULT_SUBNETPOOL_ID: shared-default-subnetpool-v4 + # TODO(YiFeng): At present, the version of kubernetes should be + # 1.23.3, and the returned response can pass the verification of + # kubernetes-client (1.23.3). This configuration will be removed + # after kuryr-kubernetes fixes the following bug. + # https://bugs.launchpad.net/kuryr-kubernetes/+bug/1968960 + KURYR_KUBERNETES_VERSION: 1.23.3 MYSQL_HOST: "{{ hostvars['controller']['nodepool']['private_ipv4'] }}" OCTAVIA_AMP_IMAGE_FILE: "/tmp/test-only-amphora-x64-haproxy-ubuntu-bionic.qcow2" OCTAVIA_AMP_IMAGE_NAME: "test-only-amphora-x64-haproxy-ubuntu-bionic" diff --git a/tacker/tests/unit/sol_refactored/infra_drivers/kubernetes/fakes.py b/tacker/tests/unit/sol_refactored/infra_drivers/kubernetes/fakes.py index 07a0a978c..7978a6edf 100644 --- a/tacker/tests/unit/sol_refactored/infra_drivers/kubernetes/fakes.py +++ b/tacker/tests/unit/sol_refactored/infra_drivers/kubernetes/fakes.py @@ -290,7 +290,7 @@ def fake_pc(): def fake_persistent_volume( - name='curry-sc-pv', phase='UnAvailable'): + name='curry-sc-pv', phase='Pending'): return client.V1PersistentVolume( api_version='v1', kind='PersistentVolume', @@ -395,6 +395,8 @@ def fake_rq(): def fake_stateful_set(ready_replicas=0): + client_config = client.Configuration.get_default_copy() + client_config.client_side_validation = False return client.V1StatefulSet( api_version='apps/v1', kind='StatefulSet', @@ -424,7 +426,8 @@ def fake_stateful_set(ready_replicas=0): ), status=client.V1StatefulSetStatus( replicas=2, - ready_replicas=ready_replicas + ready_replicas=ready_replicas, + local_vars_configuration=client_config ), ) diff --git a/tacker/tests/unit/sol_refactored/infra_drivers/kubernetes/test_kubernetes.py b/tacker/tests/unit/sol_refactored/infra_drivers/kubernetes/test_kubernetes.py index 2d34488ff..81d7e2d21 100644 --- a/tacker/tests/unit/sol_refactored/infra_drivers/kubernetes/test_kubernetes.py +++ b/tacker/tests/unit/sol_refactored/infra_drivers/kubernetes/test_kubernetes.py @@ -845,7 +845,7 @@ class TestKubernetes(base.BaseTestCase): mock_node.return_value = fakes.fake_node(status='False') mock_read_node.side_effect = [ - fakes.fake_node(type='UnReady'), + fakes.fake_node(type='NetworkUnavailable'), fakes.fake_node(), fakes.fake_none()] self._normal_execute_procedure(req) diff --git a/tacker/tests/unit/vnfm/infra_drivers/kubernetes/fakes.py b/tacker/tests/unit/vnfm/infra_drivers/kubernetes/fakes.py index aea80800a..03da38e42 100644 --- a/tacker/tests/unit/vnfm/infra_drivers/kubernetes/fakes.py +++ b/tacker/tests/unit/vnfm/infra_drivers/kubernetes/fakes.py @@ -238,7 +238,7 @@ def fake_pvc_false(): name='curry-sc-pvc' ), status=client.V1PersistentVolumeClaimStatus( - phase='UnBound' + phase='Pending' ) ) @@ -286,7 +286,7 @@ def fake_namespace_false(): name='curry-ns' ), status=client.V1NamespaceStatus( - phase='NotActive' + phase='Terminating' ) ) @@ -658,6 +658,8 @@ def fake_v1_volume_attachment_error(): def fake_v1_stateful_set(): + client_config = client.Configuration.get_default_copy() + client_config.client_side_validation = False return client.V1StatefulSet( api_version='apps/v1', kind='StatefulSet', @@ -687,12 +689,15 @@ def fake_v1_stateful_set(): ), status=client.V1StatefulSetStatus( replicas=1, - ready_replicas=1 + ready_replicas=1, + local_vars_configuration=client_config ), ) def fake_v1_stateful_set_error(): + client_config = client.Configuration.get_default_copy() + client_config.client_side_validation = False return client.V1StatefulSet( api_version='apps/v1', kind='StatefulSet', @@ -722,7 +727,8 @@ def fake_v1_stateful_set_error(): ), status=client.V1StatefulSetStatus( replicas=2, - ready_replicas=1 + ready_replicas=1, + local_vars_configuration=client_config ) ) @@ -750,7 +756,7 @@ def fake_v1_persistent_volume_claim_error(): namespace='curryns' ), status=client.V1PersistentVolumeClaimStatus( - phase='Bound1' + phase='Pending' ) ) @@ -800,7 +806,7 @@ def fake_pod_error(): namespace='curryns' ), status=client.V1PodStatus( - phase='Terminated', + phase='Failed', ) ) @@ -850,7 +856,7 @@ def fake_persistent_volume_error(): namespace='curryns' ), status=client.V1PersistentVolumeStatus( - phase='UnBound', + phase='Pending', ) ) @@ -1017,7 +1023,7 @@ def fake_pod_list(): name="fake-name" ), status=client.V1PodStatus( - phase="Successed" + phase="Succeeded" ) )] ) diff --git a/tacker/tests/unit/vnfm/infra_drivers/kubernetes/test_translate_outputs.py b/tacker/tests/unit/vnfm/infra_drivers/kubernetes/test_translate_outputs.py index e50099b30..6f1c75de0 100644 --- a/tacker/tests/unit/vnfm/infra_drivers/kubernetes/test_translate_outputs.py +++ b/tacker/tests/unit/vnfm/infra_drivers/kubernetes/test_translate_outputs.py @@ -684,7 +684,6 @@ class TestTransformer(base.TestCase): self.assertEqual(k8s_obj.api_version, 'v1') # V1LimitRangeSpec self.assertIsNotNone(k8s_obj.spec.limits) - self.assertIsNotNone(k8s_obj.spec.limits[0].type) def test_pod_template(self): k8s_objs = self.transfromer.get_k8s_objs_from_yaml( diff --git a/tacker/vnfm/infra_drivers/kubernetes/k8s/translate_outputs.py b/tacker/vnfm/infra_drivers/kubernetes/k8s/translate_outputs.py index 44ab2763b..cd6072ad4 100644 --- a/tacker/vnfm/infra_drivers/kubernetes/k8s/translate_outputs.py +++ b/tacker/vnfm/infra_drivers/kubernetes/k8s/translate_outputs.py @@ -160,169 +160,30 @@ class Transformer(object): return kubernetes_objects + def _gen_k8s_obj_from_name(self, obj_name): + """Generate kubernetes object + + The function converts the name passed in to the corresponding + kubernetes object and returns it. By default, client_side_validation + is True. To skip client-side validation, initialize an empty object, + set client_side_validation to False, and pass the configuration to + the function of initializing the kubernetes object through the + `local_vars_configuration` parameter. + """ + client_config = client.Configuration.get_default_copy() + client_config.client_side_validation = False + config = '(local_vars_configuration=client_config)' + try: + k8s_obj = eval('client.{}{}'.format(obj_name, config)) + return k8s_obj + except (ValueError, SyntaxError, AttributeError) as e: + msg = '{kind} create failure. Reason={reason}'.format( + kind=obj_name, reason=e) + raise exceptions.InitApiFalse(error=msg) + def _create_k8s_object(self, kind, file_content_dict): - # must_param referring K8s official object page - # *e.g:https://github.com/kubernetes-client/python/blob/master/kubernetes/docs/V1Service.md - # initiating k8s object, you need to - # give the must param an empty value. - must_param = { - 'V1LocalSubjectAccessReview': '(spec="")', - 'V1HTTPGetAction': '(port="")', - 'V1DeploymentSpec': '(selector="", template="")', - 'V1PodSpec': '(containers=[])', - 'V1ConfigMapKeySelector': '(key="")', - 'V1Container': '(name="")', - 'V1EnvVar': '(name="")', - 'V1SecretKeySelector': '(key="")', - 'V1ContainerPort': '(container_port="")', - 'V1VolumeMount': '(mount_path="", name="")', - 'V1PodCondition': '(status="", type="")', - 'V1ContainerStatus': '(' - 'image="", image_id="", ' - 'name="", ready="", ' - 'restart_count="")', - 'V1ServicePort': '(port=80)', - 'V1TypedLocalObjectReference': '(kind="", name="")', - 'V1LabelSelectorRequirement': '(key="", operator="")', - 'V1PersistentVolumeClaimCondition': '(status="", type="")', - 'V1AWSElasticBlockStoreVolumeSource': '(volume_id="")', - 'V1AzureDiskVolumeSource': '(disk_name="", disk_uri="")', - 'V1AzureFileVolumeSource': '(secret_name="", share_name="")', - 'V1CephFSVolumeSource': '(monitors=[])', - 'V1CinderVolumeSource': '(volume_id="")', - 'V1KeyToPath': '(key="", path="")', - 'V1CSIVolumeSource': '(driver="")', - 'V1DownwardAPIVolumeFile': '(path="")', - 'V1ObjectFieldSelector': '(field_path="")', - 'V1ResourceFieldSelector': '(resource="")', - 'V1FlexVolumeSource': '(driver="")', - 'V1GCEPersistentDiskVolumeSource': '(pd_name="")', - 'V1GitRepoVolumeSource': '(repository="")', - 'V1GlusterfsVolumeSource': '(endpoints="", path="")', - 'V1HostPathVolumeSource': '(path="")', - 'V1ISCSIVolumeSource': '(iqn="", lun=0, target_portal="")', - 'V1Volume': '(name="")', - 'V1NFSVolumeSource': '(path="", server="")', - 'V1PersistentVolumeClaimVolumeSource': '(claim_name="")', - 'V1PhotonPersistentDiskVolumeSource': '(pd_id="")', - 'V1PortworxVolumeSource': '(volume_id="")', - 'V1ProjectedVolumeSource': '(sources=[])', - 'V1ServiceAccountTokenProjection': '(path="")', - 'V1QuobyteVolumeSource': '(registry="", volume="")', - 'V1RBDVolumeSource': '(image="", monitors=[])', - 'V1ScaleIOVolumeSource': '(' - 'gateway="", secret_ref="", ' - 'system="")', - 'V1VsphereVirtualDiskVolumeSource': '(volume_path="")', - 'V1LimitRangeSpec': '(limits=[])', - 'V1Binding': '(target="")', - 'V1ComponentCondition': '(status="", type="")', - 'V1NamespaceCondition': '(status="", type="")', - 'V1ConfigMapNodeConfigSource': '(kubelet_config_key="", ' - 'name="", namespace="")', - 'V1Taint': '(effect="", key="")', - 'V1NodeAddress': '(address="", type="")', - 'V1NodeCondition': '(status="", type="")', - 'V1DaemonEndpoint': '(port=0)', - 'V1ContainerImage': '(names=[])', - 'V1NodeSystemInfo': '(architecture="", boot_id="", ' - 'container_runtime_version="",' - 'kernel_version="", ' - 'kube_proxy_version="", ' - 'kubelet_version="",' - 'machine_id="", operating_system="", ' - 'os_image="", system_uuid="")', - 'V1AttachedVolume': '(device_path="", name="")', - 'V1ScopedResourceSelectorRequirement': - '(operator="", scope_name="")', - 'V1APIServiceSpec': '(group_priority_minimum=0, ' - 'service="", ' - 'version_priority=0)', - 'V1APIServiceCondition': '(status="", type="")', - 'V1DaemonSetSpec': '(selector="", template="")', - 'V1ReplicaSetSpec': '(selector="")', - 'V1StatefulSetSpec': '(selector="", ' - 'service_name="", template="")', - 'V1StatefulSetCondition': '(status="", type="")', - 'V1StatefulSetStatus': '(replicas=0)', - 'V1ControllerRevision': '(revision=0)', - 'V1TokenReview': '(spec="")', - 'V1SubjectAccessReviewStatus': '(allowed=True)', - 'V1SelfSubjectAccessReview': '(spec="")', - 'V1SelfSubjectRulesReview': '(spec="")', - 'V1SubjectRulesReviewStatus': '(incomplete=True, ' - 'non_resource_rules=[], ' - 'resource_rules=[])', - 'V1NonResourceRule': '(verbs=[])', - 'V1SubjectAccessReview': '(spec="")', - 'V1HorizontalPodAutoscalerSpec': - '(max_replicas=0, scale_target_ref="")', - 'V1CrossVersionObjectReference': '(kind="", name="")', - 'V1HorizontalPodAutoscalerStatus': - '(current_replicas=0, desired_replicas=0)', - 'V1JobSpec': '(template="")', - 'V1NetworkPolicySpec': '(pod_selector="")', - 'V1PolicyRule': '(verbs=[])', - 'V1ClusterRoleBinding': '(role_ref="")', - 'V1RoleRef': '(api_group="", kind="", name="")', - 'V1Subject': '(kind="", name="")', - 'V1RoleBinding': '(role_ref="")', - 'V1PriorityClass': '(value=0)', - 'V1StorageClass': '(provisioner="")', - 'V1TopologySelectorLabelRequirement': '(key="", values=[])', - 'V1VolumeAttachment': '(spec="")', - 'V1VolumeAttachmentSpec': - '(attacher="", node_name="", source="")', - 'V1VolumeAttachmentStatus': '(attached=True)', - 'V1NodeSelector': '(node_selector_terms=[])', - 'V1NodeSelectorRequirement': '(key="", operator="")', - 'V1PreferredSchedulingTerm': '(preference="", weight=1)', - 'V1PodAffinityTerm': '(topology_key="")', - 'V1WeightedPodAffinityTerm': '(pod_affinity_term="", weight=1)', - 'V1OwnerReference': '(api_version="", kind="", name="", uid="")', - 'V1HTTPHeader': '(name="", value="")', - 'V1TCPSocketAction': '(port="")', - 'V1VolumeDevice': '(device_path="", name="")', - 'V1PodReadinessGate': '(condition_type="")', - 'V1Sysctl': '(name="", value="")', - 'V1ContainerStateTerminated': '(exit_code=0)', - 'V1AzureFilePersistentVolumeSource': '(secret_name="",' - ' share_name="")', - 'V1CephFSPersistentVolumeSource': '(monitors=[])', - 'V1CinderPersistentVolumeSource': '(volume_id="")', - 'V1CSIPersistentVolumeSource': '(driver="", volume_handle="")', - 'V1FlexPersistentVolumeSource': '(driver="")', - 'V1GlusterfsPersistentVolumeSource': '(endpoints="", path="")', - 'V1ISCSIPersistentVolumeSource': '(iqn="", lun=0,' - ' target_portal="")', - 'V1LocalVolumeSource': '(path="")', - 'V1RBDPersistentVolumeSource': '(image="", monitors=[])', - 'V1ScaleIOPersistentVolumeSource': '(' - 'gateway="",' - ' secret_ref="",' - ' system="")', - 'V1DaemonSetStatus': '(current_number_scheduled=0, ' - 'desired_number_scheduled=0, ' - 'number_misscheduled=0, ' - 'number_ready=0)', - 'V1DaemonSetCondition': '(status="", type="")', - 'V1DeploymentCondition': '(status="", type="")', - 'V1ReplicaSetStatus': '(replicas=0)', - 'V1ReplicaSetCondition': '(status="", type="")', - 'V1ResourceRule': '(verbs=[])', - 'V1JobCondition': '(status="", type="")', - 'V1IPBlock': '(cidr="")', - 'V1EphemeralContainer': '(name="")', - 'V1TopologySpreadConstraint': '(max_skew=0, topology_key="",' - ' when_unsatisfiable="")', - 'V1LimitRangeItem': '(type="")' - } - whole_kind = 'V1' + kind - if whole_kind in must_param.keys(): - k8s_obj = eval('client.V1' + kind + must_param.get(whole_kind)) - else: - k8s_obj = eval('client.V1' + kind + '()') - self._init_k8s_obj(k8s_obj, file_content_dict, must_param) + k8s_obj = self._gen_k8s_obj_from_name('V1' + kind) + self._init_k8s_obj(k8s_obj, file_content_dict) return k8s_obj def _get_k8s_obj_from_file_content_dict(self, file_content_dict, @@ -499,7 +360,7 @@ class Transformer(object): name = name.strip() return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower() - def _init_k8s_obj(self, obj, content, must_param): + def _init_k8s_obj(self, obj, content): for key, value in content.items(): param_value = self._get_lower_case_name(key) if hasattr(obj, param_value) and \ @@ -511,12 +372,8 @@ class Transformer(object): if obj_name == 'dict(str, str)': setattr(obj, param_value, value) else: - if obj_name in must_param.keys(): - rely_obj = eval('client.' + obj_name + - must_param.get(obj_name)) - else: - rely_obj = eval('client.' + obj_name + '()') - self._init_k8s_obj(rely_obj, value, must_param) + rely_obj = self._gen_k8s_obj_from_name(obj_name) + self._init_k8s_obj(rely_obj, value) setattr(obj, param_value, rely_obj) elif isinstance(value, list): obj_name = obj.openapi_types.get(param_value) @@ -527,13 +384,8 @@ class Transformer(object): rely_obj_name = \ re.findall(r".*\[([^\[\]]*)\].*", obj_name)[0] for v in value: - if rely_obj_name in must_param.keys(): - rely_obj = eval('client.' + rely_obj_name + - must_param.get(rely_obj_name)) - else: - rely_obj = \ - eval('client.' + rely_obj_name + '()') - self._init_k8s_obj(rely_obj, v, must_param) + rely_obj = self._gen_k8s_obj_from_name(rely_obj_name) + self._init_k8s_obj(rely_obj, v) rely_objs.append(rely_obj) setattr(obj, param_value, rely_objs) @@ -553,9 +405,8 @@ class Transformer(object): return sorted_k8s_objs def get_object_meta(self, content): - must_param = {} v1_object_meta = client.V1ObjectMeta() - self._init_k8s_obj(v1_object_meta, content, must_param) + self._init_k8s_obj(v1_object_meta, content) return v1_object_meta # config_labels configures label