File size: 10,489 Bytes
105b369
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
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