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.
- dbt_platform_helper/COMMANDS.md +38 -35
- dbt_platform_helper/commands/codebase.py +5 -8
- dbt_platform_helper/commands/conduit.py +2 -2
- dbt_platform_helper/commands/config.py +1 -1
- dbt_platform_helper/commands/environment.py +32 -18
- dbt_platform_helper/commands/pipeline.py +0 -3
- dbt_platform_helper/domain/codebase.py +13 -20
- dbt_platform_helper/domain/conduit.py +10 -12
- dbt_platform_helper/domain/config_validator.py +40 -7
- dbt_platform_helper/domain/copilot_environment.py +133 -129
- dbt_platform_helper/domain/database_copy.py +38 -37
- dbt_platform_helper/domain/maintenance_page.py +206 -193
- dbt_platform_helper/domain/pipelines.py +10 -11
- dbt_platform_helper/domain/terraform_environment.py +3 -3
- dbt_platform_helper/providers/cloudformation.py +12 -1
- dbt_platform_helper/providers/config.py +10 -12
- dbt_platform_helper/providers/io.py +31 -0
- dbt_platform_helper/providers/load_balancers.py +29 -3
- dbt_platform_helper/providers/platform_config_schema.py +10 -7
- dbt_platform_helper/providers/vpc.py +81 -32
- dbt_platform_helper/templates/COMMANDS.md.jinja +5 -3
- dbt_platform_helper/templates/pipelines/codebase/overrides/package-lock.json +819 -623
- dbt_platform_helper/utils/messages.py +2 -3
- {dbt_platform_helper-12.5.1.dist-info → dbt_platform_helper-12.6.0.dist-info}/METADATA +2 -2
- {dbt_platform_helper-12.5.1.dist-info → dbt_platform_helper-12.6.0.dist-info}/RECORD +28 -27
- {dbt_platform_helper-12.5.1.dist-info → dbt_platform_helper-12.6.0.dist-info}/LICENSE +0 -0
- {dbt_platform_helper-12.5.1.dist-info → dbt_platform_helper-12.6.0.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
20
|
+
io: ClickIOProvider = None,
|
|
22
21
|
):
|
|
23
22
|
self.config = {}
|
|
24
|
-
self.validator = config_validator
|
|
25
|
-
self.
|
|
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
|
-
|
|
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
|
|
27
|
-
load_balancer_arn =
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
Filters=[{"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
|
-
|
|
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
|
-
|
|
25
|
-
raise AWSException(f"VPC not found for name '{vpc_name}'")
|
|
62
|
+
return public_subnets, private_subnets
|
|
26
63
|
|
|
27
|
-
|
|
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
|
|
75
|
+
raise VpcIdMissingException(f"VPC id not present in vpc '{vpc_name}'")
|
|
31
76
|
|
|
32
|
-
|
|
77
|
+
return vpc_id
|
|
33
78
|
|
|
34
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
|
102
|
+
raise SecurityGroupNotFoundException(
|
|
103
|
+
f"No matching security groups found in vpc '{vpc_name}'"
|
|
104
|
+
)
|
|
56
105
|
|
|
57
|
-
return Vpc(
|
|
106
|
+
return Vpc(vpc_id, public_subnets, private_subnets, sec_groups)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# Commands Reference
|
|
2
2
|
|
|
3
|
-
{% for
|
|
4
|
-
|
|
5
|
-
{
|
|
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 -%}
|