dbt-platform-helper 11.0.1__py3-none-any.whl → 11.2.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.

@@ -1,19 +1,11 @@
1
- import random
2
- import re
3
- import string
4
- from pathlib import Path
5
- from typing import List
6
- from typing import Union
7
-
8
1
  import boto3
9
2
  import click
10
3
  from schema import SchemaError
11
4
 
12
5
  from dbt_platform_helper.constants import DEFAULT_TERRAFORM_PLATFORM_MODULES_VERSION
13
6
  from dbt_platform_helper.constants import PLATFORM_CONFIG_FILE
14
- from dbt_platform_helper.utils.application import Environment
15
- from dbt_platform_helper.utils.application import Service
16
- from dbt_platform_helper.utils.application import load_application
7
+ from dbt_platform_helper.domain.maintenance_page import MaintenancePageProvider
8
+ from dbt_platform_helper.providers.load_balancers import find_https_listener
17
9
  from dbt_platform_helper.utils.aws import get_aws_session_or_abort
18
10
  from dbt_platform_helper.utils.click import ClickDocOptGroup
19
11
  from dbt_platform_helper.utils.files import apply_environment_defaults
@@ -47,72 +39,8 @@ def environment():
47
39
  @click.option("--vpc", type=str)
48
40
  def offline(app, env, svc, template, vpc):
49
41
  """Take load-balanced web services offline with a maintenance page."""
50
- application = get_application(app)
51
- application_environment = get_app_environment(app, env)
52
-
53
- if "*" in svc:
54
- services = [
55
- s for s in application.services.values() if s.kind == "Load Balanced Web Service"
56
- ]
57
- else:
58
- all_services = [get_app_service(app, s) for s in list(svc)]
59
- services = [s for s in all_services if s.kind == "Load Balanced Web Service"]
60
-
61
- if not services:
62
- click.secho(f"No services deployed yet to {app} environment {env}", fg="red")
63
- raise click.Abort
64
-
65
- try:
66
- https_listener = find_https_listener(application_environment.session, app, env)
67
- current_maintenance_page = get_maintenance_page(
68
- application_environment.session, https_listener
69
- )
70
- remove_current_maintenance_page = False
71
- if current_maintenance_page:
72
- remove_current_maintenance_page = click.confirm(
73
- f"There is currently a '{current_maintenance_page}' maintenance page for the {env} "
74
- f"environment in {app}.\nWould you like to replace it with a '{template}' "
75
- f"maintenance page?"
76
- )
77
- if not remove_current_maintenance_page:
78
- raise click.Abort
79
-
80
- if remove_current_maintenance_page or click.confirm(
81
- f"You are about to enable the '{template}' maintenance page for the {env} "
82
- f"environment in {app}.\nWould you like to continue?"
83
- ):
84
- if current_maintenance_page and remove_current_maintenance_page:
85
- remove_maintenance_page(application_environment.session, https_listener)
86
-
87
- allowed_ips = get_env_ips(vpc, application_environment)
88
-
89
- add_maintenance_page(
90
- application_environment.session,
91
- https_listener,
92
- app,
93
- env,
94
- services,
95
- allowed_ips,
96
- template,
97
- )
98
- click.secho(
99
- f"Maintenance page '{template}' added for environment {env} in application {app}",
100
- fg="green",
101
- )
102
- else:
103
- raise click.Abort
104
-
105
- except LoadBalancerNotFoundError:
106
- click.secho(
107
- f"No load balancer found for environment {env} in the application {app}.", fg="red"
108
- )
109
- raise click.Abort
110
-
111
- except ListenerNotFoundError:
112
- click.secho(
113
- f"No HTTPS listener found for environment {env} in the application {app}.", fg="red"
114
- )
115
- raise click.Abort
42
+ maintenance_page = MaintenancePageProvider()
43
+ maintenance_page.activate(app, env, svc, template, vpc)
116
44
 
117
45
 
118
46
  @environment.command()
@@ -120,85 +48,8 @@ def offline(app, env, svc, template, vpc):
120
48
  @click.option("--env", type=str, required=True)
121
49
  def online(app, env):
122
50
  """Remove a maintenance page from an environment."""
123
- application_environment = get_app_environment(app, env)
124
-
125
- try:
126
- https_listener = find_https_listener(application_environment.session, app, env)
127
- current_maintenance_page = get_maintenance_page(
128
- application_environment.session, https_listener
129
- )
130
- if not current_maintenance_page:
131
- click.secho("There is no current maintenance page to remove", fg="red")
132
- raise click.Abort
133
-
134
- if not click.confirm(
135
- f"There is currently a '{current_maintenance_page}' maintenance page, "
136
- f"would you like to remove it?"
137
- ):
138
- raise click.Abort
139
-
140
- remove_maintenance_page(application_environment.session, https_listener)
141
- click.secho(
142
- f"Maintenance page removed from environment {env} in application {app}", fg="green"
143
- )
144
-
145
- except LoadBalancerNotFoundError:
146
- click.secho(
147
- f"No load balancer found for environment {env} in the application {app}.", fg="red"
148
- )
149
- raise click.Abort
150
-
151
- except ListenerNotFoundError:
152
- click.secho(
153
- f"No HTTPS listener found for environment {env} in the application {app}.", fg="red"
154
- )
155
- raise click.Abort
156
-
157
-
158
- def get_application(app_name: str):
159
- return load_application(app_name)
160
-
161
-
162
- def get_app_environment(app_name: str, env_name: str) -> Environment:
163
- application = get_application(app_name)
164
- application_environment = application.environments.get(env_name)
165
-
166
- if not application_environment:
167
- click.secho(
168
- f"The environment {env_name} was not found in the application {app_name}. "
169
- f"It either does not exist, or has not been deployed.",
170
- fg="red",
171
- )
172
- raise click.Abort
173
-
174
- return application_environment
175
-
176
-
177
- def get_app_service(app_name: str, svc_name: str) -> Service:
178
- application = get_application(app_name)
179
- application_service = application.services.get(svc_name)
180
-
181
- if not application_service:
182
- click.secho(
183
- f"The service {svc_name} was not found in the application {app_name}. "
184
- f"It either does not exist, or has not been deployed.",
185
- fg="red",
186
- )
187
- raise click.Abort
188
-
189
- return application_service
190
-
191
-
192
- def get_listener_rule_by_tag(elbv2_client, listener_arn, tag_key, tag_value):
193
- response = elbv2_client.describe_rules(ListenerArn=listener_arn)
194
- for rule in response["Rules"]:
195
- rule_arn = rule["RuleArn"]
196
-
197
- tags_response = elbv2_client.describe_tags(ResourceArns=[rule_arn])
198
- for tag_description in tags_response["TagDescriptions"]:
199
- for tag in tag_description["Tags"]:
200
- if tag["Key"] == tag_key and tag["Value"] == tag_value:
201
- return rule
51
+ maintenance_page = MaintenancePageProvider()
52
+ maintenance_page.deactivate(app, env)
202
53
 
203
54
 
204
55
  def get_vpc_id(session, env_name, vpc_name=None):
@@ -250,20 +101,6 @@ def get_cert_arn(session, application, env_name):
250
101
  return arn
251
102
 
252
103
 
253
- def get_env_ips(vpc: str, application_environment: Environment) -> List[str]:
254
- account_name = f"{application_environment.session.profile_name}-vpc"
255
- vpc_name = vpc if vpc else account_name
256
- ssm_client = application_environment.session.client("ssm")
257
-
258
- try:
259
- param_value = ssm_client.get_parameter(Name=f"/{vpc_name}/EGRESS_IPS")["Parameter"]["Value"]
260
- except ssm_client.exceptions.ParameterNotFound:
261
- click.secho(f"No parameter found with name: /{vpc_name}/EGRESS_IPS")
262
- raise click.Abort
263
-
264
- return [ip.strip() for ip in param_value.split(",")]
265
-
266
-
267
104
  @environment.command()
268
105
  @click.option("--vpc-name", hidden=True)
269
106
  @click.option("--name", "-n", required=True)
@@ -362,44 +199,6 @@ def _determine_terraform_platform_modules_version(env_conf, cli_terraform_platfo
362
199
  return [version for version in version_preference_order if version][0]
363
200
 
364
201
 
365
- def find_load_balancer(session: boto3.Session, app: str, env: str) -> str:
366
- lb_client = session.client("elbv2")
367
-
368
- describe_response = lb_client.describe_load_balancers()
369
- load_balancers = [lb["LoadBalancerArn"] for lb in describe_response["LoadBalancers"]]
370
-
371
- load_balancers = lb_client.describe_tags(ResourceArns=load_balancers)["TagDescriptions"]
372
-
373
- load_balancer_arn = None
374
- for lb in load_balancers:
375
- tags = {t["Key"]: t["Value"] for t in lb["Tags"]}
376
- if tags.get("copilot-application") == app and tags.get("copilot-environment") == env:
377
- load_balancer_arn = lb["ResourceArn"]
378
-
379
- if not load_balancer_arn:
380
- raise LoadBalancerNotFoundError()
381
-
382
- return load_balancer_arn
383
-
384
-
385
- def find_https_listener(session: boto3.Session, app: str, env: str) -> str:
386
- load_balancer_arn = find_load_balancer(session, app, env)
387
- lb_client = session.client("elbv2")
388
- listeners = lb_client.describe_listeners(LoadBalancerArn=load_balancer_arn)["Listeners"]
389
-
390
- listener_arn = None
391
-
392
- try:
393
- listener_arn = next(l["ListenerArn"] for l in listeners if l["Protocol"] == "HTTPS")
394
- except StopIteration:
395
- pass
396
-
397
- if not listener_arn:
398
- raise ListenerNotFoundError()
399
-
400
- return listener_arn
401
-
402
-
403
202
  def find_https_certificate(session: boto3.Session, app: str, env: str) -> str:
404
203
  listener_arn = find_https_listener(session, app, env)
405
204
  cert_client = session.client("elbv2")
@@ -407,8 +206,6 @@ def find_https_certificate(session: boto3.Session, app: str, env: str) -> str:
407
206
  "Certificates"
408
207
  ]
409
208
 
410
- certificate_arn = None
411
-
412
209
  try:
413
210
  certificate_arn = next(c["CertificateArn"] for c in certificates if c["IsDefault"])
414
211
  except StopIteration:
@@ -417,314 +214,5 @@ def find_https_certificate(session: boto3.Session, app: str, env: str) -> str:
417
214
  return certificate_arn
418
215
 
419
216
 
420
- def find_target_group(app: str, env: str, svc: str, session: boto3.Session) -> str:
421
- rg_tagging_client = session.client("resourcegroupstaggingapi")
422
- response = rg_tagging_client.get_resources(
423
- TagFilters=[
424
- {
425
- "Key": "copilot-application",
426
- "Values": [
427
- app,
428
- ],
429
- "Key": "copilot-environment",
430
- "Values": [
431
- env,
432
- ],
433
- "Key": "copilot-service",
434
- "Values": [
435
- svc,
436
- ],
437
- },
438
- ],
439
- ResourceTypeFilters=[
440
- "elasticloadbalancing:targetgroup",
441
- ],
442
- )
443
- for resource in response["ResourceTagMappingList"]:
444
- tags = {tag["Key"]: tag["Value"] for tag in resource["Tags"]}
445
-
446
- if (
447
- "copilot-service" in tags
448
- and tags["copilot-service"] == svc
449
- and "copilot-environment" in tags
450
- and tags["copilot-environment"] == env
451
- and "copilot-application" in tags
452
- and tags["copilot-application"] == app
453
- ):
454
- return resource["ResourceARN"]
455
-
456
- click.secho(
457
- f"No target group found for application: {app}, environment: {env}, service: {svc}",
458
- fg="red",
459
- )
460
-
461
- return None
462
-
463
-
464
- def get_maintenance_page(session: boto3.Session, listener_arn: str) -> Union[str, None]:
465
- lb_client = session.client("elbv2")
466
-
467
- rules = lb_client.describe_rules(ListenerArn=listener_arn)["Rules"]
468
- tag_descriptions = get_rules_tag_descriptions(rules, lb_client)
469
-
470
- maintenance_page_type = None
471
- for description in tag_descriptions:
472
- tags = {t["Key"]: t["Value"] for t in description["Tags"]}
473
- if tags.get("name") == "MaintenancePage":
474
- maintenance_page_type = tags.get("type")
475
-
476
- return maintenance_page_type
477
-
478
-
479
- def delete_listener_rule(tag_descriptions: list, tag_name: str, lb_client: boto3.client):
480
- current_rule_arn = None
481
-
482
- for description in tag_descriptions:
483
- tags = {t["Key"]: t["Value"] for t in description["Tags"]}
484
- if tags.get("name") == tag_name:
485
- current_rule_arn = description["ResourceArn"]
486
- if current_rule_arn:
487
- lb_client.delete_rule(RuleArn=current_rule_arn)
488
-
489
- return current_rule_arn
490
-
491
-
492
- def remove_maintenance_page(session: boto3.Session, listener_arn: str):
493
- lb_client = session.client("elbv2")
494
-
495
- rules = lb_client.describe_rules(ListenerArn=listener_arn)["Rules"]
496
- tag_descriptions = get_rules_tag_descriptions(rules, lb_client)
497
- tag_descriptions = lb_client.describe_tags(ResourceArns=[r["RuleArn"] for r in rules])[
498
- "TagDescriptions"
499
- ]
500
-
501
- for name in ["MaintenancePage", "AllowedIps", "BypassIpFilter", "AllowedSourceIps"]:
502
- deleted = delete_listener_rule(tag_descriptions, name, lb_client)
503
-
504
- if name == "MaintenancePage" and not deleted:
505
- raise ListenerRuleNotFoundError()
506
-
507
-
508
- def get_rules_tag_descriptions(rules: list, lb_client):
509
- tag_descriptions = []
510
- chunk_size = 20
511
-
512
- for i in range(0, len(rules), chunk_size):
513
- chunk = rules[i : i + chunk_size]
514
- resource_arns = [r["RuleArn"] for r in chunk]
515
- response = lb_client.describe_tags(ResourceArns=resource_arns)
516
- tag_descriptions.extend(response["TagDescriptions"])
517
-
518
- return tag_descriptions
519
-
520
-
521
- def get_host_conditions(lb_client: boto3.client, listener_arn: str, target_group_arn: str):
522
- rules = lb_client.describe_rules(ListenerArn=listener_arn)["Rules"]
523
-
524
- # Get current set of forwarding conditions for the target group
525
- for rule in rules:
526
- for action in rule["Actions"]:
527
- if action["Type"] == "forward" and action["TargetGroupArn"] == target_group_arn:
528
- conditions = rule["Conditions"]
529
-
530
- # filter to host-header conditions
531
- conditions = [
532
- {i: condition[i] for i in condition if i != "Values"}
533
- for condition in conditions
534
- if condition["Field"] == "host-header"
535
- ]
536
-
537
- # remove internal hosts
538
- conditions[0]["HostHeaderConfig"]["Values"] = [
539
- v for v in conditions[0]["HostHeaderConfig"]["Values"]
540
- ]
541
-
542
- return conditions
543
-
544
-
545
- def create_header_rule(
546
- lb_client: boto3.client,
547
- listener_arn: str,
548
- target_group_arn: str,
549
- header_name: str,
550
- values: list,
551
- rule_name: str,
552
- priority: int,
553
- ):
554
- conditions = get_host_conditions(lb_client, listener_arn, target_group_arn)
555
-
556
- # add new condition to existing conditions
557
- combined_conditions = [
558
- {
559
- "Field": "http-header",
560
- "HttpHeaderConfig": {"HttpHeaderName": header_name, "Values": values},
561
- }
562
- ] + conditions
563
-
564
- lb_client.create_rule(
565
- ListenerArn=listener_arn,
566
- Priority=priority,
567
- Conditions=combined_conditions,
568
- Actions=[{"Type": "forward", "TargetGroupArn": target_group_arn}],
569
- Tags=[
570
- {"Key": "name", "Value": rule_name},
571
- ],
572
- )
573
-
574
- click.secho(
575
- f"Creating listener rule {rule_name} for HTTPS Listener with arn {listener_arn}.\n\nIf request header {header_name} contains one of the values {values}, the request will be forwarded to target group with arn {target_group_arn}.",
576
- fg="green",
577
- )
578
-
579
-
580
- def create_source_ip_rule(
581
- lb_client: boto3.client,
582
- listener_arn: str,
583
- target_group_arn: str,
584
- values: list,
585
- rule_name: str,
586
- priority: int,
587
- ):
588
- conditions = get_host_conditions(lb_client, listener_arn, target_group_arn)
589
-
590
- # add new condition to existing conditions
591
- combined_conditions = [
592
- {
593
- "Field": "source-ip",
594
- "SourceIpConfig": {"Values": [value + "/32" for value in values]},
595
- }
596
- ] + conditions
597
-
598
- lb_client.create_rule(
599
- ListenerArn=listener_arn,
600
- Priority=priority,
601
- Conditions=combined_conditions,
602
- Actions=[{"Type": "forward", "TargetGroupArn": target_group_arn}],
603
- Tags=[
604
- {"Key": "name", "Value": rule_name},
605
- ],
606
- )
607
-
608
- click.secho(
609
- f"Creating listener rule {rule_name} for HTTPS Listener with arn {listener_arn}.\n\nIf request source ip matches one of the values {values}, the request will be forwarded to target group with arn {target_group_arn}.",
610
- fg="green",
611
- )
612
-
613
-
614
- def add_maintenance_page(
615
- session: boto3.Session,
616
- listener_arn: str,
617
- app: str,
618
- env: str,
619
- services: List[Service],
620
- allowed_ips: tuple,
621
- template: str = "default",
622
- ):
623
- lb_client = session.client("elbv2")
624
- maintenance_page_content = get_maintenance_page_template(template)
625
- bypass_value = "".join(random.choices(string.ascii_lowercase + string.digits, k=12))
626
-
627
- service_number = 1
628
-
629
- for svc in services:
630
- target_group_arn = find_target_group(app, env, svc.name, session)
631
-
632
- # not all of an application's services are guaranteed to have been deployed to an environment
633
- if not target_group_arn:
634
- continue
635
-
636
- allowed_ips = list(allowed_ips)
637
- max_allowed_ips = 100
638
- for ip_index, ip in enumerate(allowed_ips):
639
- forwarded_rule_priority = (service_number * max_allowed_ips) + ip_index
640
- create_header_rule(
641
- lb_client,
642
- listener_arn,
643
- target_group_arn,
644
- "X-Forwarded-For",
645
- [ip],
646
- "AllowedIps",
647
- forwarded_rule_priority,
648
- )
649
- create_source_ip_rule(
650
- lb_client,
651
- listener_arn,
652
- target_group_arn,
653
- [ip],
654
- "AllowedSourceIps",
655
- forwarded_rule_priority + 1,
656
- )
657
-
658
- bypass_rule_priority = service_number
659
- create_header_rule(
660
- lb_client,
661
- listener_arn,
662
- target_group_arn,
663
- "Bypass-Key",
664
- [bypass_value],
665
- "BypassIpFilter",
666
- bypass_rule_priority,
667
- )
668
-
669
- service_number += 1
670
-
671
- click.secho(
672
- f"\nUse a browser plugin to add `Bypass-Key` header with value {bypass_value} to your requests. For more detail, visit https://platform.readme.trade.gov.uk/activities/holding-and-maintenance-pages/",
673
- fg="green",
674
- )
675
-
676
- fixed_rule_priority = (service_number + 5) * max_allowed_ips
677
- lb_client.create_rule(
678
- ListenerArn=listener_arn,
679
- Priority=fixed_rule_priority, # big number because we create multiple higher priority "AllowedIps" rules for each allowed ip for each service above.
680
- Conditions=[
681
- {
682
- "Field": "path-pattern",
683
- "PathPatternConfig": {"Values": ["/*"]},
684
- }
685
- ],
686
- Actions=[
687
- {
688
- "Type": "fixed-response",
689
- "FixedResponseConfig": {
690
- "StatusCode": "503",
691
- "ContentType": "text/html",
692
- "MessageBody": maintenance_page_content,
693
- },
694
- }
695
- ],
696
- Tags=[
697
- {"Key": "name", "Value": "MaintenancePage"},
698
- {"Key": "type", "Value": template},
699
- ],
700
- )
701
-
702
-
703
- def get_maintenance_page_template(template) -> str:
704
- template_contents = (
705
- Path(__file__)
706
- .parent.parent.joinpath(
707
- f"templates/svc/maintenance_pages/{template}.html",
708
- )
709
- .read_text()
710
- .replace("\n", "")
711
- )
712
-
713
- # [^\S]\s+ - Remove any space that is not preceded by a non-space character.
714
- return re.sub(r"[^\S]\s+", "", template_contents)
715
-
716
-
717
217
  class CertificateNotFoundError(Exception):
718
218
  pass
719
-
720
-
721
- class LoadBalancerNotFoundError(Exception):
722
- pass
723
-
724
-
725
- class ListenerNotFoundError(Exception):
726
- pass
727
-
728
-
729
- class ListenerRuleNotFoundError(Exception):
730
- pass