dbt-platform-helper 11.3.0__py3-none-any.whl → 12.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of dbt-platform-helper might be problematic. Click here for more details.

Files changed (33) hide show
  1. dbt_platform_helper/COMMANDS.md +3 -252
  2. dbt_platform_helper/addons-template-map.yml +7 -33
  3. dbt_platform_helper/commands/application.py +8 -7
  4. dbt_platform_helper/commands/conduit.py +1 -4
  5. dbt_platform_helper/commands/copilot.py +14 -110
  6. dbt_platform_helper/commands/environment.py +0 -5
  7. dbt_platform_helper/commands/pipeline.py +1 -13
  8. dbt_platform_helper/domain/database_copy.py +2 -2
  9. dbt_platform_helper/domain/maintenance_page.py +0 -3
  10. dbt_platform_helper/templates/addon-instructions.txt +1 -1
  11. dbt_platform_helper/templates/addons/svc/s3-policy.yml +0 -8
  12. dbt_platform_helper/utils/aws.py +3 -1
  13. dbt_platform_helper/utils/platform_config.py +2 -7
  14. dbt_platform_helper/utils/validation.py +3 -78
  15. {dbt_platform_helper-11.3.0.dist-info → dbt_platform_helper-12.0.0.dist-info}/METADATA +1 -1
  16. {dbt_platform_helper-11.3.0.dist-info → dbt_platform_helper-12.0.0.dist-info}/RECORD +20 -33
  17. platform_helper.py +0 -8
  18. dbt_platform_helper/commands/check_cloudformation.py +0 -87
  19. dbt_platform_helper/commands/dns.py +0 -952
  20. dbt_platform_helper/custom_resources/__init__.py +0 -0
  21. dbt_platform_helper/custom_resources/s3_object.py +0 -85
  22. dbt_platform_helper/templates/addons/env/addons.parameters.yml +0 -19
  23. dbt_platform_helper/templates/addons/env/aurora-postgres.yml +0 -604
  24. dbt_platform_helper/templates/addons/env/monitoring.yml +0 -121
  25. dbt_platform_helper/templates/addons/env/opensearch.yml +0 -257
  26. dbt_platform_helper/templates/addons/env/rds-postgres.yml +0 -603
  27. dbt_platform_helper/templates/addons/env/redis-cluster.yml +0 -171
  28. dbt_platform_helper/templates/addons/env/s3.yml +0 -219
  29. dbt_platform_helper/templates/addons/env/vpc.yml +0 -120
  30. dbt_platform_helper/utils/cloudformation.py +0 -34
  31. {dbt_platform_helper-11.3.0.dist-info → dbt_platform_helper-12.0.0.dist-info}/LICENSE +0 -0
  32. {dbt_platform_helper-11.3.0.dist-info → dbt_platform_helper-12.0.0.dist-info}/WHEEL +0 -0
  33. {dbt_platform_helper-11.3.0.dist-info → dbt_platform_helper-12.0.0.dist-info}/entry_points.txt +0 -0
@@ -1,952 +0,0 @@
1
- #!/usr/bin/env python
2
-
3
- import os
4
- import re
5
- import time
6
- from pathlib import Path
7
-
8
- import click
9
- import yaml
10
-
11
- from dbt_platform_helper.utils.aws import check_response
12
- from dbt_platform_helper.utils.aws import get_aws_session_or_abort
13
- from dbt_platform_helper.utils.aws import get_load_balancer_configuration
14
- from dbt_platform_helper.utils.aws import get_load_balancer_domain_and_configuration
15
- from dbt_platform_helper.utils.click import ClickDocOptGroup
16
- from dbt_platform_helper.utils.messages import abort_with_error
17
- from dbt_platform_helper.utils.versioning import (
18
- check_platform_helper_version_needs_update,
19
- )
20
-
21
- # To do
22
- # -----
23
- # Check user has logged into the aws accounts before scanning the accounts
24
- # When adding records from parent to subdomain, if ok, it then should remove them from parent domain
25
- # (run a test before removing)
26
-
27
- # Base domain depth
28
- AVAILABLE_DOMAINS = {
29
- "great.gov.uk": "live",
30
- "trade.gov.uk": "live",
31
- "prod.uktrade.digital": "live",
32
- "uktrade.digital": "dev",
33
- }
34
- AWS_CERT_REGION = "eu-west-2"
35
- COPILOT_DIR = "copilot"
36
-
37
-
38
- def _wait_for_certificate_validation(acm_client, certificate_arn, sleep_time=10, timeout=600):
39
- click.secho(f"Waiting up to {timeout} seconds for certificate to be validated...", fg="yellow")
40
- status = acm_client.describe_certificate(CertificateArn=certificate_arn)["Certificate"][
41
- "Status"
42
- ]
43
- elapsed_time = 0
44
- while status == "PENDING_VALIDATION":
45
- if elapsed_time >= timeout:
46
- raise Exception(
47
- f"Timeout ({timeout}s) reached for certificate validation, might be worth checking things in the AWS Console"
48
- )
49
- click.echo(
50
- click.style(f"{certificate_arn}", fg="white", bold=True)
51
- + click.style(
52
- f": Waiting {sleep_time}s for validation, {elapsed_time}s elapsed, {timeout - elapsed_time}s until we give up...",
53
- fg="yellow",
54
- ),
55
- )
56
- time.sleep(sleep_time)
57
- status = acm_client.describe_certificate(CertificateArn=certificate_arn)["Certificate"][
58
- "Status"
59
- ]
60
- elapsed_time += sleep_time
61
-
62
- if status == "ISSUED":
63
- click.secho("Certificate validated...", fg="green")
64
- else:
65
- raise Exception(f"""Certificate validation failed with the status "{status}".""")
66
-
67
-
68
- def create_cert(client, domain_client, domain_name, zone_id):
69
- certificate_arn = _get_existing_cert_if_one_exists_and_cleanup_bad_certs(client, domain_name)
70
-
71
- if certificate_arn:
72
- return certificate_arn
73
-
74
- if not click.confirm(
75
- click.style("Creating Certificate for ", fg="yellow")
76
- + click.style(f"{domain_name}\n", fg="white", bold=True)
77
- + click.style("Do you want to continue?", fg="yellow"),
78
- ):
79
- exit()
80
-
81
- certificate_arn = _request_cert(client, domain_name)
82
-
83
- # Create DNS validation records
84
- cert_record = _get_certificate_record(certificate_arn, client)
85
-
86
- if not click.confirm(
87
- click.style("Updating DNS record for certificate ", fg="yellow")
88
- + click.style(f"{domain_name}", fg="white", bold=True)
89
- + click.style(" with value ", fg="yellow")
90
- + click.style(f"""{cert_record["Value"]}\n""", fg="white", bold=True)
91
- + click.style("Do you want to continue?", fg="yellow"),
92
- ):
93
- exit()
94
-
95
- _create_resource_records(cert_record, domain_client, zone_id)
96
-
97
- # Wait for certificate to get to validation state before continuing.
98
- # Will upped the timeout from 600 to 1200 because he repeatedly saw it timeout at 600
99
- # and wants to give it a chance to take longer in case that's all that's wrong.
100
- _wait_for_certificate_validation(client, certificate_arn=certificate_arn, timeout=1200)
101
-
102
- return certificate_arn
103
-
104
-
105
- def _get_existing_cert_if_one_exists_and_cleanup_bad_certs(client, domain):
106
- for cert in _certs_for_domain(client, domain):
107
- if cert["Status"] == "ISSUED":
108
- click.secho("Certificate already exists, do not need to create.", fg="green")
109
- return cert["CertificateArn"]
110
- else:
111
- click.secho(
112
- f"Certificate already exists but appears to be invalid (in status '{cert['Status']}'), deleting the old cert.",
113
- fg="yellow",
114
- )
115
- client.delete_certificate(CertificateArn=cert["CertificateArn"])
116
- return None
117
-
118
-
119
- def _create_resource_records(cert_record, domain_client, zone_id):
120
- resource_records = domain_client.list_resource_record_sets(HostedZoneId=zone_id)[
121
- "ResourceRecordSets"
122
- ]
123
-
124
- for record in resource_records:
125
- if record["Type"] == cert_record["Type"] and record["Name"] == cert_record["Name"]:
126
- return
127
-
128
- domain_client.change_resource_record_sets(
129
- HostedZoneId=zone_id,
130
- ChangeBatch={
131
- "Changes": [
132
- {
133
- "Action": "CREATE",
134
- "ResourceRecordSet": {
135
- "Name": cert_record["Name"],
136
- "Type": cert_record["Type"],
137
- "TTL": 300,
138
- "ResourceRecords": [
139
- {"Value": cert_record["Value"]},
140
- ],
141
- },
142
- },
143
- ],
144
- },
145
- )
146
-
147
-
148
- def _certs_for_domain(client, domain):
149
- # Check if cert is present.
150
- cert_list = client.list_certificates(
151
- CertificateStatuses=[
152
- "PENDING_VALIDATION",
153
- "ISSUED",
154
- "INACTIVE",
155
- "EXPIRED",
156
- "VALIDATION_TIMED_OUT",
157
- "REVOKED",
158
- "FAILED",
159
- ],
160
- MaxItems=500,
161
- )["CertificateSummaryList"]
162
- certs_for_domain = [cert for cert in cert_list if cert["DomainName"] == domain]
163
- return certs_for_domain
164
-
165
-
166
- def _get_certificate_record(arn, client):
167
- # Need a pause for it to populate the DNS resource records
168
- cert_description = client.describe_certificate(CertificateArn=arn)
169
- while (
170
- cert_description["Certificate"].get("DomainValidationOptions") is None
171
- or cert_description["Certificate"]["DomainValidationOptions"][0].get("ResourceRecord")
172
- is None
173
- ):
174
- click.secho("Waiting for DNS records...", fg="yellow")
175
- time.sleep(2)
176
- cert_description = client.describe_certificate(CertificateArn=arn)
177
- return cert_description["Certificate"]["DomainValidationOptions"][0]["ResourceRecord"]
178
-
179
-
180
- def _request_cert(client, domain):
181
- cert_response = client.request_certificate(DomainName=domain, ValidationMethod="DNS")
182
- arn = cert_response["CertificateArn"]
183
- return arn
184
-
185
-
186
- def add_records(client, records, subdomain_id, action):
187
- if records["Type"] == "A":
188
- response = client.change_resource_record_sets(
189
- HostedZoneId=subdomain_id,
190
- ChangeBatch={
191
- "Comment": "Record created for copilot",
192
- "Changes": [
193
- {
194
- "Action": action,
195
- "ResourceRecordSet": {
196
- "Name": records["Name"],
197
- "Type": records["Type"],
198
- "AliasTarget": {
199
- "HostedZoneId": records["AliasTarget"]["HostedZoneId"],
200
- "DNSName": records["AliasTarget"]["DNSName"],
201
- "EvaluateTargetHealth": records["AliasTarget"][
202
- "EvaluateTargetHealth"
203
- ],
204
- },
205
- },
206
- },
207
- ],
208
- },
209
- )
210
- else:
211
- response = client.change_resource_record_sets(
212
- HostedZoneId=subdomain_id,
213
- ChangeBatch={
214
- "Comment": "Record created for copilot",
215
- "Changes": [
216
- {
217
- "Action": action,
218
- "ResourceRecordSet": {
219
- "Name": records["Name"],
220
- "Type": records["Type"],
221
- "TTL": records["TTL"],
222
- "ResourceRecords": [
223
- {"Value": records["ResourceRecords"][0]["Value"]},
224
- ],
225
- },
226
- },
227
- ],
228
- },
229
- )
230
-
231
- check_response(response)
232
- click.echo(
233
- click.style(f"{records['Name']}, Type: {records['Type']}", fg="white", bold=True)
234
- + click.style("Added.", fg="magenta"),
235
- )
236
- return response["ChangeInfo"]["Status"]
237
-
238
-
239
- def copy_records_from_parent_to_subdomain(client, parent_zone_id, subdomain, zone_id):
240
- response = client.list_resource_record_sets(HostedZoneId=parent_zone_id)
241
-
242
- for records in response["ResourceRecordSets"]:
243
- if subdomain in records["Name"]:
244
- click.secho(records["Name"] + " found", fg="green")
245
- add_records(client, records, zone_id, "UPSERT")
246
-
247
-
248
- def create_hosted_zones(client, base_domain, subdomain):
249
- domains_to_create = get_required_subdomains(base_domain, subdomain)
250
- zone_ids = {}
251
-
252
- for domain_name in domains_to_create:
253
- domain_zone = f"{domain_name}."
254
- parent_domain = domain_name.split(".", maxsplit=1)[1]
255
- parent_zone = f"{parent_domain}."
256
-
257
- domain_zone_id, parent_zone_id = _get_paginated_zones(client, parent_zone, domain_zone)
258
-
259
- if parent_zone_id is None:
260
- click.secho(
261
- f"The hosted zone: {parent_zone} does not exist in your AWS domain account",
262
- fg="red",
263
- )
264
- exit()
265
-
266
- if domain_zone_id is not None:
267
- click.secho(f"Hosted zone '{domain_zone}' already exists", fg="yellow")
268
- zone_ids[domain_name] = domain_zone_id
269
- else:
270
- subdomain_zone_id = _create_hosted_zone(
271
- client, domain_name, parent_zone, parent_zone_id
272
- )
273
- zone_ids[domain_name] = subdomain_zone_id
274
-
275
- return zone_ids
276
-
277
-
278
- def _get_hosted_zones_paginator(client):
279
- return client.get_paginator("list_hosted_zones").paginate()
280
-
281
-
282
- def _get_paginated_zones(client, parent_zone, domain_zone):
283
- domain_zone_id = parent_zone_id = None
284
-
285
- for page in _get_hosted_zones_paginator(client):
286
- # each page is a hosted zones response type object
287
- hosted_zones = {hz["Name"]: hz for hz in page["HostedZones"]}
288
-
289
- if parent_zone in hosted_zones and parent_zone_id is None:
290
- parent_zone_id = hosted_zones[parent_zone]["Id"]
291
-
292
- if domain_zone in hosted_zones and domain_zone_id is None:
293
- domain_zone_id = hosted_zones[domain_zone]["Id"]
294
-
295
- if all([parent_zone_id, domain_zone_id]):
296
- # both were found, no need to further pagination
297
- break
298
-
299
- return domain_zone_id, parent_zone_id
300
-
301
-
302
- def _create_hosted_zone(client, domain_name, parent_zone, parent_zone_id):
303
- if not click.confirm(
304
- click.style("About to create domain: ", fg="cyan")
305
- + click.style(f"{domain_name}\n", fg="white", bold=True)
306
- + click.style("Do you want to continue?", fg="cyan"),
307
- ):
308
- exit()
309
- response = client.create_hosted_zone(
310
- Name=domain_name,
311
- # Timestamp is on the end because CallerReference must be unique for every call
312
- CallerReference=f"{domain_name}_from_code_{int(time.time())}",
313
- )
314
- ns_records = response["DelegationSet"]
315
- subdomain_zone_id = response["HostedZone"]["Id"]
316
- copy_records_from_parent_to_subdomain(client, parent_zone_id, domain_name, subdomain_zone_id)
317
- if not click.confirm(
318
- click.style(f"Updating parent {parent_zone} domain with records: ", fg="cyan")
319
- + click.style(f"{ns_records['NameServers']}\n", fg="white", bold=True)
320
- + click.style("Do you want to continue?", fg="cyan"),
321
- ):
322
- exit()
323
- _add_subdomain_ns_records_to_parent(client, domain_name, ns_records, parent_zone_id)
324
-
325
- return subdomain_zone_id
326
-
327
-
328
- def _add_subdomain_ns_records_to_parent(client, domain_name, ns_records, parent_zone_id):
329
- # Add nameserver records of subdomain to parent
330
- nameservers = ns_records["NameServers"]
331
- # append . to make fully qualified domain name
332
- nameservers = [f"{nameserver}." for nameserver in nameservers]
333
- nameserver_resource_records = [{"Value": nameserver} for nameserver in nameservers]
334
- client.change_resource_record_sets(
335
- HostedZoneId=parent_zone_id,
336
- ChangeBatch={
337
- "Changes": [
338
- {
339
- "Action": "UPSERT",
340
- "ResourceRecordSet": {
341
- "Name": domain_name,
342
- "Type": "NS",
343
- "TTL": 300,
344
- "ResourceRecords": nameserver_resource_records,
345
- },
346
- },
347
- ],
348
- },
349
- )
350
-
351
-
352
- class InvalidDomainException(Exception):
353
- pass
354
-
355
-
356
- def _get_subdomains_from_base(base: list[str], subdomain: list[str]) -> list[list[str]]:
357
- """
358
- Recurse over domain to get a list of subdomains.
359
-
360
- These are ordered from smallest to largest, so the domains can be created in
361
- the correct order.
362
- """
363
- if base == subdomain:
364
- return []
365
- return _get_subdomains_from_base(base, subdomain[1:]) + [subdomain]
366
-
367
-
368
- def get_required_subdomains(base_domain: str, subdomain: str) -> list[str]:
369
- """
370
- We only want to get subdomains up to 4 levels deep.
371
-
372
- Prod base domains should be 3 levels deep, so we should create up to one
373
- additional subdomain. Dev base domain is always "uktrade.digital" and should
374
- have the app and env subdomains created, so 4 levels in either case.
375
- """
376
- if base_domain not in AVAILABLE_DOMAINS:
377
- raise InvalidDomainException(f"{base_domain} is not a supported base domain")
378
- _validate_subdomain(base_domain, subdomain)
379
- domains_as_lists = _get_subdomains_from_base(base_domain.split("."), subdomain.split("."))
380
-
381
- max_domain_levels = 4
382
- return [
383
- ".".join(domain_parts)
384
- for domain_parts in domains_as_lists
385
- if len(domain_parts) <= max_domain_levels
386
- ]
387
-
388
-
389
- def _validate_subdomain(base_domain, subdomain):
390
- errors = []
391
- if not subdomain.endswith("." + base_domain):
392
- errors.append(f"{subdomain} is not a subdomain of {base_domain}")
393
-
394
- errors.extend(_validate_basic_domain_syntax(subdomain))
395
-
396
- if errors:
397
- raise InvalidDomainException("\n".join(errors))
398
-
399
-
400
- def _validate_basic_domain_syntax(domain):
401
- errors = []
402
- if ".." in domain or not re.match(r"^[0-9a-zA-Z-.]+$", domain):
403
- errors.append(f"Subdomain {domain} is not a valid domain")
404
-
405
- return errors
406
-
407
-
408
- def validate_subdomains(subdomains: list[str]) -> None:
409
- bad_domains = []
410
- for subdomain in subdomains:
411
- is_valid = False
412
- for key in AVAILABLE_DOMAINS:
413
- if subdomain.endswith(key):
414
- is_valid = True
415
- break
416
- if not is_valid:
417
- bad_domains.append(subdomain)
418
- if bad_domains:
419
- csv_domains = ", ".join(bad_domains)
420
- raise InvalidDomainException(
421
- f"The following subdomains do not have one of the allowed base domains: {csv_domains}"
422
- )
423
-
424
-
425
- def get_base_domain(subdomains: list[str]) -> str:
426
- matching_domains = set()
427
- domains_by_length = sorted(AVAILABLE_DOMAINS.keys(), key=lambda d: len(d), reverse=True)
428
- for subdomain in subdomains:
429
- for domain_name in domains_by_length:
430
- if subdomain.endswith(f".{domain_name}"):
431
- matching_domains.add(domain_name)
432
- break
433
-
434
- if len(matching_domains) > 1:
435
- ordered_subdomains = ", ".join(sorted(matching_domains))
436
- raise InvalidDomainException(f"Multiple base domains were found: {ordered_subdomains}")
437
-
438
- if not matching_domains:
439
- ordered_subdomains = ", ".join(sorted(subdomains))
440
- raise InvalidDomainException(
441
- f"No base domains were found for subdomains: {ordered_subdomains}"
442
- )
443
-
444
- return matching_domains.pop()
445
-
446
-
447
- def get_certificate_zone_id(hosted_zones):
448
- hosted_zone_names = sorted([zone for zone in hosted_zones], key=lambda z: len(z))
449
- longest_hosted_zone = hosted_zone_names[-1]
450
-
451
- return hosted_zones[longest_hosted_zone]
452
-
453
-
454
- def create_required_zones_and_certs(domain_client, project_client, subdomain, base_domain):
455
- hosted_zones = create_hosted_zones(domain_client, base_domain, subdomain)
456
-
457
- cert_zone_id = get_certificate_zone_id(hosted_zones)
458
-
459
- return create_cert(project_client, domain_client, subdomain, cert_zone_id)
460
-
461
-
462
- @click.group(chain=True, cls=ClickDocOptGroup)
463
- def domain():
464
- check_platform_helper_version_needs_update()
465
-
466
-
467
- @domain.command()
468
- @click.option(
469
- "--project-profile", help="AWS account profile name for certificates account", required=True
470
- )
471
- @click.option("--env", help="AWS Copilot environment name", required=True)
472
- def configure(project_profile, env):
473
- """Creates subdomains if they do not exist and then creates certificates for
474
- them."""
475
-
476
- # If you need to reset to debug this command, you will need to delete any of the following
477
- # which have been created:
478
- # the certificate in your application's AWS account,
479
- # the hosted zone for the application environment in the dev AWS account,
480
- # and the applications records on the hosted zone for the environment in the dev AWS account.
481
-
482
- manifests = _get_manifests_or_abort()
483
-
484
- subdomains = _get_subdomains_from_env_manifests(env, manifests)
485
- validate_subdomains(subdomains)
486
- base_domain = get_base_domain(subdomains)
487
- domain_profile = AVAILABLE_DOMAINS[base_domain]
488
-
489
- domain_session = get_aws_session_or_abort(domain_profile)
490
- project_session = get_aws_session_or_abort(project_profile)
491
- domain_client = domain_session.client("route53")
492
- project_client = project_session.client("acm", region_name=AWS_CERT_REGION)
493
-
494
- cert_list = {}
495
-
496
- for subdomain in subdomains:
497
- cert_arn = create_required_zones_and_certs(
498
- domain_client,
499
- project_client,
500
- subdomain,
501
- base_domain,
502
- )
503
- cert_list.update({subdomain: cert_arn})
504
-
505
- if cert_list:
506
- click.secho("\nHere are your Certificate ARNs:", fg="cyan")
507
- for domain, cert in cert_list.items():
508
- click.secho(f"Domain: {domain} => Cert ARN: {cert}", fg="white", bold=True)
509
- else:
510
- click.secho("No domains found, please check the manifest file", fg="red")
511
-
512
-
513
- def _get_domain_aliases(conf, environment):
514
- aliases = []
515
- for env, domain in conf["environments"].items():
516
- if env == environment:
517
- # ensure configure doesn't blow up when http or alias
518
- # are missing
519
- if domain.get("http") and domain.get("http").get("alias"):
520
- aliases.append(domain["http"]["alias"])
521
- return aliases
522
-
523
-
524
- def _get_subdomains_from_env_manifests(environment, manifests):
525
- subdomains = []
526
- for manifest in manifests:
527
- # Need to check that the manifest file is correctly configured.
528
- with open(manifest, "r") as fd:
529
- conf = yaml.safe_load(fd)
530
- if "environments" in conf:
531
- click.echo(
532
- click.style("Checking file: ", fg="cyan") + click.style(manifest, fg="white"),
533
- )
534
- aliases = _get_domain_aliases(conf, environment)
535
- if not len(aliases):
536
- click.echo(
537
- click.style(
538
- f"No http.alias present for {environment} environment in {manifest}, skipping...",
539
- fg="cyan",
540
- ),
541
- )
542
- else:
543
- click.secho("Domains listed in manifest file", fg="cyan", underline=True)
544
- click.secho(
545
- " " + "\n ".join(aliases),
546
- fg="yellow",
547
- bold=True,
548
- )
549
- subdomains.extend(aliases)
550
- return subdomains
551
-
552
-
553
- def _get_manifests_or_abort():
554
- if not os.path.exists(COPILOT_DIR):
555
- abort_with_error("Please check path, copilot directory appears to be missing.")
556
-
557
- manifests = []
558
- for root, dirs, files in os.walk(COPILOT_DIR):
559
- for file in files:
560
- if file == "manifest.yml" or file == "manifest.yaml":
561
- manifests.append(os.path.join(root, file))
562
-
563
- if not manifests:
564
- abort_with_error("Please check path, no manifest files were found")
565
-
566
- return manifests
567
-
568
-
569
- @domain.command()
570
- @click.option("--app", help="Application Name", required=True)
571
- @click.option("--env", help="Environment", required=True)
572
- @click.option("--svc", help="Service Name", required=True)
573
- @click.option(
574
- "--domain-profile",
575
- help="AWS account profile name for Route53 domains account",
576
- required=True,
577
- type=click.Choice(["dev", "live"]),
578
- )
579
- @click.option(
580
- "--project-profile", help="AWS account profile name for application account", required=True
581
- )
582
- def assign(app, domain_profile, project_profile, svc, env):
583
- """Assigns the load balancer for a service to its domain name."""
584
- domain_session = get_aws_session_or_abort(domain_profile)
585
- project_session = get_aws_session_or_abort(project_profile)
586
-
587
- if not Path("./copilot").exists() or not Path("./copilot").is_dir():
588
- click.secho(
589
- "Cannot find copilot directory. Run this command in the root of the deployment repository.",
590
- bg="red",
591
- )
592
- exit(1)
593
- # Find the Load Balancer name.
594
- domain_name, load_balancer_configuration = get_load_balancer_domain_and_configuration(
595
- project_session, app, env, svc
596
- )
597
- elb_name = load_balancer_configuration["DNSName"]
598
-
599
- click.echo(
600
- click.style("The Domain: ", fg="yellow")
601
- + click.style(f"{domain_name}\n", fg="white", bold=True)
602
- + click.style("has been assigned the Load Balancer: ", fg="yellow")
603
- + click.style(f"{elb_name}\n", fg="white", bold=True)
604
- + click.style("Checking to see if this is in Route53", fg="yellow"),
605
- )
606
-
607
- domain_client = domain_session.client("route53")
608
- response = domain_client.list_hosted_zones_by_name()
609
- check_response(response)
610
-
611
- # Scan Route53 Zone for matching domains and update records if needed.
612
- hosted_zones = {}
613
- for hz in response["HostedZones"]:
614
- hosted_zones[hz["Name"]] = hz
615
-
616
- parts = domain_name.split(".")
617
- for _ in range(len(parts) - 1):
618
- subdomain = ".".join(parts) + "."
619
- click.echo(
620
- click.style("Searching for ", fg="yellow")
621
- + click.style(f"{subdomain}..", fg="white", bold=True)
622
- )
623
-
624
- if subdomain in hosted_zones:
625
- click.echo(
626
- click.style("Found hosted zone ", fg="yellow")
627
- + click.style(hosted_zones[subdomain]["Name"], fg="white", bold=True),
628
- )
629
- hosted_zone_id = hosted_zones[subdomain]["Id"]
630
-
631
- # Does record existing
632
- response = domain_client.list_resource_record_sets(
633
- HostedZoneId=hosted_zone_id,
634
- )
635
- check_response(response)
636
-
637
- for record in response["ResourceRecordSets"]:
638
- if domain_name == record["Name"][:-1]:
639
- click.echo(
640
- click.style("Record: ", fg="yellow")
641
- + click.style(f"{record['Name']} found", fg="white", bold=True),
642
- )
643
- click.echo(
644
- click.style("is pointing to LB ", fg="yellow")
645
- + click.style(
646
- f"{record['ResourceRecords'][0]['Value']}", fg="white", bold=True
647
- ),
648
- )
649
- if record["ResourceRecords"][0]["Value"] != elb_name:
650
- if click.confirm(
651
- click.style("This doesnt match with the current LB ", fg="yellow")
652
- + click.style(f"{elb_name}", fg="white", bold=True)
653
- + click.style("Do you wish to update the record?", fg="yellow"),
654
- ):
655
- record = {
656
- "Name": domain_name,
657
- "Type": "CNAME",
658
- "TTL": 300,
659
- "ResourceRecords": [{"Value": elb_name}],
660
- }
661
- add_records(domain_client, record, hosted_zone_id, "UPSERT")
662
- else:
663
- click.secho("No need to add as it already exists", fg="green")
664
- exit()
665
-
666
- record = {
667
- "Name": domain_name,
668
- "Type": "CNAME",
669
- "TTL": 300,
670
- "ResourceRecords": [{"Value": elb_name}],
671
- }
672
-
673
- if not click.confirm(
674
- click.style("Creating Route53 record: ", fg="yellow")
675
- + click.style(
676
- f"{record['Name']} -> {record['ResourceRecords'][0]['Value']}\n",
677
- fg="white",
678
- bold=True,
679
- )
680
- + click.style("In Domain: ", fg="yellow")
681
- + click.style(f"{subdomain}", fg="white", bold=True)
682
- + click.style("\tZone ID: ", fg="yellow")
683
- + click.style(f"{hosted_zone_id}\n", fg="white", bold=True)
684
- + click.style("Do you want to continue?", fg="yellow"),
685
- ):
686
- exit()
687
- add_records(domain_client, record, hosted_zone_id, "CREATE")
688
- exit()
689
-
690
- parts.pop(0)
691
-
692
- else:
693
- click.echo(
694
- click.style("No hosted zone found for ", fg="yellow")
695
- + click.style(f"{domain_name}", fg="white", bold=True),
696
- )
697
- return
698
-
699
-
700
- def update_rule(delete, listener, conditions, rule_arn, elb_client, acm_client, cdn_domain):
701
- # Update Rule
702
- response = elb_client.modify_rule(RuleArn=rule_arn, Conditions=conditions)
703
- check_response(response)
704
-
705
- # Create certificate if not already present.
706
- base_domain = get_base_domain([cdn_domain])
707
- domain_profile = AVAILABLE_DOMAINS[base_domain]
708
- domain_session = get_aws_session_or_abort(domain_profile)
709
- r53_client = domain_session.client("route53")
710
- cert_arn = create_required_zones_and_certs(
711
- r53_client,
712
- acm_client,
713
- cdn_domain,
714
- base_domain,
715
- )
716
-
717
- # Attach/Remove certificate from LB.
718
- if delete:
719
- response = elb_client.remove_listener_certificates(
720
- ListenerArn=listener["ListenerArn"],
721
- Certificates=[
722
- {
723
- "CertificateArn": cert_arn,
724
- },
725
- ],
726
- )
727
- else:
728
- response = elb_client.add_listener_certificates(
729
- ListenerArn=listener["ListenerArn"],
730
- Certificates=[
731
- {
732
- "CertificateArn": cert_arn,
733
- },
734
- ],
735
- )
736
- check_response(response)
737
-
738
- # List newly configured domains.
739
- for cond in conditions:
740
- if cond["Field"] == "host-header":
741
- click.echo(
742
- click.style("Domains now configured: ", fg="green")
743
- + click.style(
744
- f"{cond['HostHeaderConfig']['Values']}",
745
- fg="white",
746
- bold=True,
747
- ),
748
- )
749
-
750
-
751
- def find_domain_rules(action, delete, project_profile, env, app, svc):
752
- project_session = get_aws_session_or_abort(project_profile)
753
- elb_client = project_session.client("elbv2")
754
- acm_client = project_session.client("acm")
755
-
756
- loadbalancerarn = get_load_balancer_configuration(project_session, app, env, svc)[
757
- "LoadBalancers"
758
- ][0]["LoadBalancerArn"]
759
-
760
- response = elb_client.describe_listeners(
761
- LoadBalancerArn=loadbalancerarn,
762
- )
763
- check_response(response)
764
-
765
- # Certificates and domains only need to be configured on the HTTPS listener.
766
- for listener in response["Listeners"]:
767
- if listener["Protocol"] == "HTTPS":
768
- response = elb_client.describe_rules(
769
- ListenerArn=listener["ListenerArn"],
770
- )
771
- # If there are multiple services there will be more than two rules
772
- # (1 default, 1 custom domain).
773
- if len(response["Rules"]) > 2:
774
- click.echo(
775
- click.style(
776
- f"Note: Multiple Domains are configured on this Load Balancer.\n",
777
- fg="blue",
778
- bold=True,
779
- ),
780
- )
781
-
782
- # Get the current rule so we can update host header with new domain.
783
- for rule in response["Rules"]:
784
- save = True
785
- rule_arn = rule["RuleArn"]
786
- conditions = rule["Conditions"]
787
-
788
- if conditions:
789
- for cond in conditions:
790
- # Only check if the condition is using the host header
791
- if cond["Field"] == "host-header":
792
- values_string = ";".join(cond["Values"])
793
- click.echo(
794
- click.style("Domains currently configured: ", fg="yellow")
795
- + click.style(f"{values_string}", fg="white", bold=True),
796
- )
797
- cdn_domain = input(
798
- "Enter in domain you wish to remove (or press Enter to skip): "
799
- if delete
800
- else "Enter in domain you wish to add (or press Enter to skip): "
801
- )
802
-
803
- if not cdn_domain:
804
- save = False
805
- break
806
-
807
- save = action(cond, cdn_domain)
808
-
809
- if not save:
810
- break
811
-
812
- cond.pop("Values", None)
813
-
814
- if cond["Field"] == "path-pattern":
815
- cond.pop("PathPatternConfig", None)
816
-
817
- if save:
818
- update_rule(
819
- delete,
820
- listener,
821
- conditions,
822
- rule_arn,
823
- elb_client,
824
- acm_client,
825
- cdn_domain,
826
- )
827
- break
828
-
829
-
830
- @click.group(chain=True, cls=ClickDocOptGroup)
831
- def cdn():
832
- check_platform_helper_version_needs_update()
833
-
834
-
835
- @cdn.command(name="assign")
836
- @click.option(
837
- "--project-profile", help="AWS account profile name for certificates account", required=True
838
- )
839
- @click.option("--env", help="AWS Copilot environment name", required=True)
840
- @click.option("--app", help="Application Name", required=True)
841
- @click.option("--svc", help="Service Name", required=True)
842
- def cdn_assign(project_profile, env, app, svc):
843
- """Assigns a CDN domain name to application loadbalancer."""
844
-
845
- def assign_domain(cond, cdn_domain):
846
- if cdn_domain in cond["Values"]:
847
- click.echo(
848
- click.style(f"{cdn_domain} already exists, exiting", fg="red"),
849
- )
850
- save = False
851
- return save
852
-
853
- cond["HostHeaderConfig"]["Values"].append(cdn_domain)
854
- save = True
855
- return save
856
-
857
- find_domain_rules(assign_domain, False, project_profile, env, app, svc)
858
-
859
-
860
- @cdn.command(name="delete")
861
- @click.option(
862
- "--project-profile", help="AWS account profile name for certificates account", required=True
863
- )
864
- @click.option("--env", help="AWS Copilot environment name", required=True)
865
- @click.option("--app", help="Application Name", required=True)
866
- @click.option("--svc", help="Service Name", required=True)
867
- # Feature to be added at later date if needed, there shouldn't be a need to remove all domains
868
- # @click.option("--force", help="Force remove", is_flag=True)
869
- def cdn_delete(project_profile, env, app, svc, force=False):
870
- """Assigns a CDN domain name to application loadbalancer."""
871
-
872
- def delete_domain(cond, cdn_domain):
873
- if click.confirm(
874
- click.style("Are you sure you wish to delete the domain ", fg="yellow")
875
- + click.style(f"{cdn_domain}?", fg="white", bold=True),
876
- ):
877
- # Exit if specified domain doesn't exist.
878
- if cdn_domain not in cond["Values"]:
879
- click.echo(
880
- click.style(f"{cdn_domain} doesn't exists, exiting", fg="red"),
881
- )
882
- save = False
883
- return save
884
-
885
- # At present at least 1 domain needs to be on the listener.
886
- if len(cond["Values"]) == 1 and not force:
887
- click.echo(
888
- click.style(
889
- f"{cdn_domain} is the only domain configured on the "
890
- + "LoadBalancer, exiting",
891
- fg="red",
892
- ),
893
- )
894
- save = False
895
- return save
896
-
897
- click.echo(
898
- click.style(f"deleting {cdn_domain}", fg="green"),
899
- )
900
- cond["HostHeaderConfig"]["Values"].remove(cdn_domain)
901
- save = True
902
- return save
903
- # Exit if not sure.
904
- else:
905
- save = False
906
- return save
907
-
908
- find_domain_rules(delete_domain, True, project_profile, env, app, svc)
909
-
910
-
911
- @cdn.command(name="list")
912
- @click.option(
913
- "--project-profile", help="AWS account profile name for certificates account", required=True
914
- )
915
- @click.option("--env", help="AWS Copilot environment name", required=True)
916
- @click.option("--app", help="Application Name", required=True)
917
- @click.option("--svc", help="Service Name", required=True)
918
- def cdn_list(project_profile, env, app, svc):
919
- """List CDN domain name attached to application loadbalancer."""
920
- project_session = get_aws_session_or_abort(project_profile)
921
-
922
- elb_client = project_session.client("elbv2")
923
-
924
- loadbalancerarn = get_load_balancer_configuration(project_session, app, env, svc)[
925
- "LoadBalancers"
926
- ][0]["LoadBalancerArn"]
927
-
928
- response = elb_client.describe_listeners(
929
- LoadBalancerArn=loadbalancerarn,
930
- )
931
- check_response(response)
932
-
933
- for listener in response["Listeners"]:
934
- if listener["Protocol"] == "HTTPS":
935
- response = elb_client.describe_rules(
936
- ListenerArn=listener["ListenerArn"],
937
- )
938
-
939
- # Get the current rule so we can list the configured domains.
940
- for rule in response["Rules"]:
941
- conditions = rule["Conditions"]
942
- if conditions:
943
- for cond in conditions:
944
- if cond["Field"] == "host-header":
945
- click.echo(
946
- click.style("Domains currently configured: ", fg="yellow")
947
- + click.style(f"{cond['Values']}", fg="white", bold=True),
948
- )
949
-
950
-
951
- if __name__ == "__main__":
952
- domain()