diff --git a/acto/deploy.py b/acto/deploy.py index 9adf79dcfe..355e536440 100644 --- a/acto/deploy.py +++ b/acto/deploy.py @@ -1,18 +1,19 @@ import time import kubernetes +import yaml -from acto.kubectl_client.kubectl import KubectlClient -from acto.lib.operator_config import DeployConfig import acto.utils as utils from acto.common import * +from acto.kubectl_client.kubectl import KubectlClient +from acto.lib.operator_config import DELEGATED_NAMESPACE, DeployConfig from acto.utils import get_thread_logger from acto.utils.preprocess import add_acto_label def wait_for_pod_ready(apiclient: kubernetes.client.ApiClient): logger = get_thread_logger(with_prefix=True) - logger.debug('Waiting for all pods to be ready') + logger.debug("Waiting for all pods to be ready") time.sleep(5) pod_ready = False for tick in range(600): @@ -22,17 +23,17 @@ def wait_for_pod_ready(apiclient: kubernetes.client.ApiClient): all_pods_ready = True for pod in pods: - if pod.status.phase == 'Succeeded': + if pod.status.phase == "Succeeded": continue if not utils.is_pod_ready(pod): all_pods_ready = False if all_pods_ready: - logger.info('Operator ready') + logger.info("Operator ready") pod_ready = True break time.sleep(5) - logger.info('All pods took %d seconds to get ready' % (tick * 5)) + logger.info("All pods took %d seconds to get ready" % (tick * 5)) if not pod_ready: logger.error("Some pods failed to be ready within timeout") return False @@ -51,7 +52,7 @@ def __init__(self, deploy_config: DeployConfig) -> None: self._operator_yaml = step.apply.file break else: - raise Exception('No operator yaml found in deploy config') + raise Exception("No operator yaml found in deploy config") @property def operator_yaml(self) -> str: @@ -63,22 +64,41 @@ def deploy(self, kubectl_client: KubectlClient, namespace: str): logger = get_thread_logger(with_prefix=True) - print_event('Deploying operator...') + print_event("Deploying operator...") api_client = kubernetes_client(kubeconfig, context_name) ret = utils.create_namespace(api_client, namespace) - if ret == None: - logger.error('Failed to create namespace') + if ret is None: + logger.error("Failed to create namespace") # Run the steps in the deploy config one by one for step in self._deploy_config.steps: if step.apply: + args = ["apply", "--server-side", "-f", step.apply.file, + "--context", context_name] + + # Use the namespace from the argument if the namespace is delegated + # If the namespace from the config is explicitly specified, + # use the specified namespace + # If the namespace from the config is set to None, do not apply + # with namespace + if step.apply.namespace == DELEGATED_NAMESPACE: + args += ["-n", namespace] + elif step.apply.namespace is not None: + args += ["-n", step.apply.namespace] + # Apply the yaml file and then wait for the pod to be ready - kubectl_client.kubectl( - ['apply', '--server-side', '-f', step.apply.file, '-n', namespace, - '--context', context_name]) - if not wait_for_pod_ready(api_client): - logger.error('Failed to deploy operator') + p = kubectl_client.kubectl(args) + if p.returncode != 0: + logger.error( + "Failed to deploy operator due to error from kubectl" + + f" (returncode={p.returncode})" + + f" (stdout={p.stdout})" + + f" (stderr={p.stderr})") + return False + elif not wait_for_pod_ready(api_client): + logger.error( + "Failed to deploy operator due to timeout waiting for pod to be ready") return False elif step.wait: # Simply wait for the specified duration @@ -87,10 +107,12 @@ def deploy(self, # Add acto label to the operator pod add_acto_label(api_client, namespace) if not wait_for_pod_ready(api_client): - logger.error('Failed to deploy operator') + logger.error("Failed to deploy operator") return False - print_event('Operator deployed') + time.sleep(20) + + print_event("Operator deployed") return True def deploy_with_retry(self, @@ -104,5 +126,13 @@ def deploy_with_retry(self, if self.deploy(kubeconfig, context_name, kubectl_client, namespace): return True else: - logger.error('Failed to deploy operator, retrying...') + logger.error("Failed to deploy operator, retrying...") return False + + def operator_name(self) -> str: + with open(self._operator_yaml) as f: + operator_yamls = yaml.load_all(f, Loader=yaml.FullLoader) + for yaml_ in operator_yamls: + if yaml_["kind"] == "Deployment": + return yaml_["metadata"]["name"] + return None diff --git a/acto/lib/operator_config.py b/acto/lib/operator_config.py index c09151f577..095379636b 100644 --- a/acto/lib/operator_config.py +++ b/acto/lib/operator_config.py @@ -2,97 +2,105 @@ from pydantic import BaseModel, Field +DELEGATED_NAMESPACE = "__DELEGATED__" -class ApplyStep(BaseModel, extra='forbid'): + +class ApplyStep(BaseModel, extra="forbid"): """Configuration for each step of kubectl apply""" file: str = Field( - description='Path to the file for kubectl apply') + description="Path to the file for kubectl apply") operator: Optional[bool] = Field( - description='If the file contains the operator deployment', + description="If the file contains the operator deployment", default=False) + namespace: Optional[str] = Field( + description="Namespace for applying the file, if not specified," + + "use the namespace in the file or Acto namespace", + default=DELEGATED_NAMESPACE) -class WaitStep(BaseModel, extra='forbid'): +class WaitStep(BaseModel, extra="forbid"): """Configuration for each step of waiting for the operator""" duration: int = Field( - description='Wait for the specified seconds', + description="Wait for the specified seconds", default=10) -class DeployStep(BaseModel, extra='forbid'): +class DeployStep(BaseModel, extra="forbid"): apply: ApplyStep = Field( - description='Configuration for each step of kubectl apply', default=None) + description="Configuration for each step of kubectl apply", + default=None) wait: WaitStep = Field( - description='Configuration for each step of waiting for the operator', default=None) + description="Configuration for each step of waiting for the operator", + default=None) # TODO: Add support for helm and kustomize # helm: str = Field( - # description='Path to the file for helm install') + # description="Path to the file for helm install") # kustomize: str = Field( - # description='Path to the file for kustomize build') + # description="Path to the file for kustomize build") -class DeployConfig(BaseModel, extra='forbid'): +class DeployConfig(BaseModel, extra="forbid"): """Configuration for deploying the operator""" steps: List[DeployStep] = Field( - description='Steps to deploy the operator', + description="Steps to deploy the operator", min_items=1,) -class AnalysisConfig(BaseModel, extra='forbid'): +class AnalysisConfig(BaseModel, extra="forbid"): "Configuration for static analysis" github_link: str = Field( - description='HTTPS URL for cloning the operator repo') + description="HTTPS URL for cloning the operator repo") commit: str = Field( - description='Commit hash to specify the version to conduct static analysis') - type: str = Field(description='Type name of the CR') + description="Commit hash to specify the version to conduct static analysis") + type: str = Field(description="Type name of the CR") package: str = Field( - description='Package name in which the type of the CR is defined') + description="Package name in which the type of the CR is defined") entrypoint: Optional[str] = Field( - description='The relative path of the main package for the operator') + description="The relative path of the main package for the operator") -class KubernetesEngineConfig(BaseModel, extra='forbid'): +class KubernetesEngineConfig(BaseModel, extra="forbid"): feature_gates: Dict[str, bool] = Field( - description='Path to the feature gates file', default=None) + description="Path to the feature gates file", default=None) -class OperatorConfig(BaseModel, extra='forbid'): +class OperatorConfig(BaseModel, extra="forbid"): """Configuration for porting operators to Acto""" deploy: DeployConfig analysis: Optional[AnalysisConfig] = Field( default=None, - description='Configuration for static analysis') + description="Configuration for static analysis") - seed_custom_resource: str = Field(description='Path to the seed CR file') + seed_custom_resource: str = Field(description="Path to the seed CR file") num_nodes: int = Field( - description='Number of workers in the Kubernetes cluster', default=4) + description="Number of workers in the Kubernetes cluster", default=4) wait_time: int = Field( - description='Timeout duration (seconds) for the resettable timer for system convergence', + description="Timeout duration (seconds) for the resettable timer for system convergence", default=60) collect_coverage: bool = False custom_oracle: Optional[str] = Field( - default=None, description='Path to the custom oracle file') + default=None, description="Path to the custom oracle file") diff_ignore_fields: List[str] = Field(default_factory=list) kubernetes_version: str = Field( - default='v1.22.9', description='Kubernetes version') + default="v1.22.9", description="Kubernetes version") kubernetes_engine: KubernetesEngineConfig = Field( - default=KubernetesEngineConfig(), description='Configuration for the Kubernetes engine' - ) + default=KubernetesEngineConfig(), + description="Configuration for the Kubernetes engine") monkey_patch: Optional[str] = Field( - default=None, description='Path to the monkey patch file') + default=None, description="Path to the monkey patch file") custom_fields: Optional[str] = Field( - default=None, description='Path to the custom fields file') + default=None, description="Path to the custom fields file") crd_name: Optional[str] = Field( - default=None, description='Name of the CRD') + default=None, description="Name of the CRD") blackbox_custom_fields: Optional[str] = Field( - default=None, description='Path to the blackbox custom fields file') + default=None, description="Path to the blackbox custom fields file") k8s_fields: Optional[str] = Field( - default=None, description='Path to the k8s fields file') + default=None, description="Path to the k8s fields file") example_dir: Optional[str] = Field( - default=None, description='Path to the example dir') + default=None, description="Path to the example dir") context: Optional[str] = Field( - default=None, description='Path to the context file') - focus_fields: Optional[List[List[str]]] = Field(default=None, - description='List of focus fields') + default=None, description="Path to the context file") + focus_fields: Optional[List[List[str]]] = Field( + default=None, description="List of focus fields")