dbt-platform-helper 12.5.1__py3-none-any.whl → 12.6.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 (28) hide show
  1. dbt_platform_helper/COMMANDS.md +38 -35
  2. dbt_platform_helper/commands/codebase.py +5 -8
  3. dbt_platform_helper/commands/conduit.py +2 -2
  4. dbt_platform_helper/commands/config.py +1 -1
  5. dbt_platform_helper/commands/environment.py +32 -18
  6. dbt_platform_helper/commands/pipeline.py +0 -3
  7. dbt_platform_helper/domain/codebase.py +13 -20
  8. dbt_platform_helper/domain/conduit.py +10 -12
  9. dbt_platform_helper/domain/config_validator.py +40 -7
  10. dbt_platform_helper/domain/copilot_environment.py +133 -129
  11. dbt_platform_helper/domain/database_copy.py +38 -37
  12. dbt_platform_helper/domain/maintenance_page.py +206 -193
  13. dbt_platform_helper/domain/pipelines.py +10 -11
  14. dbt_platform_helper/domain/terraform_environment.py +3 -3
  15. dbt_platform_helper/providers/cloudformation.py +12 -1
  16. dbt_platform_helper/providers/config.py +10 -12
  17. dbt_platform_helper/providers/io.py +31 -0
  18. dbt_platform_helper/providers/load_balancers.py +29 -3
  19. dbt_platform_helper/providers/platform_config_schema.py +10 -7
  20. dbt_platform_helper/providers/vpc.py +81 -32
  21. dbt_platform_helper/templates/COMMANDS.md.jinja +5 -3
  22. dbt_platform_helper/templates/pipelines/codebase/overrides/package-lock.json +819 -623
  23. dbt_platform_helper/utils/messages.py +2 -3
  24. {dbt_platform_helper-12.5.1.dist-info → dbt_platform_helper-12.6.0.dist-info}/METADATA +2 -2
  25. {dbt_platform_helper-12.5.1.dist-info → dbt_platform_helper-12.6.0.dist-info}/RECORD +28 -27
  26. {dbt_platform_helper-12.5.1.dist-info → dbt_platform_helper-12.6.0.dist-info}/LICENSE +0 -0
  27. {dbt_platform_helper-12.5.1.dist-info → dbt_platform_helper-12.6.0.dist-info}/WHEEL +0 -0
  28. {dbt_platform_helper-12.5.1.dist-info → dbt_platform_helper-12.6.0.dist-info}/entry_points.txt +0 -0
@@ -1,28 +1,27 @@
1
1
  from copy import deepcopy
2
2
  from pathlib import Path
3
3
 
4
- import click
5
4
  from schema import SchemaError
6
5
 
7
6
  from dbt_platform_helper.constants import PLATFORM_CONFIG_FILE
8
7
  from dbt_platform_helper.domain.config_validator import ConfigValidator
8
+ from dbt_platform_helper.providers.io import ClickIOProvider
9
9
  from dbt_platform_helper.providers.platform_config_schema import PlatformConfigSchema
10
10
  from dbt_platform_helper.providers.yaml_file import FileNotFoundException
11
11
  from dbt_platform_helper.providers.yaml_file import FileProviderException
12
12
  from dbt_platform_helper.providers.yaml_file import YamlFileProvider
13
- from dbt_platform_helper.utils.messages import abort_with_error
14
13
 
15
14
 
16
15
  class ConfigProvider:
17
16
  def __init__(
18
17
  self,
19
- config_validator: ConfigValidator,
18
+ config_validator: ConfigValidator = None,
20
19
  file_provider: YamlFileProvider = None,
21
- echo=click.secho,
20
+ io: ClickIOProvider = None,
22
21
  ):
23
22
  self.config = {}
24
- self.validator = config_validator
25
- self.echo = echo
23
+ self.validator = config_validator or ConfigValidator()
24
+ self.io = io or ClickIOProvider()
26
25
  self.file_provider = file_provider or YamlFileProvider
27
26
 
28
27
  # TODO refactor so that apply_environment_defaults isn't set, discarded and set again
@@ -42,24 +41,23 @@ class ConfigProvider:
42
41
  try:
43
42
  self.config = self.file_provider.load(path)
44
43
  except FileNotFoundException as e:
45
- abort_with_error(
44
+ self.io.abort_with_error(
46
45
  f"{e} Please check it exists and you are in the root directory of your deployment project."
47
46
  )
48
47
  except FileProviderException as e:
49
- abort_with_error(f"Error loading configuration from {path}: {e}")
48
+ self.io.abort_with_error(f"Error loading configuration from {path}: {e}")
50
49
 
51
50
  try:
52
51
  self.validate_platform_config()
53
52
  except SchemaError as e:
54
- abort_with_error(f"Schema error in {path}. {e}")
53
+ self.io.abort_with_error(f"Schema error in {path}. {e}")
55
54
 
56
55
  return self.config
57
56
 
58
- @staticmethod
59
57
  # TODO this general function should be moved out of ConfigProvider
60
- def config_file_check(path=PLATFORM_CONFIG_FILE):
58
+ def config_file_check(self, path=PLATFORM_CONFIG_FILE):
61
59
  if not Path(path).exists():
62
- abort_with_error(
60
+ self.io.abort_with_error(
63
61
  f"`{path}` is missing. "
64
62
  "Please check it exists and you are in the root directory of your deployment project."
65
63
  )
@@ -0,0 +1,31 @@
1
+ import click
2
+
3
+ from dbt_platform_helper.platform_exception import PlatformException
4
+
5
+
6
+ class ClickIOProvider:
7
+ def warn(self, message: str):
8
+ click.secho(message, fg="magenta")
9
+
10
+ def error(self, message: str):
11
+ click.secho(f"Error: {message}", fg="red")
12
+
13
+ def info(self, message: str):
14
+ click.secho(message)
15
+
16
+ def input(self, message: str) -> str:
17
+ return click.prompt(message)
18
+
19
+ def confirm(self, message: str) -> bool:
20
+ try:
21
+ return click.confirm(message)
22
+ except click.Abort:
23
+ raise ClickIOProviderException(message + " [y/N]: Error: invalid input")
24
+
25
+ def abort_with_error(self, message: str):
26
+ click.secho(f"Error: {message}", err=True, fg="red")
27
+ exit(1)
28
+
29
+
30
+ class ClickIOProviderException(PlatformException):
31
+ pass
@@ -2,8 +2,11 @@ import boto3
2
2
 
3
3
  from dbt_platform_helper.platform_exception import PlatformException
4
4
 
5
+ # TODO - a good candidate for a dataclass when this is refactored into a class.
6
+ # Below methods should also really be refactored to not be so tightly coupled with eachother.
5
7
 
6
- def find_load_balancer(session: boto3.Session, app: str, env: str) -> str:
8
+
9
+ def get_load_balancer_for_application(session: boto3.Session, app: str, env: str) -> str:
7
10
  lb_client = session.client("elbv2")
8
11
 
9
12
  describe_response = lb_client.describe_load_balancers()
@@ -23,8 +26,8 @@ def find_load_balancer(session: boto3.Session, app: str, env: str) -> str:
23
26
  return load_balancer_arn
24
27
 
25
28
 
26
- def find_https_listener(session: boto3.Session, app: str, env: str) -> str:
27
- load_balancer_arn = find_load_balancer(session, app, env)
29
+ def get_https_listener_for_application(session: boto3.Session, app: str, env: str) -> str:
30
+ load_balancer_arn = get_load_balancer_for_application(session, app, env)
28
31
  lb_client = session.client("elbv2")
29
32
  listeners = lb_client.describe_listeners(LoadBalancerArn=load_balancer_arn)["Listeners"]
30
33
 
@@ -41,6 +44,22 @@ def find_https_listener(session: boto3.Session, app: str, env: str) -> str:
41
44
  return listener_arn
42
45
 
43
46
 
47
+ def get_https_certificate_for_application(session: boto3.Session, app: str, env: str) -> str:
48
+
49
+ listener_arn = get_https_listener_for_application(session, app, env)
50
+ cert_client = session.client("elbv2")
51
+ certificates = cert_client.describe_listener_certificates(ListenerArn=listener_arn)[
52
+ "Certificates"
53
+ ]
54
+
55
+ try:
56
+ certificate_arn = next(c["CertificateArn"] for c in certificates if c["IsDefault"])
57
+ except StopIteration:
58
+ raise CertificateNotFoundException(env)
59
+
60
+ return certificate_arn
61
+
62
+
44
63
  class LoadBalancerException(PlatformException):
45
64
  pass
46
65
 
@@ -55,3 +74,10 @@ class ListenerNotFoundException(LoadBalancerException):
55
74
 
56
75
  class ListenerRuleNotFoundException(LoadBalancerException):
57
76
  pass
77
+
78
+
79
+ class CertificateNotFoundException(PlatformException):
80
+ def __init__(self, environment_name: str):
81
+ super().__init__(
82
+ f"""No certificate found with domain name matching environment {environment_name}."."""
83
+ )
@@ -443,14 +443,17 @@ class PlatformConfigSchema:
443
443
  error=f"{key} must contain a valid ARN for an S3 bucket",
444
444
  )
445
445
 
446
+ _single_import_config = {
447
+ Optional("source_kms_key_arn"): PlatformConfigSchema.__valid_kms_key_arn(
448
+ "source_kms_key_arn"
449
+ ),
450
+ "source_bucket_arn": _valid_s3_bucket_arn("source_bucket_arn"),
451
+ "worker_role_arn": PlatformConfigSchema.__valid_iam_role_arn("worker_role_arn"),
452
+ }
453
+
446
454
  _valid_s3_data_migration = {
447
- "import": {
448
- Optional("source_kms_key_arn"): PlatformConfigSchema.__valid_kms_key_arn(
449
- "source_kms_key_arn"
450
- ),
451
- "source_bucket_arn": _valid_s3_bucket_arn("source_bucket_arn"),
452
- "worker_role_arn": PlatformConfigSchema.__valid_iam_role_arn("worker_role_arn"),
453
- },
455
+ Optional("import"): _single_import_config,
456
+ Optional("import_sources"): [_single_import_config],
454
457
  }
455
458
 
456
459
  _valid_s3_bucket_retention_policy = Or(
@@ -1,57 +1,106 @@
1
1
  from dataclasses import dataclass
2
2
 
3
- from dbt_platform_helper.providers.aws import AWSException
3
+ from dbt_platform_helper.platform_exception import PlatformException
4
+
5
+
6
+ class VpcProviderException(PlatformException):
7
+ pass
8
+
9
+
10
+ class SubnetsNotFoundException(VpcProviderException):
11
+ pass
12
+
13
+
14
+ class PrivateSubnetsNotFoundException(VpcProviderException):
15
+ pass
16
+
17
+
18
+ class PublicSubnetsNotFoundException(VpcProviderException):
19
+ pass
20
+
21
+
22
+ class SecurityGroupNotFoundException(VpcProviderException):
23
+ pass
24
+
25
+
26
+ class VpcNotFoundForNameException(VpcProviderException):
27
+ pass
28
+
29
+
30
+ class VpcIdMissingException(VpcProviderException):
31
+ pass
4
32
 
5
33
 
6
34
  @dataclass
7
35
  class Vpc:
8
- subnets: list[str]
36
+ id: str
37
+ public_subnets: list[str]
38
+ private_subnets: list[str]
9
39
  security_groups: list[str]
10
40
 
11
41
 
12
42
  class VpcProvider:
13
43
  def __init__(self, session):
14
44
  self.ec2_client = session.client("ec2")
15
- self.ec2_resource = session.resource("ec2")
16
45
 
17
- def get_vpc_info_by_name(self, app: str, env: str, vpc_name: str) -> Vpc:
18
- vpc_response = self.ec2_client.describe_vpcs(
19
- Filters=[{"Name": "tag:Name", "Values": [vpc_name]}]
20
- )
46
+ def _get_subnet_ids(self, vpc_id):
47
+ subnets = self.ec2_client.describe_subnets(
48
+ Filters=[{"Name": "vpc-id", "Values": [vpc_id]}]
49
+ ).get("Subnets")
50
+
51
+ if not subnets:
52
+ raise SubnetsNotFoundException(f"No subnets found for VPC with id: {vpc_id}.")
53
+
54
+ public_tag = {"Key": "subnet_type", "Value": "public"}
55
+ public_subnets = [subnet["SubnetId"] for subnet in subnets if public_tag in subnet["Tags"]]
21
56
 
22
- matching_vpcs = vpc_response.get("Vpcs", [])
57
+ private_tag = {"Key": "subnet_type", "Value": "private"}
58
+ private_subnets = [
59
+ subnet["SubnetId"] for subnet in subnets if private_tag in subnet["Tags"]
60
+ ]
23
61
 
24
- if not matching_vpcs:
25
- raise AWSException(f"VPC not found for name '{vpc_name}'")
62
+ return public_subnets, private_subnets
26
63
 
27
- vpc_id = vpc_response["Vpcs"][0].get("VpcId")
64
+ def _get_vpc_id_by_name(self, vpc_name: str) -> str:
65
+ vpcs = self.ec2_client.describe_vpcs(
66
+ Filters=[{"Name": "tag:Name", "Values": [vpc_name]}]
67
+ ).get("Vpcs", [])
68
+
69
+ if not vpcs:
70
+ raise VpcNotFoundForNameException(f"VPC not found for name '{vpc_name}'")
71
+
72
+ vpc_id = vpcs[0].get("VpcId")
28
73
 
29
74
  if not vpc_id:
30
- raise AWSException(f"VPC id not present in vpc '{vpc_name}'")
75
+ raise VpcIdMissingException(f"VPC id not present in vpc '{vpc_name}'")
31
76
 
32
- vpc = self.ec2_resource.Vpc(vpc_id)
77
+ return vpc_id
33
78
 
34
- route_tables = self.ec2_client.describe_route_tables(
35
- Filters=[{"Name": "vpc-id", "Values": [vpc_id]}]
36
- )["RouteTables"]
37
-
38
- subnets = []
39
- for route_table in route_tables:
40
- private_routes = [route for route in route_table["Routes"] if "NatGatewayId" in route]
41
- if not private_routes:
42
- continue
43
- for association in route_table["Associations"]:
44
- if "SubnetId" in association:
45
- subnet_id = association["SubnetId"]
46
- subnets.append(subnet_id)
79
+ def _get_security_groups(self, app: str, env: str, vpc_id: str) -> list:
47
80
 
48
- if not subnets:
49
- raise AWSException(f"No private subnets found in vpc '{vpc_name}'")
81
+ vpc_filter = {"Name": "vpc-id", "Values": [vpc_id]}
82
+ tag_filter = {"Name": f"tag:Name", "Values": [f"copilot-{app}-{env}-env"]}
83
+ response = self.ec2_client.describe_security_groups(Filters=[vpc_filter, tag_filter])
84
+
85
+ return [sg.get("GroupId") for sg in response.get("SecurityGroups")]
86
+
87
+ def get_vpc(self, app: str, env: str, vpc_name: str) -> Vpc:
88
+
89
+ vpc_id = self._get_vpc_id_by_name(vpc_name)
90
+
91
+ public_subnets, private_subnets = self._get_subnet_ids(vpc_id)
92
+
93
+ if not private_subnets:
94
+ raise PrivateSubnetsNotFoundException(f"No private subnets found in vpc '{vpc_name}'")
95
+
96
+ if not public_subnets:
97
+ raise PublicSubnetsNotFoundException(f"No public subnets found in vpc '{vpc_name}'")
50
98
 
51
- tag_value = {"Key": "Name", "Value": f"copilot-{app}-{env}-env"}
52
- sec_groups = [sg.id for sg in vpc.security_groups.all() if sg.tags and tag_value in sg.tags]
99
+ sec_groups = self._get_security_groups(app, env, vpc_id)
53
100
 
54
101
  if not sec_groups:
55
- raise AWSException(f"No matching security groups found in vpc '{vpc_name}'")
102
+ raise SecurityGroupNotFoundException(
103
+ f"No matching security groups found in vpc '{vpc_name}'"
104
+ )
56
105
 
57
- return Vpc(subnets, sec_groups)
106
+ return Vpc(vpc_id, public_subnets, private_subnets, sec_groups)
@@ -1,8 +1,10 @@
1
1
  # Commands Reference
2
2
 
3
- {% for command in toc -%}
4
- - [{{ command }}](#{{ command.lower() | replace(" ", "-") }})
5
- {% endfor %}
3
+ {% for meta in metadata -%}
4
+ {% if meta.name -%}
5
+ {{ meta.indent }}- [{{ meta.name }}](#{{ meta.name.lower() | replace(" ", "-") }})
6
+ {% endif -%}
7
+ {% endfor -%}
6
8
 
7
9
  {%- for meta in metadata +%}
8
10
  {% if meta.name -%}