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)
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
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']}'")
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, )
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", )
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())
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')
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)
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, )
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, )
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)