from pathlib import Path from typing import Optional, Any, List, Dict from typing_extensions import Literal from pydantic import BaseModel from phi.aws.api_client import AwsApiClient from phi.aws.resource.base import AwsResource from phi.cli.console import print_info, print_subheading from phi.utils.log import logger class CertificateSummary(BaseModel): CertificateArn: str DomainName: Optional[str] = None class AcmCertificate(AwsResource): """ You can use Amazon Web Services Certificate Manager (ACM) to manage SSL/TLS certificates for your Amazon Web Services-based websites and applications. Reference: - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/acm.html """ resource_type: Optional[str] = "AcmCertificate" service_name: str = "acm" # website base domain name, such as example.com name: str # Fully qualified domain name (FQDN), such as www.example.com, # that you want to secure with an ACM certificate. # # Use an asterisk (*) to create a wildcard certificate that protects several sites in the same domain. # For example, *.example.com protects www.example.com, site.example.com, and images.example.com. # The first domain name you enter cannot exceed 64 octets, including periods. # Each subsequent Subject Alternative Name (SAN), however, can be up to 253 octets in length. # If None, defaults to "*.name" domain_name: Optional[str] = None # The method you want to use if you are requesting a public certificate to validate that you own or control domain. # You can validate with DNS or validate with email . # We recommend that you use DNS validation. validation_method: Literal["EMAIL", "DNS"] = "DNS" # Additional FQDNs to be included in the Subject Alternative Name extension of the ACM certificate. # For example, add the name www.example.net to a certificate for which the DomainName field is www.example.com # if users can reach your site by using either name. The maximum number of domain names that you can add to an # ACM certificate is 100. However, the initial quota is 10 domain names. If you need more than 10 names, # you must request a quota increase. subject_alternative_names: Optional[List[str]] = None # Customer chosen string that can be used to distinguish between calls to RequestCertificate . # Idempotency tokens time out after one hour. Therefore, if you call RequestCertificate multiple times with # the same idempotency token within one hour, ACM recognizes that you are requesting only one certificate # and will issue only one. If you change the idempotency token for each call, ACM recognizes that you are # requesting multiple certificates. idempotency_token: Optional[str] = None # The domain name that you want ACM to use to send you emails so that you can validate domain ownership. domain_validation_options: Optional[List[dict]] = None options: Optional[dict] = None certificate_authority_arn: Optional[str] = None tags: Optional[List[dict]] = None # If True, stores the certificate summary in the file `certificate_summary_file` store_cert_summary: bool = False # Path for the certificate_summary_file certificate_summary_file: Optional[Path] = None wait_for_create: bool = False def _create(self, aws_client: AwsApiClient) -> bool: """Requests an ACM certificate for use with other Amazon Web Services. Args: aws_client: The AwsApiClient for the current cluster """ print_info(f"Creating {self.get_resource_type()}: {self.get_resource_name()}") # Step 1: Build ACM configuration domain_name = self.domain_name if domain_name is None: domain_name = self.name print_info(f"Requesting AcmCertificate for: {domain_name}") # create a dict of args which are not null, otherwise aws type validation fails not_null_args: Dict[str, Any] = {} if self.subject_alternative_names is not None: not_null_args["SubjectAlternativeNames"] = self.subject_alternative_names print_info("SANs:") for san in self.subject_alternative_names: print_info(f" - {san}") if self.idempotency_token is not None: not_null_args["IdempotencyToken"] = self.idempotency_token if self.domain_validation_options is not None: not_null_args["DomainValidationOptions"] = self.domain_validation_options if self.options is not None: not_null_args["Options"] = self.options if self.certificate_authority_arn is not None: not_null_args["CertificateAuthorityArn"] = self.certificate_authority_arn if self.tags is not None: not_null_args["Tags"] = self.tags # Step 2: Request AcmCertificate service_client = self.get_service_client(aws_client) try: request_cert_response = service_client.request_certificate( DomainName=domain_name, ValidationMethod=self.validation_method, **not_null_args, ) logger.debug(f"AcmCertificate: {request_cert_response}") # Validate AcmCertificate creation certificate_arn = request_cert_response.get("CertificateArn", None) if certificate_arn is not None: print_subheading("---- Please Note: Certificate ARN ----") print_info(f"{certificate_arn}") print_subheading("--------\n") self.active_resource = request_cert_response return True except Exception as e: logger.error(f"{self.get_resource_type()} could not be created.") logger.error(e) return False def post_create(self, aws_client: AwsApiClient) -> bool: # Wait for AcmCertificate to be validated if self.wait_for_create: try: print_info(f"Waiting for {self.get_resource_type()} to be created.") waiter = self.get_service_client(aws_client).get_waiter("certificate_validated") certificate_arn = self.get_certificate_arn(aws_client) waiter.wait( CertificateArn=certificate_arn, WaiterConfig={ "Delay": self.waiter_delay, "MaxAttempts": self.waiter_max_attempts, }, ) except Exception as e: logger.error("Waiter failed.") logger.error(e) # Store cert summary if needed if self.store_cert_summary: if self.certificate_summary_file is None: logger.error("certificate_summary_file not provided") return False try: read_cert_summary = self._read(aws_client) if read_cert_summary is None: logger.error("certificate_summary not available") return False cert_summary = CertificateSummary(**read_cert_summary) if not self.certificate_summary_file.exists(): self.certificate_summary_file.parent.mkdir(parents=True, exist_ok=True) self.certificate_summary_file.touch(exist_ok=True) self.certificate_summary_file.write_text(cert_summary.json(indent=2)) print_info(f"Certificate Summary stored at: {str(self.certificate_summary_file)}") except Exception as e: logger.error("Could not writing Certificate Summary to file") logger.error(e) return True def _read(self, aws_client: AwsApiClient) -> Optional[Any]: """Returns the Certificate ARN Args: aws_client: The AwsApiClient for the current cluster """ 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: list_certificate_response = service_client.list_certificates() # logger.debug(f"AcmCertificate: {list_certificate_response}") current_cert = None certificate_summary_list = list_certificate_response.get("CertificateSummaryList", []) for cert_summary in certificate_summary_list: domain = cert_summary.get("DomainName", None) if domain is not None and domain == self.name: current_cert = cert_summary # logger.debug(f"current_cert: {current_cert}") # logger.debug(f"current_cert type: {type(current_cert)}") if current_cert is not None: logger.debug(f"AcmCertificate found: {self.name}") self.active_resource = current_cert 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 a certificate and its associated private key. Args: aws_client: The AwsApiClient for the current cluster """ print_info(f"Deleting {self.get_resource_type()}: {self.get_resource_name()}") service_client = self.get_service_client(aws_client) self.active_resource = None try: certificate_arn = self.get_certificate_arn(aws_client) if certificate_arn is not None: delete_cert_response = service_client.delete_certificate( CertificateArn=certificate_arn, ) logger.debug(f"delete_cert_response: {delete_cert_response}") print_info(f"AcmCertificate deleted: {self.name}") else: print_info("AcmCertificate not found") return True except Exception as e: logger.error(e) return False def get_certificate_arn(self, aws_client: AwsApiClient) -> Optional[str]: cert_summary = self._read(aws_client) if cert_summary is None: return None cert_arn = cert_summary.get("CertificateArn", None) return cert_arn