From 72f131a1e9a79576f887bc64c3dbcf3494fe2150 Mon Sep 17 00:00:00 2001 From: "Jiawei \"Tyler\" Gu" <47795840+tylergu@users.noreply.github.com> Date: Mon, 7 Oct 2024 21:30:21 -0500 Subject: [PATCH] Fix kubernetes engine (#394) * Fix Kubernetes engine Signed-off-by: Tyler Gu * Add missing modules Signed-off-by: Tyler Gu * Debug workflow Signed-off-by: Tyler Gu * Debug workflow Signed-off-by: Tyler Gu * Debug workflow Signed-off-by: Tyler Gu * Debug workflow Signed-off-by: Tyler Gu * Debug workflow Signed-off-by: Tyler Gu --------- Signed-off-by: Tyler Gu --- .github/workflows/unittest.yaml | 2 + acto/input/constraint.py | 89 +++++++++++++++++++ acto/kubectl_client/helm.py | 10 ++- acto/kubernetes_engine/base.py | 5 ++ acto/kubernetes_engine/minikube.py | 2 +- acto/lib/operator_config.py | 76 ++++++++++++++-- .../test_kubernetes_engines.py | 6 +- 7 files changed, 178 insertions(+), 12 deletions(-) create mode 100644 acto/input/constraint.py diff --git a/.github/workflows/unittest.yaml b/.github/workflows/unittest.yaml index e19da32215..4486a4e334 100644 --- a/.github/workflows/unittest.yaml +++ b/.github/workflows/unittest.yaml @@ -40,6 +40,7 @@ jobs: name: .coverage.${{ github.sha }}.unittest path: .coverage.${{ github.sha }}.unittest retention-days: 1 + include-hidden-files: true integration-test: runs-on: ubuntu-latest steps: @@ -74,6 +75,7 @@ jobs: name: .coverage.${{ github.sha }}.integration-test path: .coverage.${{ github.sha }}.integration-test retention-days: 1 + include-hidden-files: true coverage-report: runs-on: ubuntu-latest needs: [unittest, integration-test] diff --git a/acto/input/constraint.py b/acto/input/constraint.py new file mode 100644 index 0000000000..cfb6f15223 --- /dev/null +++ b/acto/input/constraint.py @@ -0,0 +1,89 @@ +"""""" + +from typing import Literal, Optional + +import pydantic + +from acto.common import PropertyPath + + +class XorCondition(pydantic.BaseModel): + """Condition that is the xor of two properties""" + + left: PropertyPath + right: PropertyPath + type: Literal["xor"] + + def solve( + self, assumptions: list[tuple[PropertyPath, bool]] + ) -> Optional[tuple[PropertyPath, bool]]: + """Solve the condition given the assumptions""" + left = None + right = None + for assumption in assumptions: + if assumption[0] == self.left: + left = assumption[1] + if assumption[0] == self.right: + right = assumption[1] + if left is None and right is None: + return None + if left is None and right is not None: + return (self.left, not right) + if left is not None and right is None: + return (self.right, not left) + return None + + +# class Condition: + +# def __init__(self) -> None: +# pass + + +# class Value(abc.ABC): + +# def __init__(self) -> None: +# pass + +# def resolve(self, cr: ValueWithSchema) -> Any: +# pass + + +# class PropertyValue(Value): + +# def __init__(self, property_path: PropertyPath) -> None: +# self.property_path = property_path + +# def resolve(self, cr: ValueWithSchema) -> Any: +# return cr.get_value_by_path(self.property_path.path) + + +# class ConstantValue(Value): + +# def __init__(self, value: Any) -> None: +# self.value = value + +# def resolve(self, cr: ValueWithSchema) -> Any: +# return self.value + + +# class EqualCondition(Condition): + +# def __init__(self, left: Value, right: Value) -> None: +# self.left = left +# self.right = right + +# def solve( +# self, cr: ValueWithSchema, assumptions: list[Condition] +# ) -> Optional[list[Condition]]: +# return self.left.resolve(cr) == self.right.resolve(cr) + + +# class XorCondition(Condition): + +# def __init__(self, left: Condition, right: Condition) -> None: +# self.left = left +# self.right = right + +# def solve(self, cr: ValueWithSchema) -> bool: +# return self.left.solve(cr) ^ self.right.solve(cr) 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):