diff --git a/Tests/kaas/clusterspec.yaml b/Tests/kaas/clusterspec.yaml index b802613cc..832c1b35c 100644 --- a/Tests/kaas/clusterspec.yaml +++ b/Tests/kaas/clusterspec.yaml @@ -3,9 +3,18 @@ clusters: default: branch: default kubeconfig: kubeconfig.yaml - v1.29: - branch: "1.29" - kubeconfig: kubecfg129.yaml - v1.30: - branch: "1.30" - kubeconfig: kubecfg130.yaml + # Cluster-stack specific + # cs_name: + # cs_k8s_version: + # cs_version: + # cs_channel: + # cs_cloudname: + # cs_secret_name: + # Cluster specific + # + # v1.29: + # branch: "1.29" + # kubeconfig: kubecfg129.yaml + # v1.30: + # branch: "1.30" + # kubeconfig: kubecfg130.yaml diff --git a/Tests/kaas/plugin/interface.py b/Tests/kaas/plugin/interface.py index 3ec4c2b34..499b4c082 100644 --- a/Tests/kaas/plugin/interface.py +++ b/Tests/kaas/plugin/interface.py @@ -55,14 +55,8 @@ def create(self, name="scs-cluster", version=None, kubeconfig_filepath=None): """ self.cluster_name = name self.cluster_version = version -<<<<<<< HEAD -======= self.kubeconfig_filepath = kubeconfig_filepath - try: - self._create_cluster() ->>>>>>> Add ability to remove kubeconfig - self._create_cluster() # TODO: maybe we do not need to use try exept here? # try: # self._create_cluster() diff --git a/Tests/kaas/plugin/plugin_cluster_stacks.py b/Tests/kaas/plugin/plugin_cluster_stacks.py index fab451201..6580c8553 100644 --- a/Tests/kaas/plugin/plugin_cluster_stacks.py +++ b/Tests/kaas/plugin/plugin_cluster_stacks.py @@ -2,274 +2,19 @@ import subprocess import base64 import time - +import logging from pytest_kind import KindCluster from interface import KubernetesClusterPlugin - -class PluginClusterStacks(KubernetesClusterPlugin): - """ - Plugin to handle the provisioning of kubernetes cluster for - conformance testing purpose with the use of cluster-stacks - """ - - def __init__(self, config): - self.config = config - self.cs_cluster_name = os.getenv('CS_CLUSTER_NAME', 'cs-cluster') - self.kubeconfig_cs_cluster_filename = f"kubeconfig-{self.cs_cluster_name}" - - # Git-related variables - self.git_provider_b64 = base64.b64encode(b'github').decode('utf-8') - self.git_org_name_b64 = base64.b64encode(b'SovereignCloudStack').decode('utf-8') - self.git_repo_name_b64 = base64.b64encode(b'cluster-stacks').decode('utf-8') - - os.environ['GIT_PROVIDER_B64'] = self.git_provider_b64 - os.environ['GIT_ORG_NAME_B64'] = self.git_org_name_b64 - os.environ['GIT_REPOSITORY_NAME_B64'] = self.git_repo_name_b64 - - # Retrieve the Git token from environment variables - git_access_token = os.getenv('GIT_ACCESS_TOKEN') - if git_access_token: - # Encode the Git access token and set it as an environment variable - encoded_token = base64.b64encode(git_access_token.encode('utf-8')).decode('utf-8') - os.environ['GIT_ACCESS_TOKEN_B64'] = encoded_token - else: - raise ValueError("Error: GIT_ACCESS_TOKEN environment variable not set.") - - # Cluster Stack Parameters - self.cs_name = os.getenv('CS_NAME', 'scs') - self.cs_k8s_version = os.getenv('CS_K8S_VERSION', '1.29') - self.cs_version = os.getenv('CS_VERSION', 'v1') - self.cs_channel = os.getenv('CS_CHANNEL', 'stable') - self.cs_cloudname = os.getenv('CS_CLOUDNAME', 'openstack') - self.cs_secretname = os.getenv('CS_SECRETNAME', self.cs_cloudname) - self.cs_class_name = f"openstack-{self.cs_name}-{self.cs_k8s_version.replace('.', '-')}-{self.cs_version}" - os.environ['CLUSTER_TOPOLOGY'] = 'true' - os.environ['EXP_CLUSTER_RESOURCE_SET'] = 'true' - os.environ['EXP_RUNTIME_SDK'] = 'true' - os.environ['CS_NAME'] = self.cs_name - os.environ['CS_K8S_VERSION'] = self.cs_k8s_version - os.environ['CS_VERSION'] = self.cs_version - os.environ['CS_CHANNEL'] = self.cs_channel - os.environ['CS_CLOUDNAME'] = self.cs_cloudname - os.environ['CS_SECRETNAME'] = self.cs_secretname - os.environ['CS_CLASS_NAME'] = self.cs_class_name - - # CSP-related variables - self.cs_namespace = os.getenv("CS_NAMESPACE", "default") - self.clouds_yaml_path = os.getenv("CLOUDS_YAML_PATH") - - if not self.clouds_yaml_path: - raise ValueError("CLOUDS_YAML_PATH environment variable not set.") - - # Cluster env variables - self.cs_pod_cidr = os.getenv('CS_POD_CIDR', '192.168.0.0/16') - self.cs_service_cidr = os.getenv('CS_SERVICE_CIDR', '10.96.0.0/12') - self.cs_external_id = os.getenv('CS_EXTERNAL_ID', 'ebfe5546-f09f-4f42-ab54-094e457d42ec') - self.cs_k8s_patch_version = os.getenv('CS_K8S_PATCH_VERSION', '6') - - os.environ['CS_POD_CIDR'] = self.cs_pod_cidr - os.environ['CS_SERVICE_CIDR'] = self.cs_service_cidr - os.environ['CS_EXTERNAL_ID'] = self.cs_external_id - os.environ['CS_K8S_PATCH_VERSION'] = self.cs_k8s_patch_version - os.environ['CS_CLUSTER_NAME'] = self.cs_cluster_name - - def _create_cluster(self): - # Step 1: Create the Kind cluster - self.cluster = KindCluster( - self.cluster_name - ) - self.cluster.create() - self.kubeconfig = str(self.cluster.kubeconfig_path.resolve()) - - # Step 2: Export Kubeconfig - os.environ['KUBECONFIG'] = self.kubeconfig - - # Step 3: Initialize clusterctl with OpenStack as the infrastructure provider - try: - subprocess.run( - ["clusterctl", "init", "--infrastructure", "openstack"], - check=True - ) - print("clusterctl init completed successfully with OpenStack provider.") - except subprocess.CalledProcessError as error: - print(f"Error during clusterctl init: {error}") - raise - - # Wait for all CAPI pods to be ready - wait_for_capi_pods_ready() - - # Step 4: Download and apply the infrastructure components YAML with envsubst and kubectl - download_and_apply_cmd_cso = ( - "curl -sSL " - "https://github.com/SovereignCloudStack/cluster-stack-operator/releases/latest/download/cso-infrastructure-components.yaml" - " | /tmp/envsubst | kubectl apply -f -" - ) - download_and_apply_cmd_cspo = ( - "curl -sSL " - "https://github.com/SovereignCloudStack/cluster-stack-provider-openstack/releases/latest/download/cspo-infrastructure-components.yaml" - " | /tmp/envsubst | kubectl apply -f -" - ) - try: - subprocess.run(download_and_apply_cmd_cso, shell=True, check=True) - subprocess.run(download_and_apply_cmd_cspo, shell=True, check=True) - except subprocess.CalledProcessError as error: - print(f"Error during downloading and applying YAML: {error}") - raise - - # Step 5: Define a namespace for a tenant (CSP/per tenant) and get pat to clouds.yaml file from env variable - cs_namespace = os.getenv("CS_NAMESPACE", "default") - clouds_yaml_path = os.getenv("CLOUDS_YAML_PATH") - - if not clouds_yaml_path: - raise ValueError("CLOUDS_YAML_PATH environment variable not set.") - - # Step 6: Deploy CSP-helper chart - helm_command = ( - f"helm upgrade -i csp-helper-{cs_namespace} " - f"-n {cs_namespace} --create-namespace " - "https://github.com/SovereignCloudStack/openstack-csp-helper/releases/latest/download/openstack-csp-helper.tgz " - f"-f {clouds_yaml_path}" - ) - - try: - subprocess.run(helm_command, shell=True, check=True) - except subprocess.CalledProcessError as error: - print(f"Error during Helm upgrade: {error}") - raise - - # Wait for the CSO pods to be ready - wait_for_cso_pods_ready() - - # Step 7: Create Cluster Stack definition (CSP/per tenant) - clusterstack_yaml_path = "clusterstack.yaml" - try: - subprocess.run( - f"/tmp/envsubst < {clusterstack_yaml_path} | kubectl apply -f -", - shell=True, - check=True - ) - print(f"Successfully applied {clusterstack_yaml_path}.") - except subprocess.CalledProcessError as error: - print(f"Error during kubectl apply: {error}") - raise - - # Step 8: Create the workload cluster resource (SCS-User/customer) - cluster_yaml_path = "cluster.yaml" - try: - subprocess.run( - f"/tmp/envsubst < {cluster_yaml_path} | kubectl apply -f -", - shell=True, - check=True - ) - print(f"Successfully applied {cluster_yaml_path}.") - except subprocess.CalledProcessError as error: - print(f"Error during kubectl apply: {error}") - raise - - # Step 9: Get kubeadmcontrolplane name - max_retries = 6 - delay_between_retries = 10 # seconds - for _ in range(max_retries): - try: - kcp_command = "kubectl get kubeadmcontrolplane -o=jsonpath='{.items[0].metadata.name}'" - kcp_name = subprocess.run(kcp_command, shell=True, check=True, capture_output=True, text=True) - kcp_name_stdout = kcp_name.stdout.strip() - if kcp_name_stdout: - print(f"KubeadmControlPlane name: {kcp_name_stdout}") - break - except subprocess.CalledProcessError as error: - print(f"Error getting kubeadmcontrolplane name: {error}") - # Wait before retrying - time.sleep(delay_between_retries) - else: - raise RuntimeError("Failed to get kubeadmcontrolplane name") - - # Step 10: Wait for kubeadmcontrolplane to be available - try: - wait_command = f"kubectl wait kubeadmcontrolplane/{kcp_name.stdout.strip()} --for=condition=Available --timeout=300s" - subprocess.run(wait_command, shell=True, check=True) - except subprocess.CalledProcessError as error: - raise RuntimeError(f"Error waiting for kubeadmcontrolplane to be available: {error}") - - # Step 11: Get kubeconfig of the workload k8s cluster - self.kubeconfig_cs_cluster_path = f"{self.kubeconfig_filepath}/{self.kubeconfig_cs_cluster_filename}" - try: - kubeconfig_command = f"clusterctl get kubeconfig {self.cs_cluster_name} > {self.kubeconfig_cs_cluster_path}" - subprocess.run(kubeconfig_command, shell=True, check=True) - print(f"Kubeconfig of the workload k8s cluster has been saved to {self.kubeconfig_cs_cluster_path}.") - except subprocess.CalledProcessError as error: - raise RuntimeError(f"Error getting kubeconfig of the workload k8s cluster: {error}") - - # Step 12: Wait for clusteraddons resource to become ready - try: - kubeconfig_command = f"kubectl wait clusteraddons/cluster-addon-{self.cs_cluster_name} --for=condition=Ready --timeout=300s" - subprocess.run(kubeconfig_command, shell=True, check=True) - except subprocess.CalledProcessError as error: - raise RuntimeError(f"Error waiting for clusteraddons to be ready: {error}") - - # Step 13: Wait for all system pods in the workload k8s cluster to become ready - try: - wait_pods_command = ( - f"kubectl wait -n kube-system --for=condition=Ready --timeout=300s pod --all --kubeconfig {self.kubeconfig_cs_cluster_path}" - ) - subprocess.run(wait_pods_command, shell=True, check=True) - print("All system pods in the workload Kubernetes cluster are ready.") - except subprocess.CalledProcessError as error: - raise RuntimeError(f"Error waiting for system pods to become ready: {error}") - - def _delete_cluster(self): - # Step 1: Check if the cluster exists and if so delete it - try: - check_cluster_command = f"kubectl get cluster {self.cs_cluster_name} --kubeconfig kubeconfig" - result = subprocess.run(check_cluster_command, shell=True, check=True, capture_output=True, text=True) - - if result.returncode == 0: - print(f"Cluster {self.cs_cluster_name} exists. Proceeding with deletion.") - # Step 2: Delete the cluster with a timeout - delete_cluster_command = ( - f"kubectl delete cluster {self.cs_cluster_name} --kubeconfig kubeconfig --timeout=300s" - ) - try: - subprocess.run(delete_cluster_command, shell=True, check=True, timeout=300) - print(f"Cluster {self.cs_cluster_name} deleted successfully.") - except subprocess.TimeoutExpired: - raise TimeoutError(f"Timed out while deleting the cluster {self.cs_cluster_name}.") - else: - print(f"No cluster named {self.cs_cluster_name} found. Nothing to delete.") - - except subprocess.CalledProcessError as error: - if "NotFound" in error.stderr: - print(f"Cluster {self.cs_cluster_name} not found. Skipping deletion.") - else: - raise RuntimeError(f"Error checking for cluster existence: {error}") - - # Step 3: Remove the kubeconfig file if it exists - kubeconfig_path = self.kubeconfig_cs_cluster_path - if os.path.exists(kubeconfig_path): - try: - os.remove(kubeconfig_path) - print(f"Kubeconfig file at {kubeconfig_path} has been successfully removed.") - except OSError as e: - print(f"Error while trying to remove kubeconfig file: {e}") - else: - print(f"Kubeconfig file at {kubeconfig_path} does not exist or has already been removed.") - - # Step 4: Delete the Kind cluster - try: - self.cluster = KindCluster(self.cluster_name) - self.cluster.delete() - print(f"Kind cluster {self.cluster_name} deleted successfully.") - except Exception as error: - print(f"Error during Kind cluster deletion: {error}") - raise +logger = logging.getLogger("PluginClusterStacks") +# Helper functions def wait_for_capi_pods_ready(timeout=240, interval=15): """ - Wait for all CAPI and CAPO pods to be in the 'Running' state with containers ready. + Waits for all CAPI pods in specific namespaces to reach the 'Running' state with all containers ready. - :param timeout: Maximum time to wait in seconds. + :param timeout: Total time to wait in seconds before giving up. :param interval: Time to wait between checks in seconds. """ namespaces = [ @@ -285,84 +30,292 @@ def wait_for_capi_pods_ready(timeout=240, interval=15): for namespace in namespaces: try: - # Get all pods in the namespace + # Get pod status in the namespace result = subprocess.run( - f"kubectl get pods -n {namespace} -o=jsonpath='{{range .items[*]}}{{.metadata.name}} {{.status.phase}} {{.status.containerStatuses[*].ready}}{{\"\\n\"}}{{end}}'", - shell=True, capture_output=True, text=True + f"kubectl get pods -n {namespace} -o=jsonpath='{{range .items[*]}}{{.metadata.name}} {{.status.phase}} {{range .status.containerStatuses[*]}}{{.ready}} {{end}}{{\"\\n\"}}{{end}}'", + shell=True, capture_output=True, text=True, check=True ) if result.returncode == 0: pods_status = result.stdout.strip().splitlines() for pod_status in pods_status: pod_info = pod_status.split() - pod_name, phase, ready = pod_info[0], pod_info[1], pod_info[2] + pod_name, phase, *readiness_states = pod_info - # Check if pod is in Running phase and containers are ready - if phase != "Running" or ready != "true": + # Check pod phase and all containers readiness + if phase != "Running" or "false" in readiness_states: all_pods_ready = False - print(f"Pod {pod_name} in namespace {namespace} is not ready. Phase: {phase}, Ready: {ready}") + print(f"Pod {pod_name} in {namespace} is not ready. Phase: {phase}, Ready: {readiness_states}") else: - print(f"Error fetching pods in namespace {namespace}: {result.stderr}") + print(f"Error fetching pods in {namespace}: {result.stderr}") all_pods_ready = False except subprocess.CalledProcessError as error: - print(f"Error checking pods in namespace {namespace}: {error}") + print(f"Error checking pods in {namespace}: {error}") all_pods_ready = False if all_pods_ready: - print("All CAPI and CAPO system pods are ready.") + print("All CAPI system pods are ready.") return True - print("Waiting for all CAPI and CAPO pods to become ready...") + print("Waiting for all CAPI pods to become ready...") time.sleep(interval) - raise TimeoutError("Timeout waiting for CAPI and CAPO system pods to become ready.") + raise TimeoutError(f"Timed out after {timeout} seconds waiting for CAPI and CAPO system pods to become ready.") def wait_for_cso_pods_ready(timeout=240, interval=15): """ - Wait for all CSO (Cluster Stack Operator) pods in the 'cso-system' namespace to be in the 'Running' state with containers ready. + Waits for all CSO (Cluster Stack Operator) pods in the 'cso-system' namespace to reach 'Running' with containers ready. - :param timeout: Maximum time to wait in seconds. + :param timeout: Total time to wait in seconds before giving up. :param interval: Time to wait between checks in seconds. """ cso_namespace = "cso-system" - start_time = time.time() while time.time() - start_time < timeout: all_pods_ready = True try: - # Get all pods in the cso-system namespace + # Get pod status in the 'cso-system' namespace result = subprocess.run( - f"kubectl get pods -n {cso_namespace} -o=jsonpath='{{range .items[*]}}{{.metadata.name}} {{.status.phase}} {{.status.containerStatuses[*].ready}}{{\"\\n\"}}{{end}}'", - shell=True, capture_output=True, text=True + f"kubectl get pods -n {cso_namespace} -o=jsonpath='{{range .items[*]}}{{.metadata.name}} {{.status.phase}} {{range .status.containerStatuses[*]}}{{.ready}} {{end}}{{\"\\n\"}}{{end}}'", + shell=True, capture_output=True, text=True, check=True ) if result.returncode == 0: pods_status = result.stdout.strip().splitlines() for pod_status in pods_status: pod_info = pod_status.split() - pod_name, phase, ready = pod_info[0], pod_info[1], pod_info[2] + pod_name, phase, *readiness_states = pod_info - # Check if pod is in Running phase and containers are ready - if phase != "Running" or ready != "true": + # Check pod phase and all containers readiness + if phase != "Running" or "false" in readiness_states: all_pods_ready = False - print(f"Pod {pod_name} in namespace {cso_namespace} is not ready. Phase: {phase}, Ready: {ready}") + print(f"Pod {pod_name} in {cso_namespace} is not ready. Phase: {phase}, Ready: {readiness_states}") else: - print(f"Error fetching pods in namespace {cso_namespace}: {result.stderr}") + print(f"Error fetching pods in {cso_namespace}: {result.stderr}") all_pods_ready = False except subprocess.CalledProcessError as error: - print(f"Error checking pods in namespace {cso_namespace}: {error}") + print(f"Error checking pods in {cso_namespace}: {error}") all_pods_ready = False if all_pods_ready: - print("All CSO pods in the 'cso-system' namespace are ready.") + print("All CSO pods in 'cso-system' namespace are ready.") return True print("Waiting for CSO pods in 'cso-system' namespace to become ready...") time.sleep(interval) - raise TimeoutError("Timeout waiting for CSO pods in 'cso-system' namespace to become ready.") + raise TimeoutError(f"Timed out after {timeout} seconds waiting for CSO pods in 'cso-system' namespace to become ready.") + + +def wait_for_workload_pods_ready(namespace="kube-system", timeout=300, kubeconfig_path=None): + """ + Waits for all pods in a specific namespace on a workload Kubernetes cluster to become ready. + + :param namespace: The Kubernetes namespace where pods are located (default is "kube-system"). + :param timeout: The timeout in seconds to wait for pods to become ready (default is 300). + :param kubeconfig_path: Path to the kubeconfig file for the target Kubernetes cluster. + :raises RuntimeError: If pods are not ready within the specified timeout. + """ + try: + kubeconfig_option = f"--kubeconfig {kubeconfig_path}" if kubeconfig_path else "" + wait_pods_command = ( + f"kubectl wait -n {namespace} --for=condition=Ready --timeout={timeout}s pod --all {kubeconfig_option}" + ) + + # Run the command + subprocess.run(wait_pods_command, shell=True, check=True) + print(f"All pods in namespace '{namespace}' in the workload Kubernetes cluster are ready.") + + except subprocess.CalledProcessError as error: + raise RuntimeError(f"Error waiting for pods in namespace '{namespace}' to become ready: {error}") + + +class PluginClusterStacks(KubernetesClusterPlugin): + def __init__(self, config): + super().__init__(config) + self._setup_environment_variables() + self._setup_git_env() + self.kubeconfig_cs_cluster_filename = f"kubeconfig-{self.cs_cluster_name}" + self.clouds_yaml_path = os.getenv("CLOUDS_YAML_PATH") + + if not self.clouds_yaml_path: + raise ValueError("CLOUDS_YAML_PATH environment variable not set.") + + def _setup_environment_variables(self): + # Cluster Stack Parameters + self.cs_cluster_name = os.getenv('CS_CLUSTER_NAME', 'cs-cluster') + self.cs_name = os.getenv('CS_NAME', 'scs') + self.cs_k8s_version = os.getenv('CS_K8S_VERSION', '1.29') + self.cs_version = os.getenv('CS_VERSION', 'v1') + self.cs_channel = os.getenv('CS_CHANNEL', 'stable') + self.cs_cloudname = os.getenv('CS_CLOUDNAME', 'openstack') + self.cs_secretname = os.getenv('CS_SECRETNAME', self.cs_cloudname) + self.cs_class_name = f"openstack-{self.cs_name}-{self.cs_k8s_version.replace('.', '-')}-{self.cs_version}" + + # CSP-related variables and additional cluster configuration + self.cs_namespace = os.getenv("CS_NAMESPACE", "default") + self.cs_pod_cidr = os.getenv('CS_POD_CIDR', '192.168.0.0/16') + self.cs_service_cidr = os.getenv('CS_SERVICE_CIDR', '10.96.0.0/12') + self.cs_external_id = os.getenv('CS_EXTERNAL_ID', 'ebfe5546-f09f-4f42-ab54-094e457d42ec') + self.cs_k8s_patch_version = os.getenv('CS_K8S_PATCH_VERSION', '6') + + os.environ.update({ + 'CLUSTER_TOPOLOGY': 'true', + 'EXP_CLUSTER_RESOURCE_SET': 'true', + 'EXP_RUNTIME_SDK': 'true', + 'CS_NAME': self.cs_name, + 'CS_K8S_VERSION': self.cs_k8s_version, + 'CS_VERSION': self.cs_version, + 'CS_CHANNEL': self.cs_channel, + 'CS_CLOUDNAME': self.cs_cloudname, + 'CS_SECRETNAME': self.cs_secretname, + 'CS_CLASS_NAME': self.cs_class_name, + 'CS_NAMESPACE': self.cs_namespace, + 'CS_POD_CIDR': self.cs_pod_cidr, + 'CS_SERVICE_CIDR': self.cs_service_cidr, + 'CS_EXTERNAL_ID': self.cs_external_id, + 'CS_K8S_PATCH_VERSION': self.cs_k8s_patch_version, + 'CS_CLUSTER_NAME': self.cs_cluster_name, + }) + + def _setup_git_env(self): + # Setup Git environment variables + git_provider = base64.b64encode(b'github').decode('utf-8') + git_org_name = base64.b64encode(b'SovereignCloudStack').decode('utf-8') + git_repo_name = base64.b64encode(b'cluster-stacks').decode('utf-8') + os.environ.update({ + 'GIT_PROVIDER_B64': git_provider, + 'GIT_ORG_NAME_B64': git_org_name, + 'GIT_REPOSITORY_NAME_B64': git_repo_name + }) + + git_access_token = os.getenv('GIT_ACCESS_TOKEN') + if git_access_token: + os.environ['GIT_ACCESS_TOKEN_B64'] = base64.b64encode(git_access_token.encode()).decode('utf-8') + else: + raise ValueError("GIT_ACCESS_TOKEN environment variable not set.") + + def _create_cluster(self): + # Step 1: Create the Kind cluster + self.cluster = KindCluster(self.cluster_name) + self.cluster.create() + self.kubeconfig = str(self.cluster.kubeconfig_path.resolve()) + os.environ['KUBECONFIG'] = self.kubeconfig + + # Step 2: Initialize clusterctl with OpenStack as the infrastructure provider + self._run_subprocess(["clusterctl", "init", "--infrastructure", "openstack"], "Error during clusterctl init") + + # Wait for all CAPI pods to be ready + wait_for_capi_pods_ready() + + # Step 3: Apply infrastructure components + self._apply_yaml_with_envsubst("cso-infrastructure-components.yaml", "Error applying CSO infrastructure components") + self._apply_yaml_with_envsubst("cspo-infrastructure-components.yaml", "Error applying CSPO infrastructure components") + + # Step 4: Deploy CSP-helper chart + helm_command = ( + f"helm upgrade -i csp-helper-{self.cs_namespace} -n {self.cs_namespace} " + f"--create-namespace https://github.com/SovereignCloudStack/openstack-csp-helper/releases/latest/download/openstack-csp-helper.tgz " + f"-f {self.clouds_yaml_path}" + ) + self._run_subprocess(helm_command, "Error deploying CSP-helper chart", shell=True) + + wait_for_cso_pods_ready() + + # Step 5: Create Cluster Stack definition and workload cluster + self._apply_yaml_with_envsubst("clusterstack.yaml", "Error applying clusterstack.yaml") + self._apply_yaml_with_envsubst("cluster.yaml", "Error applying cluster.yaml") + + # Step 6: Get and wait on kubeadmcontrolplane and retrieve workload cluster kubeconfig + kcp_name = self._get_kubeadm_control_plane_name() + self._wait_kcp_ready(kcp_name) + self._retrieve_kubeconfig() + + # Step 7: Wait for workload system pods to be ready + self.kubeconfig_cs_cluster_path = os.path.join(self.kubeconfig_filepath, self.kubeconfig_cs_cluster_filename) + wait_for_workload_pods_ready(kubeconfig_path=self.kubeconfig_cs_cluster_path) + + def _delete_cluster(self): + try: + check_cluster_command = f"kubectl get cluster {self.cs_cluster_name} --kubeconfig kubeconfig" + result = subprocess.run(check_cluster_command, shell=True, check=True, capture_output=True, text=True) + if result.returncode == 0: + delete_command = f"kubectl delete cluster {self.cs_cluster_name} --kubeconfig kubeconfig --timeout=300s" + self._run_subprocess(delete_command, "Timeout while deleting the cluster", shell=True, timeout=300) + except subprocess.CalledProcessError as error: + if "NotFound" in error.stderr: + logger.info(f"Cluster {self.cs_cluster_name} not found. Skipping deletion.") + else: + raise RuntimeError(f"Error checking for cluster existence: {error}") + + # Step 4: Delete the Kind cluster and cs cluster kubeconfig + self.cluster.delete() + if os.path.exists(self.kubeconfig_cs_cluster_path): + os.remove(self.kubeconfig_cs_cluster_path) + + def _apply_yaml_with_envsubst(self, yaml_file, error_msg): + try: + # Determine if the file is a local path or a URL + if os.path.isfile(yaml_file): + command = f"/tmp/envsubst < {yaml_file} | kubectl apply -f -" + elif yaml_file == "cso-infrastructure-components.yaml": + url = "https://github.com/SovereignCloudStack/cluster-stack-operator/releases/latest/download/cso-infrastructure-components.yaml" + command = f"curl -sSL {url} | /tmp/envsubst | kubectl apply -f -" + elif yaml_file == "cspo-infrastructure-components.yaml": + url = "https://github.com/SovereignCloudStack/cluster-stack-provider-openstack/releases/latest/download/cspo-infrastructure-components.yaml" + command = f"curl -sSL {url} | /tmp/envsubst | kubectl apply -f -" + else: + raise ValueError(f"Unknown file or URL: {yaml_file}") + + self._run_subprocess(command, error_msg, shell=True) + except subprocess.CalledProcessError as error: + raise RuntimeError(f"{error_msg}: {error}") + + def _get_kubeadm_control_plane_name(self): + max_retries = 6 + delay_between_retries = 10 + for _ in range(max_retries): + try: + kcp_name = subprocess.run( + "kubectl get kubeadmcontrolplane -o=jsonpath='{.items[0].metadata.name}'", + shell=True, check=True, capture_output=True, text=True + ) + kcp_name_stdout = kcp_name.stdout.strip() + if kcp_name_stdout: + print(f"KubeadmControlPlane name: {kcp_name_stdout}") + return kcp_name_stdout + except subprocess.CalledProcessError as error: + print(f"Error getting kubeadmcontrolplane name: {error}") + # Wait before retrying + time.sleep(delay_between_retries) + else: + raise RuntimeError("Failed to get kubeadmcontrolplane name") + + def _wait_kcp_ready(self, kcp_name): + try: + self._run_subprocess( + f"kubectl wait kubeadmcontrolplane/{kcp_name} --for=condition=Available --timeout=300s", + "Error waiting for kubeadmcontrolplane availability", + shell=True + ) + except subprocess.CalledProcessError as error: + raise RuntimeError(f"Error waiting for kubeadmcontrolplane to be ready: {error}") + + def _retrieve_kubeconfig(self): + kubeconfig_command = ( + f"clusterctl get kubeconfig {self.cs_cluster_name} > {self.kubeconfig_cs_cluster_filename}" + ) + self._run_subprocess(kubeconfig_command, "Error retrieving kubeconfig", shell=True) + + def _run_subprocess(self, command, error_msg, shell=False, timeout=None): + try: + subprocess.run(command, shell=shell, check=True, timeout=timeout) + logger.info(f"{command} executed successfully") + except subprocess.CalledProcessError as error: + logger.error(f"{error_msg}: {error}") + raise