Exemple #1
0
def patch_v_s_route_from_yaml(custom_objects: CustomObjectsApi, name,
                              yaml_manifest, namespace) -> None:
    """
    Update a VirtualServerRoute based on yaml manifest

    :param custom_objects: CustomObjectsApi
    :param name:
    :param yaml_manifest: an absolute path to file
    :param namespace:
    :return:
    """
    print(f"Update a VirtualServerRoute: {name}")
    with open(yaml_manifest) as f:
        dep = yaml.safe_load(f)
    try:
        custom_objects.patch_namespaced_custom_object("k8s.nginx.org", "v1",
                                                      namespace,
                                                      "virtualserverroutes",
                                                      name, dep)
        print(
            f"VirtualServerRoute updated with name '{dep['metadata']['name']}'"
        )
    except ApiException as ex:
        logging.exception(
            f"Failed with exception {ex} while patching VirtualServerRoute: {name}"
        )
        raise
def _patch_and_delete_stubborn_custom_resources(  # type: ignore
    group: str,
    version: str,
    plural: str,
    namespace: str,
    status_element: str,
    logger: kopf.Logger,
    use_async=True,
    **_: Any,
):
    logger.info(f"_patch_and_delete_stubborn_custom_resources for {plural}.{group} in namespace {namespace}")
    co = CustomObjectsApi()
    resp = co.list_namespaced_custom_object(group=group, version=version, plural=plural, namespace=namespace)
    failed_res = [
        item.get("metadata").get("name")
        for item in resp["items"]
        if item.get("status", {}).get(status_element) in ["Failed", "Completed", "InProgress"]
    ]
    for item in failed_res:
        try:
            logger.info(f"Patching item {item} in {plural}.{group}")
            patch = json.loads("""{"metadata":{"finalizers":[]}}""")
            co.patch_namespaced_custom_object(
                group=group, version=version, plural=plural, namespace=namespace, name=item, body=patch
            )
            logger.info(f"Deleting item {item} in {plural}.{group}")
            co.delete_namespaced_custom_object(
                group=group,
                version=version,
                plural=plural,
                namespace=namespace,
                name=item,
            )
        except ApiException as e:
            logger.warn("Trying to patch and delete failed: %s\n" % e)
Exemple #3
0
def patch_virtual_server_from_yaml(custom_objects: CustomObjectsApi, name,
                                   yaml_manifest, namespace) -> None:
    """
    Patch a VS based on yaml manifest
    :param custom_objects: CustomObjectsApi
    :param name:
    :param yaml_manifest: an absolute path to file
    :param namespace:
    :return:
    """
    print(f"Update a VirtualServer: {name}, namespace: {namespace}")
    with open(yaml_manifest) as f:
        dep = yaml.safe_load(f)

    try:
        print(f"Try to patch VirtualServer: {dep}")
        custom_objects.patch_namespaced_custom_object("k8s.nginx.org", "v1",
                                                      namespace,
                                                      "virtualservers", name,
                                                      dep)
        print(f"VirtualServer updated with name '{dep['metadata']['name']}'")
    except ApiException:
        logging.exception(
            f"Failed with exception while patching VirtualServer: {name}")
        raise
    except Exception as ex:
        logging.exception(
            f"Failed with exception while patching VirtualServer: {name}, Exception: {ex.with_traceback}"
        )
        raise
 def set_nodepool_node_count(self, kube_api_client: ApiClient, node_count: int) -> None:
     log.info(f"Setting HyperShift cluster {self.name} node count to: {node_count}")
     crd_api = CustomObjectsApi(kube_api_client)
     node_count = node_count
     body = {"spec": {"nodeCount": node_count}}
     crd_api.patch_namespaced_custom_object(
         group=HyperShift.HYPERSHIFT_API_GROUP,
         version=HyperShift.HYPERSHIFT_API_VERSION,
         plural=HyperShift.NODEPOOL_PLOURAL,
         name=self.name,
         namespace=HyperShift.NODEPOOL_NAMESPACE,
         body=body,
     )
 def set_nodepool_replicas(self, node_count: int) -> None:
     log.info(
         f"Setting HyperShift cluster {self.name} replicas to: {node_count}"
     )
     crd_api = CustomObjectsApi(self.management_kube_api_client)
     body = {"spec": {"replicas": node_count}}
     crd_api.patch_namespaced_custom_object(
         group=HyperShift.HYPERSHIFT_API_GROUP,
         version=HyperShift.HYPERSHIFT_API_VERSION,
         plural=HyperShift.NODEPOOL_PLOURAL,
         name=self.name,
         namespace=HyperShift.NODEPOOL_NAMESPACE,
         body=body,
     )
def patch_ts(custom_objects: CustomObjectsApi, namespace, body) -> None:
    """
    Patch a TransportServer
    """
    name = body['metadata']['name']

    print(f"Update a Resource: {name}")

    try:
        custom_objects.patch_namespaced_custom_object(
            "k8s.nginx.org", "v1alpha1", namespace, "transportservers", name, body
        )
    except ApiException:
        logging.exception(f"Failed with exception while patching custom resource: {name}")
        raise
Exemple #7
0
def patch_custom_resource_v1alpha1(custom_objects: CustomObjectsApi, name, yaml_manifest, namespace, plural) -> None:
    """
    Patch a custom resource based on yaml manifest
    """
    print(f"Update a Resource: {name}")
    with open(yaml_manifest) as f:
        dep = yaml.safe_load(f)

    try:
        custom_objects.patch_namespaced_custom_object(
            "k8s.nginx.org", "v1alpha1", namespace, plural, name, dep
        )
    except ApiException:
        logging.exception(f"Failed with exception while patching custom resource: {name}")
        raise
def patch_virtual_server(custom_objects: CustomObjectsApi, name, namespace,
                         body) -> str:
    """
    Update a VirtualServer based on a dict.

    :param custom_objects: CustomObjectsApi
    :param body: dict
    :param namespace:
    :return: str
    """
    print("Update a VirtualServer:")
    custom_objects.patch_namespaced_custom_object("k8s.nginx.org", "v1alpha1",
                                                  namespace, "virtualservers",
                                                  name, body)
    print(f"VirtualServer updated with a name '{body['metadata']['name']}'")
    return body['metadata']['name']
def patch_v_s_route(custom_objects: CustomObjectsApi, name, namespace, body) -> str:
    """
    Update a VirtualServerRoute based on a dict.

    :param custom_objects: CustomObjectsApi
    :param name:
    :param body: dict
    :param namespace:
    :return: str
    """
    print("Update a VirtualServerRoute:")
    custom_objects.patch_namespaced_custom_object(
        "k8s.nginx.org", "v1", namespace, "virtualserverroutes", name, body
    )
    print(f"VirtualServerRoute updated with a name '{body['metadata']['name']}'")
    return body["metadata"]["name"]
def patch_virtual_server_from_yaml(custom_objects: CustomObjectsApi, name,
                                   yaml_manifest, namespace) -> None:
    """
    Update a VS based on yaml manifest

    :param custom_objects: CustomObjectsApi
    :param name:
    :param yaml_manifest: an absolute path to file
    :param namespace:
    :return:
    """
    print(f"Update a VirtualServer: {name}")
    with open(yaml_manifest) as f:
        dep = yaml.safe_load(f)

    custom_objects.patch_namespaced_custom_object("k8s.nginx.org", "v1alpha1",
                                                  namespace, "virtualservers",
                                                  name, dep)
    print(f"VirtualServer updated with name '{dep['metadata']['name']}'")
Exemple #11
0
class Agent(BaseCustomResource):
    """
    A CRD that represents host's agent in assisted-service.
    When host is registered to the cluster the service will create an Agent
    resource and assign it to the relevant cluster.
    In oder to start the installation, all assigned agents must be approved.
    """

    _plural = "agents"

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = consts.DEFAULT_NAMESPACE,
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    @classmethod
    def list(
        cls,
        crd_api: CustomObjectsApi,
        cluster_deployment,
        agents_namespace=None,
    ) -> List["Agent"]:
        agents_namespace = agents_namespace or cluster_deployment.ref.namespace
        resources = crd_api.list_namespaced_custom_object(
            group=consts.CRD_API_GROUP,
            version=consts.CRD_API_VERSION,
            plural=cls._plural,
            namespace=agents_namespace,
        )
        assigned_agents = []
        for item in resources.get("items", []):
            if item["spec"].get("clusterDeploymentName") is None:
                # Unbound late-binding agent, not part of the given cluster_deployment
                continue

            assigned_cluster_ref = ObjectReference(
                name=item["spec"]["clusterDeploymentName"]["name"],
                namespace=item["spec"]["clusterDeploymentName"]["namespace"],
            )
            if assigned_cluster_ref == cluster_deployment.ref:
                assigned_agents.append(
                    cls(
                        kube_api_client=cluster_deployment.crd_api.api_client,
                        name=item["metadata"]["name"],
                        namespace=item["metadata"]["namespace"],
                    )
                )

        return assigned_agents

    def create(self):
        raise RuntimeError("agent resource must be created by the assisted-installer operator")

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=consts.CRD_API_GROUP,
            version=consts.CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def patch(self, **kwargs) -> None:
        body = {"spec": kwargs}

        self.crd_api.patch_namespaced_custom_object(
            group=consts.CRD_API_GROUP,
            version=consts.CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        log.info("patching agent %s: %s", self.ref, pformat(body))

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=consts.CRD_API_GROUP,
            version=consts.CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        log.info("deleted agent %s", self.ref)

    def status(self, timeout: Union[int, float] = consts.DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT) -> dict:
        def _attempt_to_get_status() -> dict:
            return self.get()["status"]

        return waiting.wait(
            _attempt_to_get_status,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f"agent {self.ref} status",
            expected_exceptions=KeyError,
        )

    @property
    def role(self) -> Optional[str]:
        return self.get()["spec"].get("role")

    def set_role(self, role: str) -> None:
        self.patch(role=role)
        log.info(f"set agent {self.ref} role to {role}")

    def approve(self) -> None:
        self.patch(approved=True)
        log.info("approved agent %s", self.ref)

    def bind(self, cluster_deployment) -> None:
        """
        Bind an unbound agent to a cluster deployment
        """
        self.patch(
            clusterDeploymentName={
                "name": cluster_deployment.ref.name,
                "namespace": cluster_deployment.ref.namespace,
            }
        )
        log.info(f"Bound agent {self.ref} to cluster_deployment {cluster_deployment.ref}")

    @classmethod
    def wait_for_agents_to_be_bound(
        cls, agents: List["Agent"], timeout: Union[int, float] = consts.CLUSTER_READY_FOR_INSTALL_TIMEOUT
    ) -> None:
        cls.wait_till_all_agents_are_in_status(
            agents=agents,
            status_type=consts.AgentStatus.BOUND,
            timeout=timeout,
        )

    @classmethod
    def wait_for_agents_to_be_ready_for_install(
        cls, agents: List["Agent"], timeout: Union[int, float] = consts.CLUSTER_READY_FOR_INSTALL_TIMEOUT
    ) -> None:
        for status_type in (
            consts.AgentStatus.SPEC_SYNCED,
            consts.AgentStatus.CONNECTED,
            consts.AgentStatus.REQUIREMENTS_MET,
            consts.AgentStatus.VALIDATED,
        ):
            cls.wait_till_all_agents_are_in_status(
                agents=agents,
                status_type=status_type,
                timeout=timeout,
            )

    @classmethod
    def wait_for_agents_to_unbound(
        cls, agents: List["Agent"], timeout: Union[int, float] = consts.DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT
    ) -> None:
        cls.wait_for_agents_to_be_ready_for_install(agents=agents, timeout=timeout)
        cls.wait_till_all_agents_are_in_status(
            agents=agents,
            status_type=consts.AgentStatus.BOUND,
            status="False",
            timeout=timeout,
        )

    @classmethod
    def wait_for_agents_to_install(
        cls, agents: List["Agent"], timeout: Union[int, float] = consts.CLUSTER_INSTALLATION_TIMEOUT
    ) -> None:
        cls.wait_for_agents_to_be_ready_for_install(agents=agents, timeout=timeout)
        cls.wait_till_all_agents_are_in_status(
            agents=agents,
            status_type=consts.AgentStatus.INSTALLED,
            timeout=timeout,
        )

    @staticmethod
    def are_agents_in_status(
        agents: List["Agent"],
        status_type: str,
        status: str,
    ) -> bool:
        agents_conditions = {
            agent.ref.name: {condition["type"]: condition["status"] for condition in agent.status()["conditions"]}
            for agent in agents
        }

        log.info(
            f"Waiting for agents to have the condition '{status_type}' ="
            f" '{status}' and currently agent conditions are {agents_conditions}"
        )

        return all(agent_conditions.get(status_type, None) == status for agent_conditions in agents_conditions.values())

    @staticmethod
    def wait_till_all_agents_are_in_status(
        agents: List["Agent"],
        status_type: str,
        timeout,
        status="True",
        interval=10,
    ) -> None:
        log.info(f"Now Wait till agents have status as {status_type}")

        waiting.wait(
            lambda: Agent.are_agents_in_status(
                agents,
                status_type,
                status=status,
            ),
            timeout_seconds=timeout,
            sleep_seconds=interval,
            waiting_for=f"Agents to have {status_type} status",
        )
class PrometheusServiceMonitorReconciler(
        KubernetesObjectReconciler[PrometheusServiceMonitor]):
    """Kubernetes Prometheus ServiceMonitor Reconciler"""
    def __init__(self, controller: "KubernetesController") -> None:
        super().__init__(controller)
        self.api_ex = ApiextensionsV1Api(controller.client)
        self.api = CustomObjectsApi(controller.client)

    @property
    def noop(self) -> bool:
        return (not self._crd_exists()) or (self.is_embedded)

    def _crd_exists(self) -> bool:
        """Check if the Prometheus ServiceMonitor exists"""
        return bool(
            len(
                self.api_ex.list_custom_resource_definition(
                    field_selector=f"metadata.name={CRD_NAME}").items))

    def get_reference_object(self) -> PrometheusServiceMonitor:
        """Get service monitor object for outpost"""
        return PrometheusServiceMonitor(
            apiVersion=f"{CRD_GROUP}/{CRD_VERSION}",
            kind="ServiceMonitor",
            metadata=PrometheusServiceMonitorMetadata(
                name=self.name,
                namespace=self.namespace,
                labels=self.get_object_meta().labels,
            ),
            spec=PrometheusServiceMonitorSpec(
                endpoints=[
                    PrometheusServiceMonitorSpecEndpoint(port="http-metrics", )
                ],
                selector=PrometheusServiceMonitorSpecSelector(
                    matchLabels=self.get_object_meta(name=self.name).labels, ),
            ),
        )

    def create(self, reference: PrometheusServiceMonitor):
        return self.api.create_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            plural=CRD_PLURAL,
            namespace=self.namespace,
            body=asdict(reference),
            field_manager=FIELD_MANAGER,
        )

    def delete(self, reference: PrometheusServiceMonitor):
        return self.api.delete_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            namespace=self.namespace,
            plural=CRD_PLURAL,
            name=self.name,
        )

    def retrieve(self) -> PrometheusServiceMonitor:
        return from_dict(
            PrometheusServiceMonitor,
            self.api.get_namespaced_custom_object(
                group=CRD_GROUP,
                version=CRD_VERSION,
                namespace=self.namespace,
                plural=CRD_PLURAL,
                name=self.name,
            ),
        )

    def update(self, current: PrometheusServiceMonitor,
               reference: PrometheusServiceMonitor):
        return self.api.patch_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            namespace=self.namespace,
            plural=CRD_PLURAL,
            name=self.name,
            body=asdict(reference),
            field_manager=FIELD_MANAGER,
        )
Exemple #13
0
class ClusterDeployment(BaseCustomResource):
    """
    A CRD that represents cluster in assisted-service.
    On creation the cluster will be registered to the service.
    On deletion it will be unregistered from the service.
    When has sufficient data installation will start automatically.
    """

    _plural = "clusterdeployments"
    _platform_field = {"platform": {"agentBareMetal": {"agentSelector": {}}}}

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = env_variables["namespace"],
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    def create_from_yaml(self, yaml_data: dict) -> None:
        self.crd_api.create_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            body=yaml_data,
            namespace=self.ref.namespace,
        )

        logger.info("created cluster deployment %s: %s", self.ref, pformat(yaml_data))

    def create(
            self,
            secret: Secret,
            base_domain: str = env_variables["base_domain"],
            agent_cluster_install_ref: Optional[ObjectReference] = None,
            **kwargs,
    ):
        body = {
            "apiVersion": f"{HIVE_API_GROUP}/{HIVE_API_VERSION}",
            "kind": "ClusterDeployment",
            "metadata": self.ref.as_dict(),
            "spec": {
                "clusterName": self.ref.name,
                "baseDomain": base_domain,
                "pullSecretRef": secret.ref.as_dict(),
            }
        }
        body["spec"].update(self._platform_field)

        if agent_cluster_install_ref:
            body["spec"]["clusterInstallRef"] = agent_cluster_install_ref.as_dict()

        body["spec"].update(kwargs)
        self.crd_api.create_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            body=body,
            namespace=self.ref.namespace,
        )

        logger.info("created cluster deployment %s: %s", self.ref, pformat(body))

    def patch(
        self,
        secret: Optional[Secret] = None,
        **kwargs,
    ) -> None:
        body = {"spec": kwargs}
        body["spec"]["platform"] = {"agentBareMetal": {}}

        spec = body["spec"]
        body["spec"].update(self._platform_field)

        if secret:
            spec["pullSecretRef"] = secret.ref.as_dict()

        if "agent_cluster_install_ref" in kwargs:
            spec["clusterInstallRef"] = kwargs["agent_cluster_install_ref"].as_dict()

        self.crd_api.patch_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info("patching cluster deployment %s: %s", self.ref, pformat(body))

    def annotate_install_config(self, install_config: str) -> None:
        body = {"metadata": {"annotations": {f"{CRD_API_GROUP}/install-config-overrides": install_config}}}

        self.crd_api.patch_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info("patching cluster install config %s: %s", self.ref, pformat(body))

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        logger.info("deleted cluster deployment %s", self.ref)

    def status(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT,
    ) -> dict:
        """
        Status is a section in the CRD that is created after registration to
        assisted service and it defines the observed state of ClusterDeployment.
        Since the status key is created only after resource is processed by the
        controller in the service, it might take a few seconds before appears.
        """

        def _attempt_to_get_status() -> dict:
            return self.get()["status"]

        return waiting.wait(
            _attempt_to_get_status,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f"cluster {self.ref} status",
            expected_exceptions=KeyError,
        )

    def condition(
            self,
            cond_type,
            timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT,
    ) -> Tuple[Optional[str], Optional[str]]:
        for condition in self.status(timeout).get("conditions", []):
            if cond_type == condition.get("type"):
                return condition.get("status"), condition.get("reason")
        return None, None

    def wait_for_condition(
        self,
        cond_type: str,
        required_status: str,
        required_reason: Optional[str] = None,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT,
    ) -> None:
        def _has_required_condition() -> Optional[bool]:
            status, reason = self.condition(cond_type=cond_type, timeout=0.5)
            if status == required_status:
                if required_reason:
                    return required_reason == reason
                return True
            return False

        logger.info(
            "Waiting till cluster will be in condition %s with status: %s "
            "reason: %s", cond_type, required_status, required_reason
        )

        waiting.wait(
            _has_required_condition,
            timeout_seconds=timeout,
            waiting_for=f"cluster {self.ref} condition {cond_type} to be in {required_status}",
            expected_exceptions=waiting.exceptions.TimeoutExpired,
        )

    def list_agents(self) -> List[Agent]:
        return Agent.list(self.crd_api, self)

    def wait_for_agents(
        self,
        num_agents: int = 1,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_AGENTS_TIMEOUT,
    ) -> List[Agent]:
        def _wait_for_sufficient_agents_number() -> List[Agent]:
            agents = self.list_agents()
            return agents if len(agents) == num_agents else []

        return waiting.wait(
            _wait_for_sufficient_agents_number,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f"cluster {self.ref} to have {num_agents} agents",
        )
Exemple #14
0
class ClusterDeployment(BaseCustomResource):
    """
    A CRD that represents cluster in assisted-service.
    On creation the cluster will be registered to the service.
    On deletion it will be unregistered from the service.
    When has sufficient data installation will start automatically.
    """
    _plural = 'clusterdeployments'

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = env_variables['namespace'],
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    def create_from_yaml(self, yaml_data: dict) -> None:
        self.crd_api.create_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            body=yaml_data,
            namespace=self.ref.namespace,
        )

        logger.info('created cluster deployment %s: %s', self.ref,
                    pformat(yaml_data))

    def create(
        self,
        platform: Platform,
        install_strategy: InstallStrategy,
        secret: Secret,
        base_domain: str = env_variables['base_domain'],
        **kwargs,
    ):
        body = {
            'apiVersion': f'{HIVE_API_GROUP}/{HIVE_API_VERSION}',
            'kind': 'ClusterDeployment',
            'metadata': self.ref.as_dict(),
            'spec': {
                'clusterName': self.ref.name,
                'baseDomain': base_domain,
                'platform': platform.as_dict(),
                'provisioning': {
                    'installStrategy': install_strategy.as_dict()
                },
                'pullSecretRef': secret.ref.as_dict(),
            }
        }
        body['spec'].update(kwargs)
        self.crd_api.create_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            body=body,
            namespace=self.ref.namespace,
        )

        logger.info('created cluster deployment %s: %s', self.ref,
                    pformat(body))

    def patch(
        self,
        platform: Optional[Platform] = None,
        install_strategy: Optional[InstallStrategy] = None,
        secret: Optional[Secret] = None,
        **kwargs,
    ) -> None:
        body = {'spec': kwargs}

        spec = body['spec']
        if platform:
            spec['platform'] = platform.as_dict()

        if install_strategy:
            spec['provisioning'] = {
                'installStrategy': install_strategy.as_dict()
            }

        if secret:
            spec['pullSecretRef'] = secret.ref.as_dict()

        self.crd_api.patch_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info('patching cluster deployment %s: %s', self.ref,
                    pformat(body))

    def annotate_install_config(self, install_config: str) -> None:
        body = {
            'metadata': {
                'annotations': {
                    f'{CRD_API_GROUP}/install-config-overrides': install_config
                }
            }
        }

        self.crd_api.patch_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info('patching cluster install config %s: %s', self.ref,
                    pformat(body))

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=HIVE_API_GROUP,
            version=HIVE_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        logger.info('deleted cluster deployment %s', self.ref)

    def status(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT,
    ) -> dict:
        """
        Status is a section in the CRD that is created after registration to
        assisted service and it defines the observed state of ClusterDeployment.
        Since the status key is created only after resource is processed by the
        controller in the service, it might take a few seconds before appears.
        """
        def _attempt_to_get_status() -> dict:
            return self.get()['status']

        return waiting.wait(
            _attempt_to_get_status,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f'cluster {self.ref} status',
            expected_exceptions=KeyError,
        )

    def state(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT,
    ) -> Tuple[str, str]:
        state, state_info = None, None
        for condition in self.status(timeout).get('conditions', []):
            reason = condition.get('reason')

            if reason == 'AgentPlatformState':
                state = condition.get('message')
            elif reason == 'AgentPlatformStateInfo':
                state_info = condition.get('message')

            if state and state_info:
                break

        return state, state_info

    def wait_for_state(
        self,
        required_state: str,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT,
        *,
        raise_on_states: Iterable[str] = FAILURE_STATES,
    ) -> None:
        required_state = required_state.lower()
        raise_on_states = [x.lower() for x in raise_on_states]

        def _has_required_state() -> Optional[bool]:
            state, state_info = self.state(timeout=0.5)
            state = state.lower() if state else state
            if state == required_state:
                return True
            elif state in raise_on_states:
                raise UnexpectedStateError(
                    f'while waiting for state `{required_state}`, cluster '
                    f'{self.ref} state changed unexpectedly to `{state}`: '
                    f'{state_info}')

        logger.info("Waiting till cluster will be in %s state", required_state)
        waiting.wait(
            _has_required_state,
            timeout_seconds=timeout,
            waiting_for=f'cluster {self.ref} state to be {required_state}',
            expected_exceptions=waiting.exceptions.TimeoutExpired,
        )

    def list_agents(self) -> List[Agent]:
        return Agent.list(self.crd_api, self)

    def wait_for_agents(
        self,
        num_agents: int = 1,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_AGENTS_TIMEOUT,
    ) -> List[Agent]:
        def _wait_for_sufficient_agents_number() -> List[Agent]:
            agents = self.list_agents()
            return agents if len(agents) == num_agents else []

        return waiting.wait(
            _wait_for_sufficient_agents_number,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f'cluster {self.ref} to have {num_agents} agents',
        )

    def wait_to_be_installed(
        self,
        timeout: Union[int,
                       float] = DEFAULT_WAIT_FOR_INSTALLATION_COMPLETE_TIMEOUT,
    ) -> None:

        waiting.wait(
            lambda: self.get()['spec'].get('installed') is True,
            timeout_seconds=timeout,
            waiting_for=f'cluster {self.ref} state installed',
            expected_exceptions=waiting.exceptions.TimeoutExpired,
        )

    def download_kubeconfig(self, kubeconfig_path):
        def _get_kubeconfig_secret() -> dict:
            return self.get(
            )['spec']['clusterMetadata']['adminKubeconfigSecretRef']

        secret_ref = waiting.wait(
            _get_kubeconfig_secret,
            sleep_seconds=1,
            timeout_seconds=240,
            expected_exceptions=KeyError,
            waiting_for=f'kubeconfig secret creation for cluster {self.ref}',
        )

        kubeconfig_data = Secret(
            kube_api_client=self.crd_api.api_client,
            **secret_ref,
        ).get().data['kubeconfig']

        with open(kubeconfig_path, 'wt') as kubeconfig_file:
            kubeconfig_file.write(b64decode(kubeconfig_data).decode())
Exemple #15
0
class InfraEnv(BaseCustomResource):
    """
    InfraEnv is used to generate cluster iso.
    Image is automatically generated on CRD deployment, after InfraEnv is
    reconciled. Image download url will be exposed in the status.
    """
    _plural = 'infraenvs'

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = env_variables['namespace'],
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    def create_from_yaml(self, yaml_data: dict) -> None:
        self.crd_api.create_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            body=yaml_data,
            namespace=self.ref.namespace,
        )

        logger.info('created infraEnv %s: %s', self.ref, pformat(yaml_data))

    def create(
        self,
        cluster_deployment: ClusterDeployment,
        secret: Secret,
        proxy: Optional[Proxy] = None,
        label_selector: Optional[Dict[str, str]] = None,
        ignition_config_override: Optional[str] = None,
        nmstate_label: Optional[str] = None,
        **kwargs,
    ) -> None:
        body = {
            'apiVersion': f'{CRD_API_GROUP}/{CRD_API_VERSION}',
            'kind': 'InfraEnv',
            'metadata': self.ref.as_dict(),
            'spec': {
                'clusterRef': cluster_deployment.ref.as_dict(),
                'pullSecretRef': secret.ref.as_dict(),
                'nmStateConfigLabelSelector': {
                    'matchLabels': {
                        f'{CRD_API_GROUP}/selector-nmstate-config-name':
                        nmstate_label or ''
                    }
                },
                'agentLabelSelector': {
                    'matchLabels': label_selector or {}
                },
                'ignitionConfigOverride': ignition_config_override or '',
            }
        }
        spec = body['spec']
        if proxy:
            spec['proxy'] = proxy.as_dict()
        spec.update(kwargs)
        self.crd_api.create_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            body=body,
            namespace=self.ref.namespace,
        )

        logger.info('created infraEnv %s: %s', self.ref, pformat(body))

    def patch(
        self,
        cluster_deployment: Optional[ClusterDeployment],
        secret: Optional[Secret],
        proxy: Optional[Proxy] = None,
        label_selector: Optional[Dict[str, str]] = None,
        ignition_config_override: Optional[str] = None,
        nmstate_label: Optional[str] = None,
        **kwargs,
    ) -> None:
        body = {'spec': kwargs}

        spec = body['spec']
        if cluster_deployment:
            spec['clusterRef'] = cluster_deployment.ref.as_dict()

        if secret:
            spec['pullSecretRef'] = secret.ref.as_dict()

        if proxy:
            spec['proxy'] = proxy.as_dict()

        if label_selector:
            spec['agentLabelSelector'] = {'matchLabels': label_selector}

        if nmstate_label:
            spec['nmStateConfigLabelSelector'] = {
                'matchLabels': {
                    f'{CRD_API_GROUP}/selector-nmstate-config-name':
                    nmstate_label,
                }
            }

        if ignition_config_override:
            spec['ignitionConfigOverride'] = ignition_config_override

        self.crd_api.patch_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info('patching infraEnv %s: %s', self.ref, pformat(body))

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        logger.info('deleted infraEnv %s', self.ref)

    def status(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT,
    ) -> dict:
        """
        Status is a section in the CRD that is created after registration to
        assisted service and it defines the observed state of InfraEnv.
        Since the status key is created only after resource is processed by the
        controller in the service, it might take a few seconds before appears.
        """
        def _attempt_to_get_status() -> dict:
            return self.get()['status']

        return waiting.wait(
            _attempt_to_get_status,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f'infraEnv {self.ref} status',
            expected_exceptions=KeyError,
        )

    def get_iso_download_url(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_ISO_URL_TIMEOUT,
    ):
        def _attempt_to_get_image_url() -> str:
            return self.get()['status']['isoDownloadURL']

        return waiting.wait(
            _attempt_to_get_image_url,
            sleep_seconds=3,
            timeout_seconds=timeout,
            waiting_for='image to be created',
            expected_exceptions=KeyError,
        )

    def get_cluster_id(self):
        iso_download_url = self.get_iso_download_url()
        return ISO_URL_PATTERN.match(iso_download_url).group('cluster_id')
Exemple #16
0
class NMStateConfig(BaseCustomResource):
    """Configure nmstate (static IP) related settings for agents."""
    _plural = 'nmstateconfigs'

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = env_variables['namespace'],
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    def create_from_yaml(self, yaml_data: dict) -> None:
        self.crd_api.create_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            body=yaml_data,
            namespace=self.ref.namespace,
        )

        logger.info('created nmstate config %s: %s', self.ref,
                    pformat(yaml_data))

    def create(
        self,
        config: dict,
        interfaces: list,
        label: Optional[str] = None,
        **kwargs,
    ) -> None:
        body = {
            'apiVersion': f'{CRD_API_GROUP}/{CRD_API_VERSION}',
            'kind': 'NMStateConfig',
            'metadata': {
                'labels': {
                    f'{CRD_API_GROUP}/selector-nmstate-config-name': label,
                },
                **self.ref.as_dict()
            },
            'spec': {
                'config': config,
                'interfaces': interfaces,
            }
        }
        body['spec'].update(kwargs)
        self.crd_api.create_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            body=body,
            namespace=self.ref.namespace,
        )

        logger.info('created nmstate config %s: %s', self.ref, pformat(body))

    def patch(
        self,
        config: dict,
        interfaces: list,
        **kwargs,
    ) -> None:
        body = {'spec': kwargs}

        spec = body['spec']
        if config:
            spec['config'] = config

        if interfaces:
            spec['interfaces'] = interfaces

        self.crd_api.patch_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info('patching nmstate config %s: %s', self.ref, pformat(body))

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        logger.info('deleted nmstate config %s', self.ref)

    def status(
        self,
        timeout: Union[int,
                       float] = DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT) -> dict:
        """
        Status is a section in the CRD that is created after registration to
        assisted service and it defines the observed state of NMStateConfig.
        Since the status key is created only after resource is processed by the
        controller in the service, it might take a few seconds before appears.
        """
        def _attempt_to_get_status() -> dict:
            return self.get()['status']

        return waiting.wait(_attempt_to_get_status,
                            sleep_seconds=0.5,
                            timeout_seconds=timeout,
                            waiting_for=f'nmstate config {self.ref} status',
                            expected_exceptions=KeyError)
Exemple #17
0
class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]
                                  ):
    """Kubernetes Traefik Middleware Reconciler"""
    def __init__(self, controller: "KubernetesController") -> None:
        super().__init__(controller)
        self.api_ex = ApiextensionsV1Api(controller.client)
        self.api = CustomObjectsApi(controller.client)

    @property
    def noop(self) -> bool:
        if not ProxyProvider.objects.filter(
                outpost__in=[self.controller.outpost],
                mode__in=[ProxyMode.FORWARD_SINGLE, ProxyMode.FORWARD_DOMAIN],
        ).exists():
            self.logger.debug("No providers with forward auth enabled.")
            return True
        if not self._crd_exists():
            self.logger.debug("CRD doesn't exist")
            return True
        return False

    def _crd_exists(self) -> bool:
        """Check if the traefik middleware exists"""
        return bool(
            len(
                self.api_ex.list_custom_resource_definition(
                    field_selector=f"metadata.name={CRD_NAME}").items))

    def reconcile(self, current: TraefikMiddleware,
                  reference: TraefikMiddleware):
        super().reconcile(current, reference)
        if current.spec.forwardAuth.address != reference.spec.forwardAuth.address:
            raise NeedsUpdate()
        if (current.spec.forwardAuth.authResponseHeadersRegex !=
                reference.spec.forwardAuth.authResponseHeadersRegex):
            raise NeedsUpdate()
        # Ensure all of our headers are set, others can be added by the user.
        if not set(current.spec.forwardAuth.authResponseHeaders).issubset(
                reference.spec.forwardAuth.authResponseHeaders):
            raise NeedsUpdate()

    def get_reference_object(self) -> TraefikMiddleware:
        """Get deployment object for outpost"""
        return TraefikMiddleware(
            apiVersion=f"{CRD_GROUP}/{CRD_VERSION}",
            kind="Middleware",
            metadata=TraefikMiddlewareMetadata(
                name=self.name,
                namespace=self.namespace,
                labels=self.get_object_meta().labels,
            ),
            spec=
            TraefikMiddlewareSpec(forwardAuth=TraefikMiddlewareSpecForwardAuth(
                address=
                f"http://{self.name}.{self.namespace}:9000/akprox/auth/traefik",
                authResponseHeaders=[
                    "X-authentik-username",
                    "X-authentik-groups",
                    "X-authentik-email",
                    "X-authentik-name",
                    "X-authentik-uid",
                    "X-authentik-jwt",
                    "X-authentik-meta-jwks",
                    "X-authentik-meta-outpost",
                    "X-authentik-meta-provider",
                    "X-authentik-meta-app",
                    "X-authentik-meta-version",
                ],
                authResponseHeadersRegex="",
                trustForwardHeader=True,
            )),
        )

    def create(self, reference: TraefikMiddleware):
        return self.api.create_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            plural=CRD_PLURAL,
            namespace=self.namespace,
            body=asdict(reference),
            field_manager=FIELD_MANAGER,
        )

    def delete(self, reference: TraefikMiddleware):
        return self.api.delete_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            namespace=self.namespace,
            plural=CRD_PLURAL,
            name=self.name,
        )

    def retrieve(self) -> TraefikMiddleware:
        return from_dict(
            TraefikMiddleware,
            self.api.get_namespaced_custom_object(
                group=CRD_GROUP,
                version=CRD_VERSION,
                namespace=self.namespace,
                plural=CRD_PLURAL,
                name=self.name,
            ),
        )

    def update(self, current: TraefikMiddleware, reference: TraefikMiddleware):
        return self.api.patch_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            namespace=self.namespace,
            plural=CRD_PLURAL,
            name=self.name,
            body=asdict(reference),
            field_manager=FIELD_MANAGER,
        )
class ClusterDeployment(BaseCustomResource):
    """
    A CRD that represents cluster in assisted-service.
    On creation the cluster will be registered to the service.
    On deletion it will be unregistered from the service.
    When has sufficient data installation will start automatically.
    """

    _hive_api_group = 'hive.openshift.io'
    _plural = 'clusterdeployments'

    def __init__(self,
                 kube_api_client: ApiClient,
                 name: str,
                 namespace: str = env_variables['namespace']):
        BaseCustomResource.__init__(self, name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)
        self._assigned_secret = None

    @property
    def secret(self) -> Secret:
        return self._assigned_secret

    def create_from_yaml(self, yaml_data: dict) -> None:
        self.crd_api.create_namespaced_custom_object(
            group=self._hive_api_group,
            version='v1',
            plural=self._plural,
            body=yaml_data,
            namespace=self.ref.namespace)
        secret_ref = yaml_data['spec']['pullSecretRef']
        self._assigned_secret = Secret(
            kube_api_client=self.crd_api.api_client,
            name=secret_ref['name'],
        )

        logger.info('created cluster deployment %s: %s', self.ref,
                    pformat(yaml_data))

    def create(self,
               platform: Platform,
               install_strategy: InstallStrategy,
               secret: Secret,
               base_domain: str = env_variables['base_domain'],
               **kwargs) -> None:
        body = {
            'apiVersion': f'{self._hive_api_group}/v1',
            'kind': 'ClusterDeployment',
            'metadata': self.ref.as_dict(),
            'spec': {
                'clusterName': self.ref.name,
                'baseDomain': base_domain,
                'platform': platform.as_dict(),
                'provisioning': {
                    'installStrategy': install_strategy.as_dict()
                },
                'pullSecretRef': secret.ref.as_dict(),
            }
        }
        body['spec'].update(kwargs)
        self.crd_api.create_namespaced_custom_object(
            group=self._hive_api_group,
            version='v1',
            plural=self._plural,
            body=body,
            namespace=self.ref.namespace)
        self._assigned_secret = secret

        logger.info('created cluster deployment %s: %s', self.ref,
                    pformat(body))

    def patch(self,
              platform: Optional[Platform] = None,
              install_strategy: Optional[InstallStrategy] = None,
              secret: Optional[Secret] = None,
              **kwargs) -> None:
        body = {'spec': kwargs}

        spec = body['spec']
        if platform:
            spec['platform'] = platform.as_dict()

        if install_strategy:
            spec['provisioning'] = {
                'installStrategy': install_strategy.as_dict()
            }

        if secret:
            spec['pullSecretRef'] = secret.ref.as_dict()

        self.crd_api.patch_namespaced_custom_object(
            group=self._hive_api_group,
            version='v1',
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body)

        logger.info('patching cluster deployment %s: %s', self.ref,
                    pformat(body))

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=self._hive_api_group,
            version='v1',
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace)

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=self._hive_api_group,
            version='v1',
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace)

        logger.info('deleted cluster deployment %s', self.ref)

    def status(
        self,
        timeout: Union[int,
                       float] = DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT) -> dict:
        """
        Status is a section in the CRD that is created after registration to
        assisted service and it defines the observed state of ClusterDeployment.
        Since the status key is created only after resource is processed by the
        controller in the service, it might take a few seconds before appears.
        """
        def _attempt_to_get_status() -> dict:
            return self.get()['status']

        return waiting.wait(_attempt_to_get_status,
                            sleep_seconds=0.5,
                            timeout_seconds=timeout,
                            waiting_for=f'cluster {self.ref} status',
                            expected_exceptions=KeyError)

    def state(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT
    ) -> Tuple[str, str]:
        state, state_info = None, None
        for condition in self.status(timeout).get('conditions', []):
            reason = condition.get('reason')

            if reason == 'AgentPlatformState':
                state = condition.get('message')
            elif reason == 'AgentPlatformStateInfo':
                state_info = condition.get('message')

            if state and state_info:
                break

        return state, state_info

    def wait_for_state(
        self,
        required_state: str,
        timeout: Union[int,
                       float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT) -> None:
        required_state = required_state.lower()

        def _has_required_state() -> bool:
            state, _ = self.state(timeout=0.5)
            return state.lower() == required_state

        waiting.wait(
            _has_required_state,
            timeout_seconds=timeout,
            waiting_for=f'cluster {self.ref} state to be {required_state}',
            expected_exceptions=waiting.exceptions.TimeoutExpired)
class AgentClusterInstall(BaseCustomResource):
    """ This CRD represents a request to provision an agent based cluster.
        In the AgentClusterInstall, the user can specify requirements like
        networking, number of control plane and workers nodes and more.
        The installation will start automatically if the required number of
        hosts is available, the hosts are ready to be installed and the Agents
        are approved.
        The AgentClusterInstall reflects the ClusterDeployment/Installation
        status through Conditions."""

    _api_group = "extensions.hive.openshift.io"
    _api_version = "v1beta1"
    _plural = "agentclusterinstalls"
    _kind = "AgentClusterInstall"
    _requirements_met_condition_name = "RequirementsMet"
    _completed_condition_name = "Completed"

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = consts.DEFAULT_NAMESPACE,
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)
        self.ref.kind = self._kind
        self.ref.group = self._api_group
        self.ref.version = self._api_version

    def create(
        self,
        cluster_deployment_ref: ObjectReference,
        cluster_cidr: str,
        host_prefix: int,
        service_network: str,
        control_plane_agents: int,
        **kwargs,
    ) -> None:
        body = {
            "apiVersion":
            f"{self._api_group}/{self._api_version}",
            "kind":
            self._kind,
            "metadata":
            self.ref.as_dict(),
            "spec":
            self._get_spec_dict(
                cluster_deployment_ref=cluster_deployment_ref,
                cluster_cidr=cluster_cidr,
                host_prefix=host_prefix,
                service_network=service_network,
                control_plane_agents=control_plane_agents,
                **kwargs,
            ),
        }

        self.crd_api.create_namespaced_custom_object(
            group=self._api_group,
            version=self._api_version,
            plural=self._plural,
            body=body,
            namespace=self.ref.namespace,
        )

        logger.info("created agentclusterinstall %s: %s", self.ref,
                    pformat(body))

    def patch(
        self,
        cluster_deployment_ref: ObjectReference,
        cluster_cidr: str,
        host_prefix: int,
        service_network: str,
        control_plane_agents: int,
        **kwargs,
    ) -> None:
        body = {
            "spec":
            self._get_spec_dict(
                cluster_deployment_ref=cluster_deployment_ref,
                cluster_cidr=cluster_cidr,
                host_prefix=host_prefix,
                service_network=service_network,
                control_plane_agents=control_plane_agents,
                **kwargs,
            )
        }

        self.crd_api.patch_namespaced_custom_object(
            group=self._api_group,
            version=self._api_version,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info("patching agentclusterinstall %s: %s", self.ref,
                    pformat(body))

    @staticmethod
    def _get_spec_dict(
        cluster_deployment_ref: ObjectReference,
        cluster_cidr: str,
        host_prefix: int,
        service_network: str,
        control_plane_agents: int,
        **kwargs,
    ) -> dict:
        spec = {
            "clusterDeploymentRef":
            cluster_deployment_ref.as_dict(),
            "imageSetRef":
            kwargs.pop("image_set_ref", ClusterImageSetReference()).as_dict(),
            "networking": {
                "clusterNetwork": [{
                    "cidr": cluster_cidr,
                    "hostPrefix": host_prefix,
                }],
                "serviceNetwork": [service_network],
            },
            "provisionRequirements": {
                "controlPlaneAgents": control_plane_agents,
                "workerAgents": kwargs.pop("worker_agents", 0),
            }
        }

        if "api_vip" in kwargs:
            spec["apiVIP"] = kwargs.pop("api_vip")

        if "ingress_vip" in kwargs:
            spec["ingressVIP"] = kwargs.pop("ingress_vip")

        if "ssh_pub_key" in kwargs:
            spec["sshPublicKey"] = kwargs.pop("ssh_pub_key")

        if "machine_cidr" in kwargs:
            spec["networking"]["machineNetwork"] = [{
                "cidr":
                kwargs.pop("machine_cidr")
            }]

        spec.update(kwargs)
        return spec

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=self._api_group,
            version=self._api_version,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=self._api_group,
            version=self._api_version,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        logger.info("deleted agentclusterinstall %s", self.ref)

    def status(
            self,
            timeout: Union[int] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT) -> dict:
        def _attempt_to_get_status() -> dict:
            return self.get()["status"]

        return waiting.wait(
            _attempt_to_get_status,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f"cluster {self.ref} status",
            expected_exceptions=KeyError,
        )

    def wait_to_be_ready(
        self,
        ready: bool,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT,
    ) -> None:
        return self.wait_for_condition(
            cond_type=self._requirements_met_condition_name,
            required_status=str(ready),
            timeout=timeout,
        )

    def wait_to_be_installing(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT,
    ) -> None:
        return self.wait_for_condition(
            cond_type=self._requirements_met_condition_name,
            required_status="True",
            required_reason="ClusterAlreadyInstalling",
            timeout=timeout,
        )

    def wait_to_be_installed(
        self,
        timeout: Union[int,
                       float] = DEFAULT_WAIT_FOR_INSTALLATION_COMPLETE_TIMEOUT,
    ) -> None:
        return self.wait_for_condition(
            cond_type=self._completed_condition_name,
            required_status="True",
            required_reason="InstallationCompleted",
            timeout=timeout,
        )

    def wait_for_condition(
        self,
        cond_type: str,
        required_status: str,
        required_reason: Optional[str] = None,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT,
    ) -> None:

        logger.info(
            "waiting for agentclusterinstall %s condition %s to be in status "
            "%s", self.ref, cond_type, required_status)

        def _has_required_condition() -> Optional[bool]:
            status, reason = self.condition(cond_type=cond_type, timeout=0.5)
            logger.info(
                f"waiting for condition <{cond_type}> to be in status <{required_status}>. actual status is: {status} {reason}"
            )
            if status == required_status:
                if required_reason:
                    return required_reason == reason
                return True

        waiting.wait(
            _has_required_condition,
            timeout_seconds=timeout,
            waiting_for=f"agentclusterinstall {self.ref} condition "
            f"{cond_type} to be {required_status}",
            sleep_seconds=10,
            expected_exceptions=waiting.exceptions.TimeoutExpired,
        )

    def condition(
        self,
        cond_type,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATE_TIMEOUT,
    ) -> Tuple[Optional[str], Optional[str]]:
        for condition in self.status(timeout).get("conditions", []):
            if cond_type == condition.get("type"):
                return condition.get("status"), condition.get("reason")

        return None, None

    def download_kubeconfig(self, kubeconfig_path):
        def _get_kubeconfig_secret() -> dict:
            return self.get(
            )["spec"]["clusterMetadata"]["adminKubeconfigSecretRef"]

        secret_ref = waiting.wait(
            _get_kubeconfig_secret,
            sleep_seconds=1,
            timeout_seconds=DEFAULT_WAIT_FOR_KUBECONFIG_TIMEOUT,
            expected_exceptions=KeyError,
            waiting_for=f"kubeconfig secret creation for cluster {self.ref}",
        )

        kubeconfig_data = (Secret(
            kube_api_client=self.crd_api.api_client,
            namespace=self._reference.namespace,
            **secret_ref,
        ).get().data["kubeconfig"])

        with open(kubeconfig_path, "wt") as kubeconfig_file:
            kubeconfig_file.write(b64decode(kubeconfig_data).decode())
class NMStateConfig(BaseCustomResource):
    """Configure nmstate (static IP) related settings for agents."""

    _plural = "nmstateconfigs"

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = consts.DEFAULT_NAMESPACE,
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    def create_from_yaml(self, yaml_data: dict) -> None:
        self.crd_api.create_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            body=yaml_data,
            namespace=self.ref.namespace,
        )

        logger.info("created nmstate config %s: %s", self.ref,
                    pformat(yaml_data))

    def create(
        self,
        config: dict,
        interfaces: list,
        label: Optional[str] = None,
        **kwargs,
    ) -> None:
        body = {
            "apiVersion": f"{CRD_API_GROUP}/{CRD_API_VERSION}",
            "kind": "NMStateConfig",
            "metadata": {
                "labels": {
                    f"{CRD_API_GROUP}/selector-nmstate-config-name": label,
                },
                **self.ref.as_dict(),
            },
            "spec": {
                "config": config,
                "interfaces": interfaces,
            },
        }
        body["spec"].update(kwargs)
        self.crd_api.create_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            body=body,
            namespace=self.ref.namespace,
        )

        logger.info("created nmstate config %s: %s", self.ref, pformat(body))

    def patch(
        self,
        config: dict,
        interfaces: list,
        **kwargs,
    ) -> None:
        body = {"spec": kwargs}

        spec = body["spec"]
        if config:
            spec["config"] = config

        if interfaces:
            spec["interfaces"] = interfaces

        self.crd_api.patch_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info("patching nmstate config %s: %s", self.ref, pformat(body))

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        logger.info("deleted nmstate config %s", self.ref)

    def status(
        self,
        timeout: Union[int,
                       float] = DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT) -> dict:
        """
        Status is a section in the CRD that is created after registration to
        assisted service and it defines the observed state of NMStateConfig.
        Since the status key is created only after resource is processed by the
        controller in the service, it might take a few seconds before appears.
        """
        def _attempt_to_get_status() -> dict:
            return self.get()["status"]

        return waiting.wait(
            _attempt_to_get_status,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f"nmstate config {self.ref} status",
            expected_exceptions=KeyError,
        )
class Agent(BaseCustomResource):
    """
    A CRD that represents host's agent in assisted-service.
    When host is registered to the cluster the service will create an Agent
    resource and assign it to the relevant cluster.
    In oder to start the installation, all assigned agents must be approved.
    """

    _plural = "agents"

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = consts.DEFAULT_NAMESPACE,
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    @classmethod
    def list(
        cls,
        crd_api: CustomObjectsApi,
        cluster_deployment: "ClusterDeployment",
    ) -> List["Agent"]:
        resources = crd_api.list_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=cls._plural,
            namespace=cluster_deployment.ref.namespace,
        )
        assigned_agents = []
        for item in resources.get("items", []):
            assigned_cluster_ref = ObjectReference(
                name=item["spec"]["clusterDeploymentName"]["name"],
                namespace=item["spec"]["clusterDeploymentName"]["namespace"],
            )
            if assigned_cluster_ref == cluster_deployment.ref:
                assigned_agents.append(
                    cls(
                        kube_api_client=cluster_deployment.crd_api.api_client,
                        name=item["metadata"]["name"],
                        namespace=item["metadata"]["namespace"],
                    ))

        return assigned_agents

    def create(self):
        raise RuntimeError(
            "agent resource must be created by the assisted-installer operator"
        )

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def patch(self, **kwargs) -> None:
        body = {"spec": kwargs}

        self.crd_api.patch_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info("patching agent %s: %s", self.ref, pformat(body))

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        logger.info("deleted agent %s", self.ref)

    def status(
        self,
        timeout: Union[int,
                       float] = DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT) -> dict:
        def _attempt_to_get_status() -> dict:
            return self.get()["status"]

        return waiting.wait(
            _attempt_to_get_status,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f"agent {self.ref} status",
            expected_exceptions=KeyError,
        )

    def approve(self) -> None:
        self.patch(approved=True)
        logger.info("approved agent %s", self.ref)
class InfraEnv(BaseCustomResource):
    """
    InfraEnv is used to generate cluster iso.
    Image is automatically generated on CRD deployment, after InfraEnv is
    reconciled. Image download url will be exposed in the status.
    """

    _plural = "infraenvs"

    def __init__(
        self,
        kube_api_client: ApiClient,
        name: str,
        namespace: str = env_variables["namespace"],
    ):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    def create_from_yaml(self, yaml_data: dict) -> None:
        self.crd_api.create_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            body=yaml_data,
            namespace=self.ref.namespace,
        )

        logger.info("created infraEnv %s: %s", self.ref, pformat(yaml_data))

    def create(
        self,
        cluster_deployment: ClusterDeployment,
        secret: Secret,
        proxy: Optional[Proxy] = None,
        label_selector: Optional[Dict[str, str]] = None,
        ignition_config_override: Optional[str] = None,
        nmstate_label: Optional[str] = None,
        ssh_pub_key: Optional[str] = None,
        **kwargs,
    ) -> None:
        body = {
            "apiVersion": f"{CRD_API_GROUP}/{CRD_API_VERSION}",
            "kind": "InfraEnv",
            "metadata": self.ref.as_dict(),
            "spec": {
                "clusterRef": cluster_deployment.ref.as_dict(),
                "pullSecretRef": secret.ref.as_dict(),
                "nmStateConfigLabelSelector": {
                    "matchLabels": {
                        f"{CRD_API_GROUP}/selector-nmstate-config-name":
                        nmstate_label or ""
                    }
                },
                "agentLabelSelector": {
                    "matchLabels": label_selector or {}
                },
                "ignitionConfigOverride": ignition_config_override or "",
            },
        }
        spec = body["spec"]
        if proxy:
            spec["proxy"] = proxy.as_dict()
        if ssh_pub_key:
            spec["sshAuthorizedKey"] = ssh_pub_key

        spec.update(kwargs)
        self.crd_api.create_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            body=body,
            namespace=self.ref.namespace,
        )

        logger.info("created infraEnv %s: %s", self.ref, pformat(body))

    def patch(
        self,
        cluster_deployment: Optional[ClusterDeployment],
        secret: Optional[Secret],
        proxy: Optional[Proxy] = None,
        label_selector: Optional[Dict[str, str]] = None,
        ignition_config_override: Optional[str] = None,
        nmstate_label: Optional[str] = None,
        ssh_pub_key: Optional[str] = None,
        **kwargs,
    ) -> None:
        body = {"spec": kwargs}

        spec = body["spec"]
        if cluster_deployment:
            spec["clusterRef"] = cluster_deployment.ref.as_dict()

        if secret:
            spec["pullSecretRef"] = secret.ref.as_dict()

        if proxy:
            spec["proxy"] = proxy.as_dict()

        if label_selector:
            spec["agentLabelSelector"] = {"matchLabels": label_selector}

        if nmstate_label:
            spec["nmStateConfigLabelSelector"] = {
                "matchLabels": {
                    f"{CRD_API_GROUP}/selector-nmstate-config-name":
                    nmstate_label,
                }
            }

        if ignition_config_override:
            spec["ignitionConfigOverride"] = ignition_config_override

        if ssh_pub_key:
            spec["sshAuthorizedKey"] = ssh_pub_key

        self.crd_api.patch_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body,
        )

        logger.info("patching infraEnv %s: %s", self.ref, pformat(body))

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=CRD_API_GROUP,
            version=CRD_API_VERSION,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
        )

        logger.info("deleted infraEnv %s", self.ref)

    def status(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_CRD_STATUS_TIMEOUT,
    ) -> dict:
        """
        Status is a section in the CRD that is created after registration to
        assisted service and it defines the observed state of InfraEnv.
        Since the status key is created only after resource is processed by the
        controller in the service, it might take a few seconds before appears.
        """
        def _attempt_to_get_status() -> dict:
            return self.get()["status"]

        return waiting.wait(
            _attempt_to_get_status,
            sleep_seconds=0.5,
            timeout_seconds=timeout,
            waiting_for=f"infraEnv {self.ref} status",
            expected_exceptions=KeyError,
        )

    def get_iso_download_url(
        self,
        timeout: Union[int, float] = DEFAULT_WAIT_FOR_ISO_URL_TIMEOUT,
    ):
        def _attempt_to_get_image_url() -> str:
            return self.get()["status"]["isoDownloadURL"]

        return waiting.wait(
            _attempt_to_get_image_url,
            sleep_seconds=3,
            timeout_seconds=timeout,
            waiting_for="image to be created",
            expected_exceptions=KeyError,
        )

    def get_cluster_id(self):
        iso_download_url = self.get_iso_download_url()
        return ISO_URL_PATTERN.match(iso_download_url).group("cluster_id")

    @classmethod
    def deploy_default_infraenv(
        cls,
        kube_api_client: ApiClient,
        name: str,
        ignore_conflict: bool = True,
        cluster_deployment: Optional[ClusterDeployment] = None,
        secret: Optional[Secret] = None,
        proxy: Optional[Proxy] = None,
        label_selector: Optional[Dict[str, str]] = None,
        ignition_config_override: Optional[str] = None,
        **kwargs,
    ) -> "InfraEnv":

        infra_env = InfraEnv(kube_api_client, name)
        try:
            if "filepath" in kwargs:
                infra_env._create_infraenv_from_yaml_file(
                    filepath=kwargs["filepath"], )
            else:
                infra_env._create_infraenv_from_attrs(
                    kube_api_client=kube_api_client,
                    name=name,
                    ignore_conflict=ignore_conflict,
                    cluster_deployment=cluster_deployment,
                    secret=secret,
                    proxy=proxy,
                    label_selector=label_selector,
                    ignition_config_override=ignition_config_override,
                    **kwargs,
                )
        except ApiException as e:
            if not (e.reason == "Conflict" and ignore_conflict):
                raise

        # wait until install-env will have status (i.e until resource will be
        # processed in assisted-service).
        infra_env.status()

        return infra_env

    def _create_infraenv_from_yaml_file(
        self,
        filepath: str,
    ) -> None:
        with open(filepath) as fp:
            yaml_data = yaml.safe_load(fp)

        self.create_from_yaml(yaml_data)

    def _create_infraenv_from_attrs(
        self,
        kube_api_client: ApiClient,
        cluster_deployment: ClusterDeployment,
        secret: Optional[Secret] = None,
        proxy: Optional[Proxy] = None,
        label_selector: Optional[Dict[str, str]] = None,
        ignition_config_override: Optional[str] = None,
        **kwargs,
    ) -> None:
        if not secret:
            secret = deploy_default_secret(
                kube_api_client=kube_api_client,
                name=cluster_deployment.ref.name,
            )
        self.create(
            cluster_deployment=cluster_deployment,
            secret=secret,
            proxy=proxy,
            label_selector=label_selector,
            ignition_config_override=ignition_config_override,
            **kwargs,
        )
Exemple #23
0
class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware]
                                  ):
    """Kubernetes Traefik Middleware Reconciler"""
    def __init__(self, controller: "KubernetesController") -> None:
        super().__init__(controller)
        self.api_ex = ApiextensionsV1Api(controller.client)
        self.api = CustomObjectsApi(controller.client)

    def _crd_exists(self) -> bool:
        """Check if the traefik middleware exists"""
        return bool(
            len(
                self.api_ex.list_custom_resource_definition(
                    field_selector=f"metadata.name={CRD_NAME}").items))

    def reconcile(self, current: TraefikMiddleware,
                  reference: TraefikMiddleware):
        super().reconcile(current, reference)
        if current.spec.forwardAuth.address != reference.spec.forwardAuth.address:
            raise NeedsUpdate()

    def get_reference_object(self) -> TraefikMiddleware:
        """Get deployment object for outpost"""
        if not ProxyProvider.objects.filter(
                outpost__in=[self.controller.outpost],
                forward_auth_mode=True,
        ).exists():
            self.logger.debug("No providers with forward auth enabled.")
            raise Disabled()
        if not self._crd_exists():
            self.logger.debug("CRD doesn't exist")
            raise Disabled()
        return TraefikMiddleware(
            apiVersion=f"{CRD_GROUP}/{CRD_VERSION}",
            kind="Middleware",
            metadata=TraefikMiddlewareMetadata(
                name=self.name,
                namespace=self.namespace,
                labels=self.get_object_meta().labels,
            ),
            spec=
            TraefikMiddlewareSpec(forwardAuth=TraefikMiddlewareSpecForwardAuth(
                address=
                f"http://{self.name}.{self.namespace}:4180/akprox/auth?traefik",
                authResponseHeaders=[
                    "Set-Cookie",
                    "X-Auth-Username",
                    "X-Forwarded-Email",
                    "X-Forwarded-Preferred-Username",
                    "X-Forwarded-User",
                ],
                trustForwardHeader=True,
            )),
        )

    def create(self, reference: TraefikMiddleware):
        return self.api.create_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            plural=CRD_PLURAL,
            namespace=self.namespace,
            body=asdict(reference),
            field_manager=FIELD_MANAGER,
        )

    def delete(self, reference: TraefikMiddleware):
        return self.api.delete_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            namespace=self.namespace,
            plural=CRD_PLURAL,
            name=self.name,
        )

    def retrieve(self) -> TraefikMiddleware:
        return from_dict(
            TraefikMiddleware,
            self.api.get_namespaced_custom_object(
                group=CRD_GROUP,
                version=CRD_VERSION,
                namespace=self.namespace,
                plural=CRD_PLURAL,
                name=self.name,
            ),
        )

    def update(self, current: TraefikMiddleware, reference: TraefikMiddleware):
        return self.api.patch_namespaced_custom_object(
            group=CRD_GROUP,
            version=CRD_VERSION,
            namespace=self.namespace,
            plural=CRD_PLURAL,
            name=self.name,
            body=asdict(reference),
            field_manager=FIELD_MANAGER,
        )
Exemple #24
0
class Agent(BaseCustomResource):
    """
    A CRD that represents host's agent in assisted-service.
    When host is registered to the cluster the service will create an Agent
    resource and assign it to the relevant cluster.
    In oder to start the installation, all assigned agents must be approved.
    """
    _api_group = 'adi.io.my.domain'
    _version = 'v1alpha1'
    _plural = 'agents'

    def __init__(self,
                 kube_api_client: ApiClient,
                 name: str,
                 namespace: str = env_variables['namespace']):
        super().__init__(name, namespace)
        self.crd_api = CustomObjectsApi(kube_api_client)

    @classmethod
    def list(
        cls,
        crd_api: CustomObjectsApi,
        cluster_deployment: 'ClusterDeployment',
    ) -> List['Agent']:
        resources = crd_api.list_namespaced_custom_object(
            group=cls._api_group,
            version=cls._version,
            plural=cls._plural,
            namespace=cluster_deployment.ref.namespace,
        )
        assigned_agents = []
        for item in resources.get('items', []):
            assigned_cluster_ref = ObjectReference(
                name=item['spec']['clusterDeploymentName']['name'],
                namespace=item['spec']['clusterDeploymentName']['namespace'])
            if assigned_cluster_ref == cluster_deployment.ref:
                assigned_agents.append(
                    cls(kube_api_client=cluster_deployment.crd_api.api_client,
                        name=item['metadata']['name'],
                        namespace=item['metadata']['namespace']))

        return assigned_agents

    def create(self):
        raise RuntimeError(
            'agent resource must be created by the assisted-installer operator'
        )

    def get(self) -> dict:
        return self.crd_api.get_namespaced_custom_object(
            group=self._api_group,
            version=self._version,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace)

    def patch(self, **kwargs) -> None:
        body = {'spec': kwargs}

        self.crd_api.patch_namespaced_custom_object(
            group=self._api_group,
            version=self._version,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace,
            body=body)

        logger.info('patching agent %s: %s', self.ref, pformat(body))

    def delete(self) -> None:
        self.crd_api.delete_namespaced_custom_object(
            group=self._api_group,
            version=self._version,
            plural=self._plural,
            name=self.ref.name,
            namespace=self.ref.namespace)

        logger.info('deleted agent %s', self.ref)

    def status(self) -> dict:
        return self.get()['status']

    def approve(self) -> None:
        self.patch(approved=True)

        logger.info('approved agent %s', self.ref)