Spaces:
Runtime error
Runtime error
from pathlib import Path | |
from typing import Optional, Any, Dict | |
from phi.aws.api_client import AwsApiClient | |
from phi.k8s.enums.api_version import ApiVersion | |
from phi.aws.resource.base import AwsResource | |
from phi.aws.resource.iam.role import IamRole | |
from phi.aws.resource.eks.cluster import EksCluster | |
from phi.k8s.resource.kubeconfig import ( | |
Kubeconfig, | |
KubeconfigCluster, | |
KubeconfigClusterConfig, | |
KubeconfigContext, | |
KubeconfigContextSpec, | |
KubeconfigUser, | |
) | |
from phi.cli.console import print_info | |
from phi.utils.log import logger | |
class EksKubeconfig(AwsResource): | |
resource_type: Optional[str] = "Kubeconfig" | |
service_name: str = "na" | |
# Optional: kubeconfig name, used for filtering during phi ws up/down | |
name: str = "kubeconfig" | |
# Required: EksCluster to generate the kubeconfig for | |
eks_cluster: EksCluster | |
# Required: Path to kubeconfig file | |
kubeconfig_path: Path = Path.home().joinpath(".kube").joinpath("config").resolve() | |
# Optional: cluster_name to use in kubeconfig, defaults to eks_cluster.name | |
kubeconfig_cluster_name: Optional[str] = None | |
# Optional: cluster_user to use in kubeconfig, defaults to eks_cluster.name | |
kubeconfig_cluster_user: Optional[str] = None | |
# Optional: cluster_context to use in kubeconfig, defaults to eks_cluster.name | |
kubeconfig_cluster_context: Optional[str] = None | |
# Optional: role to assume when signing the token | |
kubeconfig_role: Optional[IamRole] = None | |
# Optional: role arn to assume when signing the token | |
kubeconfig_role_arn: Optional[str] = None | |
# Dont delete this EksKubeconfig from the kubeconfig file | |
skip_delete: bool = True | |
# Mark use_cache as False so the kubeconfig is re-created | |
# every time phi ws up/down is run | |
use_cache: bool = False | |
def _create(self, aws_client: AwsApiClient) -> bool: | |
"""Creates the EksKubeconfig | |
Args: | |
aws_client: The AwsApiClient for the current cluster | |
""" | |
print_info(f"Creating {self.get_resource_type()}: {self.get_resource_name()}") | |
try: | |
return self.write_kubeconfig(aws_client=aws_client) | |
except Exception as e: | |
logger.error(f"{self.get_resource_type()} could not be created.") | |
logger.error(e) | |
return False | |
def _read(self, aws_client: AwsApiClient) -> Optional[Any]: | |
"""Reads the EksKubeconfig | |
Args: | |
aws_client: The AwsApiClient for the current cluster | |
""" | |
logger.debug(f"Reading {self.get_resource_type()}: {self.get_resource_name()}") | |
try: | |
kubeconfig_path = self.get_kubeconfig_path() | |
if kubeconfig_path is not None: | |
return Kubeconfig.read_from_file(kubeconfig_path) | |
except Exception as e: | |
logger.error(f"Error reading {self.get_resource_type()}.") | |
logger.error(e) | |
return self.active_resource | |
def _update(self, aws_client: AwsApiClient) -> bool: | |
"""Updates the EksKubeconfig | |
Args: | |
aws_client: The AwsApiClient for the current cluster | |
""" | |
print_info(f"Updating {self.get_resource_type()}: {self.get_resource_name()}") | |
try: | |
return self.write_kubeconfig(aws_client=aws_client) | |
except Exception as e: | |
logger.error(f"{self.get_resource_type()} could not be updated.") | |
logger.error(e) | |
return False | |
def _delete(self, aws_client: AwsApiClient) -> bool: | |
"""Deletes the EksKubeconfig | |
Args: | |
aws_client: The AwsApiClient for the current cluster | |
""" | |
print_info(f"Deleting {self.get_resource_type()}: {self.get_resource_name()}") | |
try: | |
return self.clean_kubeconfig(aws_client=aws_client) | |
except Exception as e: | |
logger.error(f"{self.get_resource_type()} could not be deleted.") | |
logger.error(e) | |
return False | |
def get_kubeconfig_path(self) -> Optional[Path]: | |
return self.kubeconfig_path or self.eks_cluster.kubeconfig_path | |
def get_kubeconfig_cluster_name(self) -> str: | |
return self.kubeconfig_cluster_name or self.eks_cluster.get_kubeconfig_cluster_name() | |
def get_kubeconfig_user_name(self) -> str: | |
return self.kubeconfig_cluster_user or self.eks_cluster.get_kubeconfig_user_name() | |
def get_kubeconfig_context_name(self) -> str: | |
return self.kubeconfig_cluster_context or self.eks_cluster.get_kubeconfig_context_name() | |
def get_kubeconfig_role(self) -> Optional[IamRole]: | |
return self.kubeconfig_role or self.eks_cluster.kubeconfig_role | |
def get_kubeconfig_role_arn(self) -> Optional[str]: | |
return self.kubeconfig_role_arn or self.eks_cluster.kubeconfig_role_arn | |
def write_kubeconfig(self, aws_client: AwsApiClient) -> bool: | |
# Step 1: Get the EksCluster to generate the kubeconfig for | |
eks_cluster = self.eks_cluster._read(aws_client=aws_client) # type: ignore | |
if eks_cluster is None: | |
logger.warning(f"EKSCluster not available: {self.eks_cluster.name}") | |
return False | |
# Step 2: Get EksCluster cert, endpoint & arn | |
try: | |
cluster_cert = eks_cluster.get("cluster", {}).get("certificateAuthority", {}).get("data", None) | |
logger.debug(f"cluster_cert: {cluster_cert}") | |
cluster_endpoint = eks_cluster.get("cluster", {}).get("endpoint", None) | |
logger.debug(f"cluster_endpoint: {cluster_endpoint}") | |
cluster_arn = eks_cluster.get("cluster", {}).get("arn", None) | |
logger.debug(f"cluster_arn: {cluster_arn}") | |
except Exception as e: | |
logger.error("Cannot read EKSCluster") | |
logger.error(e) | |
return False | |
# Step 3: Build Kubeconfig components | |
# 3.1 Build KubeconfigCluster config | |
cluster_name = self.get_kubeconfig_cluster_name() | |
new_cluster = KubeconfigCluster( | |
name=cluster_name, | |
cluster=KubeconfigClusterConfig( | |
server=str(cluster_endpoint), | |
certificate_authority_data=str(cluster_cert), | |
), | |
) | |
# 3.2 Build KubeconfigUser config | |
new_user_exec_args = ["eks", "get-token", "--cluster-name", cluster_name] | |
if aws_client.aws_region is not None: | |
new_user_exec_args.extend(["--region", aws_client.aws_region]) | |
# Assume the role if the role_arn is provided | |
role = self.get_kubeconfig_role() | |
role_arn = self.get_kubeconfig_role_arn() | |
if role_arn is not None: | |
new_user_exec_args.extend(["--role-arn", role_arn]) | |
# Otherwise if role is provided, use that to get the role arn | |
elif role is not None: | |
_arn = role.get_arn(aws_client=aws_client) | |
if _arn is not None: | |
new_user_exec_args.extend(["--role-arn", _arn]) | |
new_user_exec: Dict[str, Any] = { | |
"apiVersion": ApiVersion.CLIENT_AUTHENTICATION_V1BETA1.value, | |
"command": "aws", | |
"args": new_user_exec_args, | |
} | |
if aws_client.aws_profile is not None: | |
new_user_exec["env"] = [{"name": "AWS_PROFILE", "value": aws_client.aws_profile}] | |
new_user = KubeconfigUser( | |
name=self.get_kubeconfig_user_name(), | |
user={"exec": new_user_exec}, | |
) | |
# 3.3 Build KubeconfigContext config | |
new_context = KubeconfigContext( | |
name=self.get_kubeconfig_context_name(), | |
context=KubeconfigContextSpec( | |
cluster=new_cluster.name, | |
user=new_user.name, | |
), | |
) | |
current_context = new_context.name | |
# Step 4: Get existing Kubeconfig | |
kubeconfig_path = self.get_kubeconfig_path() | |
if kubeconfig_path is None: | |
logger.error("kubeconfig_path is None") | |
return False | |
kubeconfig: Optional[Any] = Kubeconfig.read_from_file(kubeconfig_path) | |
# Step 5: Parse through the existing config to determine if | |
# an update is required. By the end of this logic | |
# if write_kubeconfig = False then no changes to kubeconfig are needed | |
# if write_kubeconfig = True then we should write the kubeconfig file | |
write_kubeconfig = False | |
# Kubeconfig exists and is valid | |
if kubeconfig is not None and isinstance(kubeconfig, Kubeconfig): | |
# Update Kubeconfig.clusters: | |
# If a cluster with the same name exists in Kubeconfig.clusters | |
# - check if server and cert values match, if not, remove the existing cluster | |
# and add the new cluster config. Mark cluster_config_exists = True | |
# If a cluster with the same name does not exist in Kubeconfig.clusters | |
# - add the new cluster config | |
cluster_config_exists = False | |
for idx, _cluster in enumerate(kubeconfig.clusters, start=0): | |
if _cluster.name == new_cluster.name: | |
cluster_config_exists = True | |
if ( | |
_cluster.cluster.server != new_cluster.cluster.server | |
or _cluster.cluster.certificate_authority_data != new_cluster.cluster.certificate_authority_data | |
): | |
logger.debug("Kubeconfig.cluster mismatch, updating cluster config") | |
kubeconfig.clusters.pop(idx) | |
# logger.debug( | |
# f"removed_cluster_config: {removed_cluster_config}" | |
# ) | |
kubeconfig.clusters.append(new_cluster) | |
write_kubeconfig = True | |
if not cluster_config_exists: | |
logger.debug("Adding Kubeconfig.cluster") | |
kubeconfig.clusters.append(new_cluster) | |
write_kubeconfig = True | |
# Update Kubeconfig.users: | |
# If a user with the same name exists in Kubeconfig.users - | |
# check if user spec matches, if not, remove the existing user | |
# and add the new user config. Mark user_config_exists = True | |
# If a user with the same name does not exist in Kubeconfig.users - | |
# add the new user config | |
user_config_exists = False | |
for idx, _user in enumerate(kubeconfig.users, start=0): | |
if _user.name == new_user.name: | |
user_config_exists = True | |
if _user.user != new_user.user: | |
logger.debug("Kubeconfig.user mismatch, updating user config") | |
kubeconfig.users.pop(idx) | |
# logger.debug(f"removed_user_config: {removed_user_config}") | |
kubeconfig.users.append(new_user) | |
write_kubeconfig = True | |
if not user_config_exists: | |
logger.debug("Adding Kubeconfig.user") | |
kubeconfig.users.append(new_user) | |
write_kubeconfig = True | |
# Update Kubeconfig.contexts: | |
# If a context with the same name exists in Kubeconfig.contexts - | |
# check if context spec matches, if not, remove the existing context | |
# and add the new context. Mark context_config_exists = True | |
# If a context with the same name does not exist in Kubeconfig.contexts - | |
# add the new context config | |
context_config_exists = False | |
for idx, _context in enumerate(kubeconfig.contexts, start=0): | |
if _context.name == new_context.name: | |
context_config_exists = True | |
if _context.context != new_context.context: | |
logger.debug("Kubeconfig.context mismatch, updating context config") | |
kubeconfig.contexts.pop(idx) | |
# logger.debug( | |
# f"removed_context_config: {removed_context_config}" | |
# ) | |
kubeconfig.contexts.append(new_context) | |
write_kubeconfig = True | |
if not context_config_exists: | |
logger.debug("Adding Kubeconfig.context") | |
kubeconfig.contexts.append(new_context) | |
write_kubeconfig = True | |
if kubeconfig.current_context is None or kubeconfig.current_context != current_context: | |
logger.debug("Updating Kubeconfig.current_context") | |
kubeconfig.current_context = current_context | |
write_kubeconfig = True | |
else: | |
# Kubeconfig does not exist or is not valid | |
# Create a new Kubeconfig | |
logger.info("Creating new Kubeconfig") | |
kubeconfig = Kubeconfig( | |
clusters=[new_cluster], | |
users=[new_user], | |
contexts=[new_context], | |
current_context=current_context, | |
) | |
write_kubeconfig = True | |
# if kubeconfig: | |
# logger.debug("Kubeconfig:\n{}".format(kubeconfig.json(exclude_none=True, by_alias=True, indent=4))) | |
# Step 5: Write Kubeconfig if an update is made | |
if write_kubeconfig: | |
return kubeconfig.write_to_file(kubeconfig_path) | |
else: | |
logger.info("Kubeconfig up-to-date") | |
return True | |
def clean_kubeconfig(self, aws_client: AwsApiClient) -> bool: | |
logger.debug(f"TO_DO: Cleaning kubeconfig at {str(self.kubeconfig_path)}") | |
return True | |