import json from pathlib import Path from typing import Optional, Any, Dict, List from phi.aws.api_client import AwsApiClient from phi.aws.resource.base import AwsResource from phi.cli.console import print_info from phi.utils.log import logger class SecretsManager(AwsResource): """ Reference: - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/secretsmanager.html """ resource_type: Optional[str] = "Secret" service_name: str = "secretsmanager" # The name of the secret. name: str client_request_token: Optional[str] = None # The description of the secret. description: Optional[str] = None kms_key_id: Optional[str] = None # The binary data to encrypt and store in the new version of the secret. # We recommend that you store your binary data in a file and then pass the contents of the file as a parameter. secret_binary: Optional[bytes] = None # The text data to encrypt and store in this new version of the secret. # We recommend you use a JSON structure of key/value pairs for your secret value. # Either SecretString or SecretBinary must have a value, but not both. secret_string: Optional[str] = None # A list of tags to attach to the secret. tags: Optional[List[Dict[str, str]]] = None # A list of Regions and KMS keys to replicate secrets. add_replica_regions: Optional[List[Dict[str, str]]] = None # Specifies whether to overwrite a secret with the same name in the destination Region. force_overwrite_replica_secret: Optional[str] = None # Read secret key/value pairs from yaml files secret_files: Optional[List[Path]] = None # Read secret key/value pairs from yaml files in a directory secrets_dir: Optional[Path] = None # Force delete the secret without recovery force_delete: Optional[bool] = True # Provided by api on create secret_arn: Optional[str] = None secret_name: Optional[str] = None secret_value: Optional[dict] = None cached_secret: Optional[Dict[str, Any]] = None def read_secrets_from_files(self) -> Dict[str, Any]: """Reads secrets from files""" from phi.utils.yaml_io import read_yaml_file secret_dict: Dict[str, Any] = {} if self.secret_files: for f in self.secret_files: _s = read_yaml_file(f) if _s is not None: secret_dict.update(_s) if self.secrets_dir: for f in self.secrets_dir.glob("*.yaml"): _s = read_yaml_file(f) if _s is not None: secret_dict.update(_s) for f in self.secrets_dir.glob("*.yml"): _s = read_yaml_file(f) if _s is not None: secret_dict.update(_s) return secret_dict def _create(self, aws_client: AwsApiClient) -> bool: """Creates the SecretsManager Args: aws_client: The AwsApiClient for the current secret """ print_info(f"Creating {self.get_resource_type()}: {self.get_resource_name()}") # Step 1: Read secrets from files secret_dict: Dict[str, Any] = self.read_secrets_from_files() # Step 2: Add secret_string if provided if self.secret_string is not None: secret_dict.update(json.loads(self.secret_string)) # Step 3: Build secret_string secret_string: Optional[str] = json.dumps(secret_dict) if len(secret_dict) > 0 else None # Step 4: Build SecretsManager configuration # create a dict of args which are not null, otherwise aws type validation fails not_null_args: Dict[str, Any] = {} if self.client_request_token: not_null_args["ClientRequestToken"] = self.client_request_token if self.description: not_null_args["Description"] = self.description if self.kms_key_id: not_null_args["KmsKeyId"] = self.kms_key_id if self.secret_binary: not_null_args["SecretBinary"] = self.secret_binary if secret_string: not_null_args["SecretString"] = secret_string if self.tags: not_null_args["Tags"] = self.tags if self.add_replica_regions: not_null_args["AddReplicaRegions"] = self.add_replica_regions if self.force_overwrite_replica_secret: not_null_args["ForceOverwriteReplicaSecret"] = self.force_overwrite_replica_secret # Step 3: Create SecretsManager service_client = self.get_service_client(aws_client) try: created_resource = service_client.create_secret( Name=self.name, **not_null_args, ) logger.debug(f"SecretsManager: {created_resource}") # Validate SecretsManager creation self.secret_arn = created_resource.get("ARN", None) self.secret_name = created_resource.get("Name", None) logger.debug(f"secret_arn: {self.secret_arn}") logger.debug(f"secret_name: {self.secret_name}") if self.secret_arn is not None: self.cached_secret = secret_dict self.active_resource = created_resource return True 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]: """Returns the SecretsManager Args: aws_client: The AwsApiClient for the current secret """ logger.debug(f"Reading {self.get_resource_type()}: {self.get_resource_name()}") from botocore.exceptions import ClientError service_client = self.get_service_client(aws_client) try: describe_response = service_client.describe_secret(SecretId=self.name) logger.debug(f"SecretsManager: {describe_response}") self.secret_arn = describe_response.get("ARN", None) self.secret_name = describe_response.get("Name", None) describe_response.get("DeletedDate", None) logger.debug(f"secret_arn: {self.secret_arn}") logger.debug(f"secret_name: {self.secret_name}") # logger.debug(f"secret_deleted_date: {secret_deleted_date}") if self.secret_arn is not None: # print_info(f"SecretsManager available: {self.name}") self.active_resource = describe_response except ClientError as ce: logger.debug(f"ClientError: {ce}") except Exception as e: logger.error(f"Error reading {self.get_resource_type()}.") logger.error(e) return self.active_resource def _delete(self, aws_client: AwsApiClient) -> bool: """Deletes the SecretsManager Args: aws_client: The AwsApiClient for the current secret """ print_info(f"Deleting {self.get_resource_type()}: {self.get_resource_name()}") service_client = self.get_service_client(aws_client) self.active_resource = None self.secret_value = None try: delete_response = service_client.delete_secret( SecretId=self.name, ForceDeleteWithoutRecovery=self.force_delete ) logger.debug(f"SecretsManager: {delete_response}") return True except Exception as e: logger.error(f"{self.get_resource_type()} could not be deleted.") logger.error("Please try again or delete resources manually.") logger.error(e) return False def _update(self, aws_client: AwsApiClient) -> bool: """Update SecretsManager""" print_info(f"Updating {self.get_resource_type()}: {self.get_resource_name()}") # Initialize final secret_dict secret_dict: Dict[str, Any] = {} # Step 1: Read secrets from AWS SecretsManager existing_secret_dict = self.get_secrets_as_dict() # logger.debug(f"existing_secret_dict: {existing_secret_dict}") if existing_secret_dict is not None: secret_dict.update(existing_secret_dict) # Step 2: Read secrets from files new_secret_dict: Dict[str, Any] = self.read_secrets_from_files() if len(new_secret_dict) > 0: secret_dict.update(new_secret_dict) # Step 3: Add secret_string is provided if self.secret_string is not None: secret_dict.update(json.loads(self.secret_string)) # Step 3: Update AWS SecretsManager service_client = self.get_service_client(aws_client) self.active_resource = None self.secret_value = None try: create_response = service_client.update_secret( SecretId=self.name, SecretString=json.dumps(secret_dict), ) logger.debug(f"SecretsManager: {create_response}") return True except Exception as e: logger.error(f"{self.get_resource_type()} could not be Updated.") logger.error(e) return False def get_secrets_as_dict(self, aws_client: Optional[AwsApiClient] = None) -> Optional[Dict[str, Any]]: """Get secret value Args: aws_client: The AwsApiClient for the current secret """ from botocore.exceptions import ClientError if self.cached_secret is not None: return self.cached_secret logger.debug(f"Getting {self.get_resource_type()}: {self.get_resource_name()}") client: AwsApiClient = aws_client or self.get_aws_client() service_client = self.get_service_client(client) try: secret_value = service_client.get_secret_value(SecretId=self.name) # logger.debug(f"SecretsManager: {secret_value}") if secret_value is None: logger.warning(f"Secret Empty: {self.name}") return None self.secret_value = secret_value self.secret_arn = secret_value.get("ARN", None) self.secret_name = secret_value.get("Name", None) secret_string = secret_value.get("SecretString", None) if secret_string is not None: self.cached_secret = json.loads(secret_string) return self.cached_secret secret_binary = secret_value.get("SecretBinary", None) if secret_binary is not None: self.cached_secret = json.loads(secret_binary.decode("utf-8")) return self.cached_secret except ClientError as ce: logger.debug(f"ClientError: {ce}") except Exception as e: logger.error(f"Error reading {self.get_resource_type()}.") logger.error(e) return None def get_secret_value(self, secret_name: str, aws_client: Optional[AwsApiClient] = None) -> Optional[Any]: secret_dict = self.get_secrets_as_dict(aws_client=aws_client) if secret_dict is not None: return secret_dict.get(secret_name, None) return None