diff --git a/acto/kubectl_client/helm.py b/acto/kubectl_client/helm.py index 21d8f15b5f..66b6ef7f09 100644 --- a/acto/kubectl_client/helm.py +++ b/acto/kubectl_client/helm.py @@ -27,20 +27,26 @@ def install( release_name: str, chart: str, namespace: str, + namespace_existed: Optional[bool] = None, repo: Optional[str] = None, + version: Optional[str] = None, args: Optional[list] = None, ) -> subprocess.CompletedProcess: - """Installs a helm chart""" + """Installs a helm chart. It uses the --wait flag to wait for the deployment to be ready""" cmd = [ "install", release_name, chart, "--namespace", namespace, - "--create-namespace", + "--wait", ] + if namespace_existed is False: + cmd.append("--create-namespace") if repo: cmd.extend(["--repo", repo]) + if version: + cmd.extend(["--version", version]) if args: cmd.extend(args) return self.helm(cmd) diff --git a/acto/kubernetes_engine/base.py b/acto/kubernetes_engine/base.py index 21e18814fe..c8cebe5190 100644 --- a/acto/kubernetes_engine/base.py +++ b/acto/kubernetes_engine/base.py @@ -111,3 +111,8 @@ def get_node_list(self, name: str): # no nodes can be found, returning an empty array return [] return p.stdout.strip().split("\n") + + @staticmethod + def cluster_name(acto_namespace: int, worker_id: int) -> str: + """Helper function to generate cluster name""" + return f"acto-{acto_namespace}-cluster-{worker_id}" diff --git a/acto/kubernetes_engine/minikube.py b/acto/kubernetes_engine/minikube.py index 400bd7c965..648dffe270 100644 --- a/acto/kubernetes_engine/minikube.py +++ b/acto/kubernetes_engine/minikube.py @@ -61,7 +61,7 @@ def create_cluster(self, name: str, kubeconfig: str): else: raise RuntimeError("Missing kubeconfig for minikube create") - cmd.extend(["--nodes", str(self.num_nodes)]) + cmd.extend(["--nodes", str(self.num_nodes + 1)]) if self._k8s_version != "": cmd.extend(["--kubernetes-version", str(self._k8s_version)]) diff --git a/acto/lib/operator_config.py b/acto/lib/operator_config.py index 0543c3d061..8df0e0bb7a 100644 --- a/acto/lib/operator_config.py +++ b/acto/lib/operator_config.py @@ -1,6 +1,9 @@ from typing import Optional import pydantic +from typing_extensions import Self + +from acto.input.constraint import XorCondition DELEGATED_NAMESPACE = "__DELEGATED__" @@ -8,8 +11,7 @@ class ApplyStep(pydantic.BaseModel, extra="forbid"): """Configuration for each step of kubectl apply""" - file: str = pydantic.Field( - description="Path to the file for kubectl apply") + file: str = pydantic.Field(description="Path to the file for kubectl apply") operator: bool = pydantic.Field( description="If the file contains the operator deployment", default=False, @@ -35,20 +37,73 @@ class WaitStep(pydantic.BaseModel, extra="forbid"): ) +class HelmInstallStep(pydantic.BaseModel, extra="forbid"): + """Configuration for each step of helm install""" + + release_name: str = pydantic.Field( + description="Name of the release for helm install", + default="operator-release", + ) + chart: str = pydantic.Field( + description="Path to the chart for helm install" + ) + namespace: Optional[str] = pydantic.Field( + description="Namespace for installing the chart. If not specified, " + + "use the namespace in the chart or Acto namespace. " + + "If set to null, use the namespace in the chart", + default=DELEGATED_NAMESPACE, + ) + repo: Optional[str] = pydantic.Field( + description="Name of the helm repository", default=None + ) + version: Optional[str] = pydantic.Field( + description="Version of the helm chart", default=None + ) + operator: bool = pydantic.Field( + description="If the file contains the operator deployment", + default=False, + ) + operator_deployment_name: Optional[str] = pydantic.Field( + description="The deployment name of the operator in the operator pod, " + "required if there are multiple deployments in the operator pod", + default=None, + ) + operator_container_name: Optional[str] = pydantic.Field( + description="The container name of the operator in the operator pod, " + "required if there are multiple containers in the operator pod", + default=None, + ) + + @pydantic.model_validator(mode="after") + def check_operator_helm_install(self) -> Self: + """Check if the operator helm install is valid""" + if self.operator: + if ( + not self.operator_deployment_name + or not self.operator_container_name + ): + raise ValueError( + "operator_deployment_name and operator_container_name " + + "are required for operator helm install for operator" + ) + return self + + class DeployStep(pydantic.BaseModel, extra="forbid"): """A step of deploying a resource""" - apply: ApplyStep = pydantic.Field( + apply: Optional[ApplyStep] = pydantic.Field( description="Configuration for each step of kubectl apply", default=None ) - wait: WaitStep = pydantic.Field( + wait: Optional[WaitStep] = pydantic.Field( description="Configuration for each step of waiting for the operator", default=None, ) + helm_install: Optional[HelmInstallStep] = pydantic.Field( + description="Configuration for each step of helm install", default=None + ) - # TODO: Add support for helm and kustomize - # helm: str = pydantic.Field( - # description="Path to the file for helm install") + # TODO: Add support and kustomize # kustomize: str = pydantic.Field( # description="Path to the file for kustomize build") @@ -130,6 +185,10 @@ class OperatorConfig(pydantic.BaseModel, extra="forbid"): default=None, description="Name of the CRD, required if there are multiple CRDs", ) + crd_version: Optional[str] = pydantic.Field( + default=None, + description="Version of the CRD, required if there are multiple CRD versions", + ) example_dir: Optional[str] = pydantic.Field( default=None, description="Path to the example dir" ) @@ -139,6 +198,9 @@ class OperatorConfig(pydantic.BaseModel, extra="forbid"): focus_fields: Optional[list[list[str]]] = pydantic.Field( default=None, description="List of focus fields" ) + constraints: Optional[list[XorCondition]] = pydantic.Field( + default=None, description="List of constraints" + ) if __name__ == "__main__": diff --git a/test/integration_tests/test_kubernetes_engines.py b/test/integration_tests/test_kubernetes_engines.py index 477caf3b2b..03fafc0569 100644 --- a/test/integration_tests/test_kubernetes_engines.py +++ b/test/integration_tests/test_kubernetes_engines.py @@ -5,10 +5,11 @@ import pytest # from acto.kubernetes_engine.base import KubernetesEngine +from acto.kubernetes_engine.base import KubernetesEngine from acto.kubernetes_engine.kind import Kind from acto.kubernetes_engine.minikube import Minikube -testcases = [("kind", 3, "v1.27.3")] +testcases = [("kind", 4, "v1.27.3")] @pytest.mark.kubernetes_engine @@ -18,6 +19,7 @@ def test_kubernetes_engines(cluster_type: str, num_nodes, version): config_path = os.path.join(os.path.expanduser("~"), ".kube/test-config") name = "test-cluster" + cluster_instance: KubernetesEngine if cluster_type == "kind": cluster_instance = Kind( acto_namespace=0, num_nodes=num_nodes, version=version @@ -34,7 +36,7 @@ def test_kubernetes_engines(cluster_type: str, num_nodes, version): cluster_instance.create_cluster(name, config_path) node_list = cluster_instance.get_node_list(name) - assert len(node_list) == num_nodes + assert len(node_list) == num_nodes + 1 cluster_instance.delete_cluster(name, config_path) with pytest.raises(RuntimeError):