dbt-platform-helper 13.1.0__py3-none-any.whl → 15.16.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.
- dbt_platform_helper/COMMANDS.md +107 -27
- dbt_platform_helper/commands/application.py +5 -6
- dbt_platform_helper/commands/codebase.py +31 -10
- dbt_platform_helper/commands/conduit.py +3 -5
- dbt_platform_helper/commands/config.py +20 -311
- dbt_platform_helper/commands/copilot.py +18 -391
- dbt_platform_helper/commands/database.py +17 -9
- dbt_platform_helper/commands/environment.py +20 -14
- dbt_platform_helper/commands/generate.py +0 -3
- dbt_platform_helper/commands/internal.py +140 -0
- dbt_platform_helper/commands/notify.py +58 -78
- dbt_platform_helper/commands/pipeline.py +23 -19
- dbt_platform_helper/commands/secrets.py +39 -93
- dbt_platform_helper/commands/version.py +7 -12
- dbt_platform_helper/constants.py +52 -7
- dbt_platform_helper/domain/codebase.py +89 -39
- dbt_platform_helper/domain/conduit.py +335 -76
- dbt_platform_helper/domain/config.py +381 -0
- dbt_platform_helper/domain/copilot.py +398 -0
- dbt_platform_helper/domain/copilot_environment.py +8 -8
- dbt_platform_helper/domain/database_copy.py +2 -2
- dbt_platform_helper/domain/maintenance_page.py +254 -430
- dbt_platform_helper/domain/notify.py +64 -0
- dbt_platform_helper/domain/pipelines.py +43 -35
- dbt_platform_helper/domain/plans.py +41 -0
- dbt_platform_helper/domain/secrets.py +279 -0
- dbt_platform_helper/domain/service.py +570 -0
- dbt_platform_helper/domain/terraform_environment.py +14 -13
- dbt_platform_helper/domain/update_alb_rules.py +412 -0
- dbt_platform_helper/domain/versioning.py +249 -0
- dbt_platform_helper/{providers → entities}/platform_config_schema.py +75 -82
- dbt_platform_helper/entities/semantic_version.py +83 -0
- dbt_platform_helper/entities/service.py +339 -0
- dbt_platform_helper/platform_exception.py +4 -0
- dbt_platform_helper/providers/autoscaling.py +24 -0
- dbt_platform_helper/providers/aws/__init__.py +0 -0
- dbt_platform_helper/providers/aws/exceptions.py +70 -0
- dbt_platform_helper/providers/aws/interfaces.py +13 -0
- dbt_platform_helper/providers/aws/opensearch.py +23 -0
- dbt_platform_helper/providers/aws/redis.py +21 -0
- dbt_platform_helper/providers/aws/sso_auth.py +75 -0
- dbt_platform_helper/providers/cache.py +40 -4
- dbt_platform_helper/providers/cloudformation.py +1 -1
- dbt_platform_helper/providers/config.py +137 -19
- dbt_platform_helper/providers/config_validator.py +112 -51
- dbt_platform_helper/providers/copilot.py +24 -16
- dbt_platform_helper/providers/ecr.py +89 -7
- dbt_platform_helper/providers/ecs.py +228 -36
- dbt_platform_helper/providers/environment_variable.py +24 -0
- dbt_platform_helper/providers/files.py +1 -1
- dbt_platform_helper/providers/io.py +36 -4
- dbt_platform_helper/providers/kms.py +22 -0
- dbt_platform_helper/providers/load_balancers.py +402 -42
- dbt_platform_helper/providers/logs.py +72 -0
- dbt_platform_helper/providers/parameter_store.py +134 -0
- dbt_platform_helper/providers/s3.py +21 -0
- dbt_platform_helper/providers/schema_migrations/__init__.py +0 -0
- dbt_platform_helper/providers/schema_migrations/schema_v0_to_v1_migration.py +43 -0
- dbt_platform_helper/providers/schema_migrator.py +77 -0
- dbt_platform_helper/providers/secrets.py +5 -5
- dbt_platform_helper/providers/slack_channel_notifier.py +62 -0
- dbt_platform_helper/providers/terraform_manifest.py +121 -19
- dbt_platform_helper/providers/version.py +106 -23
- dbt_platform_helper/providers/version_status.py +27 -0
- dbt_platform_helper/providers/vpc.py +36 -5
- dbt_platform_helper/providers/yaml_file.py +58 -2
- dbt_platform_helper/templates/environment-pipelines/main.tf +4 -3
- dbt_platform_helper/templates/svc/overrides/cfn.patches.yml +5 -0
- dbt_platform_helper/utilities/decorators.py +103 -0
- dbt_platform_helper/utils/application.py +119 -22
- dbt_platform_helper/utils/aws.py +39 -150
- dbt_platform_helper/utils/deep_merge.py +10 -0
- dbt_platform_helper/utils/git.py +1 -14
- dbt_platform_helper/utils/validation.py +1 -1
- {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/METADATA +11 -20
- dbt_platform_helper-15.16.0.dist-info/RECORD +118 -0
- {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/WHEEL +1 -1
- platform_helper.py +3 -1
- terraform/elasticache-redis/plans.yml +85 -0
- terraform/opensearch/plans.yml +71 -0
- terraform/postgres/plans.yml +128 -0
- dbt_platform_helper/addon-plans.yml +0 -224
- dbt_platform_helper/providers/aws.py +0 -37
- dbt_platform_helper/providers/opensearch.py +0 -36
- dbt_platform_helper/providers/redis.py +0 -34
- dbt_platform_helper/providers/semantic_version.py +0 -126
- dbt_platform_helper/templates/svc/manifest-backend.yml +0 -69
- dbt_platform_helper/templates/svc/manifest-public.yml +0 -109
- dbt_platform_helper/utils/cloudfoundry.py +0 -14
- dbt_platform_helper/utils/files.py +0 -53
- dbt_platform_helper/utils/manifests.py +0 -18
- dbt_platform_helper/utils/versioning.py +0 -238
- dbt_platform_helper-13.1.0.dist-info/RECORD +0 -96
- {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/entry_points.txt +0 -0
- {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,65 +1,414 @@
|
|
|
1
|
-
import
|
|
1
|
+
import json
|
|
2
|
+
from typing import Dict
|
|
3
|
+
from typing import List
|
|
2
4
|
|
|
5
|
+
from boto3 import Session
|
|
6
|
+
|
|
7
|
+
from dbt_platform_helper.constants import MANAGED_BY_PLATFORM_TERRAFORM
|
|
8
|
+
from dbt_platform_helper.constants import ROUTED_TO_PLATFORM_MODES
|
|
3
9
|
from dbt_platform_helper.platform_exception import PlatformException
|
|
10
|
+
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
11
|
+
from dbt_platform_helper.providers.parameter_store import ParameterStore
|
|
12
|
+
from dbt_platform_helper.utils.aws import get_aws_session_or_abort
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalise_to_cidr(ip: str):
|
|
16
|
+
if "/" in ip:
|
|
17
|
+
return ip
|
|
18
|
+
SINGLE_IPV4_CIDR_PREFIX_LENGTH = "32"
|
|
19
|
+
return f"{ip}/{SINGLE_IPV4_CIDR_PREFIX_LENGTH}"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ALBDataNormaliser:
|
|
4
23
|
|
|
5
|
-
|
|
6
|
-
|
|
24
|
+
@staticmethod
|
|
25
|
+
def tags_to_dict(tags: List[Dict[str, str]]) -> Dict[str, str]:
|
|
26
|
+
return {tag.get("Key", ""): tag.get("Value", "") for tag in tags}
|
|
7
27
|
|
|
28
|
+
@staticmethod
|
|
29
|
+
def conditions_to_dict(conditions: List[Dict[str, List[str]]]) -> Dict[str, List[str]]:
|
|
30
|
+
return {condition.get("Field", ""): condition.get("Values", "") for condition in conditions}
|
|
8
31
|
|
|
9
|
-
def get_load_balancer_for_application(session: boto3.Session, app: str, env: str) -> str:
|
|
10
|
-
lb_client = session.client("elbv2")
|
|
11
32
|
|
|
12
|
-
|
|
13
|
-
load_balancers = [lb["LoadBalancerArn"] for lb in describe_response["LoadBalancers"]]
|
|
33
|
+
class LoadBalancerProvider:
|
|
14
34
|
|
|
15
|
-
|
|
35
|
+
def __init__(self, session: Session = None, io: ClickIOProvider = ClickIOProvider()):
|
|
36
|
+
self.session = session
|
|
37
|
+
self.evlb_client = self._get_client("elbv2")
|
|
38
|
+
self.rg_tagging_client = self._get_client("resourcegroupstaggingapi")
|
|
39
|
+
self.parameter_store_provider = ParameterStore(self._get_client("ssm"))
|
|
40
|
+
self.io = io
|
|
16
41
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
load_balancer_arn = lb["ResourceArn"]
|
|
42
|
+
def _get_client(self, client: str):
|
|
43
|
+
if not self.session:
|
|
44
|
+
self.session = get_aws_session_or_abort()
|
|
45
|
+
return self.session.client(client)
|
|
22
46
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
47
|
+
def find_target_group(self, app: str, env: str, svc: str) -> str:
|
|
48
|
+
|
|
49
|
+
# TODO once copilot is gone this is no longer needed
|
|
50
|
+
try:
|
|
51
|
+
result = self.parameter_store_provider.get_ssm_parameter_by_name(
|
|
52
|
+
f"/platform/applications/{app}/environments/{env}"
|
|
53
|
+
)["Value"]
|
|
54
|
+
env_config = json.loads(result)
|
|
55
|
+
service_deployment_mode = env_config["service_deployment_mode"]
|
|
56
|
+
except Exception:
|
|
57
|
+
service_deployment_mode = "copilot"
|
|
58
|
+
|
|
59
|
+
if service_deployment_mode in ROUTED_TO_PLATFORM_MODES:
|
|
60
|
+
application_key = "application"
|
|
61
|
+
environment_key = "environment"
|
|
62
|
+
service_key = "service"
|
|
63
|
+
else:
|
|
64
|
+
application_key = "copilot-application"
|
|
65
|
+
environment_key = "copilot-environment"
|
|
66
|
+
service_key = "copilot-service"
|
|
67
|
+
target_group_arn = None
|
|
68
|
+
|
|
69
|
+
paginator = self.rg_tagging_client.get_paginator("get_resources")
|
|
70
|
+
page_iterator = paginator.paginate(
|
|
71
|
+
TagFilters=[
|
|
72
|
+
{
|
|
73
|
+
"Key": application_key,
|
|
74
|
+
"Values": [
|
|
75
|
+
app,
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"Key": environment_key,
|
|
80
|
+
"Values": [
|
|
81
|
+
env,
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"Key": service_key,
|
|
86
|
+
"Values": [
|
|
87
|
+
svc,
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
ResourceTypeFilters=[
|
|
92
|
+
"elasticloadbalancing:targetgroup",
|
|
93
|
+
],
|
|
26
94
|
)
|
|
27
95
|
|
|
28
|
-
|
|
96
|
+
for page in page_iterator:
|
|
97
|
+
for resource in page["ResourceTagMappingList"]:
|
|
98
|
+
tags = {tag["Key"]: tag["Value"] for tag in resource["Tags"]}
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
tags.get(service_key) == svc
|
|
102
|
+
and tags.get(environment_key) == env
|
|
103
|
+
and tags.get(application_key) == app
|
|
104
|
+
):
|
|
105
|
+
target_group_arn = resource["ResourceARN"]
|
|
106
|
+
|
|
107
|
+
if not target_group_arn:
|
|
108
|
+
self.io.error(
|
|
109
|
+
f"No target group found for application: {app}, environment: {env}, service: {svc}",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return target_group_arn
|
|
113
|
+
|
|
114
|
+
def get_target_groups(self, target_group_arns: List[str]) -> List[dict]:
|
|
115
|
+
tgs = []
|
|
116
|
+
paginator = self.evlb_client.get_paginator("describe_target_groups")
|
|
117
|
+
page_iterator = paginator.paginate(TargetGroupArns=target_group_arns)
|
|
118
|
+
for page in page_iterator:
|
|
119
|
+
tgs.extend(page["TargetGroups"])
|
|
120
|
+
|
|
121
|
+
return tgs
|
|
122
|
+
|
|
123
|
+
def get_target_groups_with_tags(
|
|
124
|
+
self, target_group_arns: List[str], normalise: bool = True
|
|
125
|
+
) -> List[dict]:
|
|
126
|
+
target_groups = self.get_target_groups(target_group_arns)
|
|
127
|
+
|
|
128
|
+
tags = self.get_resources_tag_descriptions(target_groups, "TargetGroupArn")
|
|
129
|
+
|
|
130
|
+
tgs_with_tags = self.merge_in_tags_by_resource_arn(target_groups, tags, "TargetGroupArn")
|
|
131
|
+
|
|
132
|
+
if normalise:
|
|
133
|
+
for tg in tgs_with_tags:
|
|
134
|
+
tg["Tags"] = ALBDataNormaliser.tags_to_dict(tg["Tags"])
|
|
135
|
+
return tgs_with_tags
|
|
136
|
+
|
|
137
|
+
def get_https_certificate_for_listener(self, listener_arn: str, env: str):
|
|
138
|
+
certificates = []
|
|
139
|
+
paginator = self.evlb_client.get_paginator("describe_listener_certificates")
|
|
140
|
+
page_iterator = paginator.paginate(ListenerArn=listener_arn)
|
|
141
|
+
for page in page_iterator:
|
|
142
|
+
certificates.extend(page["Certificates"])
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
certificate_arn = next(c["CertificateArn"] for c in certificates if c["IsDefault"])
|
|
146
|
+
except StopIteration:
|
|
147
|
+
raise CertificateNotFoundException(env)
|
|
148
|
+
|
|
149
|
+
return certificate_arn
|
|
150
|
+
|
|
151
|
+
def get_https_certificate_for_application(self, app: str, env: str) -> str:
|
|
152
|
+
listener_arn = self.get_https_listener_for_application(app, env)
|
|
153
|
+
return self.get_https_certificate_for_listener(listener_arn, env)
|
|
154
|
+
|
|
155
|
+
def get_listeners_for_load_balancer(self, load_balancer_arn: str) -> List[dict]:
|
|
156
|
+
listeners = []
|
|
157
|
+
paginator = self.evlb_client.get_paginator("describe_listeners")
|
|
158
|
+
page_iterator = paginator.paginate(LoadBalancerArn=load_balancer_arn)
|
|
159
|
+
for page in page_iterator:
|
|
160
|
+
listeners.extend(page["Listeners"])
|
|
161
|
+
|
|
162
|
+
return listeners
|
|
163
|
+
|
|
164
|
+
def get_https_listener_for_application(self, app: str, env: str) -> str:
|
|
165
|
+
load_balancer_arn = self.get_load_balancer_for_application(app, env)
|
|
166
|
+
self.io.debug(f"Load Balancer ARN: {load_balancer_arn}")
|
|
167
|
+
listeners = self.get_listeners_for_load_balancer(load_balancer_arn)
|
|
168
|
+
|
|
169
|
+
listener_arn = None
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
listener_arn = next(l["ListenerArn"] for l in listeners if l["Protocol"] == "HTTPS")
|
|
173
|
+
except StopIteration:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
if not listener_arn:
|
|
177
|
+
raise ListenerNotFoundException(app, env)
|
|
178
|
+
|
|
179
|
+
return listener_arn
|
|
180
|
+
|
|
181
|
+
def get_load_balancers(self) -> List[dict]:
|
|
182
|
+
load_balancers = []
|
|
183
|
+
paginator = self.evlb_client.get_paginator("describe_load_balancers")
|
|
184
|
+
page_iterator = paginator.paginate()
|
|
185
|
+
for page in page_iterator:
|
|
186
|
+
load_balancers.extend(lb["LoadBalancerArn"] for lb in page["LoadBalancers"])
|
|
187
|
+
|
|
188
|
+
return load_balancers
|
|
189
|
+
|
|
190
|
+
def get_load_balancer_for_application(self, app: str, env: str) -> str:
|
|
191
|
+
load_balancers = self.get_load_balancers()
|
|
192
|
+
tag_descriptions = []
|
|
193
|
+
for i in range(0, len(load_balancers), 20):
|
|
194
|
+
chunk = load_balancers[i : i + 20]
|
|
195
|
+
tag_descriptions.extend(
|
|
196
|
+
self.evlb_client.describe_tags(ResourceArns=chunk)["TagDescriptions"]
|
|
197
|
+
) # describe_tags cannot be paginated - 04/04/2025
|
|
198
|
+
|
|
199
|
+
for lb in tag_descriptions:
|
|
200
|
+
tags = {t["Key"]: t["Value"] for t in lb["Tags"]}
|
|
201
|
+
# TODO: DBTP-1967: copilot hangover, creates coupling to specific tags could update to check application and environment
|
|
202
|
+
if (
|
|
203
|
+
tags.get("copilot-application") == app
|
|
204
|
+
and tags.get("copilot-environment") == env
|
|
205
|
+
and tags.get("managed-by", "") == MANAGED_BY_PLATFORM_TERRAFORM
|
|
206
|
+
):
|
|
207
|
+
return lb["ResourceArn"]
|
|
208
|
+
|
|
209
|
+
raise LoadBalancerNotFoundException(app, env)
|
|
210
|
+
|
|
211
|
+
def get_host_header_conditions(self, listener_arn: str, target_group_arn: str) -> list:
|
|
212
|
+
rules = []
|
|
213
|
+
paginator = self.evlb_client.get_paginator("describe_rules")
|
|
214
|
+
page_iterator = paginator.paginate(ListenerArn=listener_arn)
|
|
215
|
+
for page in page_iterator:
|
|
216
|
+
rules.extend(page["Rules"])
|
|
217
|
+
|
|
218
|
+
conditions = []
|
|
29
219
|
|
|
220
|
+
for rule in rules:
|
|
221
|
+
for action in rule["Actions"]:
|
|
222
|
+
if "TargetGroupArn" in action:
|
|
223
|
+
if action["Type"] == "forward" and action["TargetGroupArn"] == target_group_arn:
|
|
224
|
+
conditions = rule["Conditions"]
|
|
30
225
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
lb_client = session.client("elbv2")
|
|
34
|
-
listeners = lb_client.describe_listeners(LoadBalancerArn=load_balancer_arn)["Listeners"]
|
|
226
|
+
if not conditions:
|
|
227
|
+
raise ListenerRuleConditionsNotFoundException(listener_arn)
|
|
35
228
|
|
|
36
|
-
|
|
229
|
+
# filter to host-header conditions
|
|
230
|
+
conditions = [
|
|
231
|
+
{i: condition[i] for i in condition if i != "Values"}
|
|
232
|
+
for condition in conditions
|
|
233
|
+
if condition["Field"] == "host-header"
|
|
234
|
+
]
|
|
37
235
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
236
|
+
# remove internal hosts
|
|
237
|
+
conditions[0]["HostHeaderConfig"]["Values"] = [
|
|
238
|
+
v for v in conditions[0]["HostHeaderConfig"]["Values"]
|
|
239
|
+
]
|
|
42
240
|
|
|
43
|
-
|
|
44
|
-
raise ListenerNotFoundException(f"No HTTPS listener for {app} in the {env} environment")
|
|
241
|
+
return conditions
|
|
45
242
|
|
|
46
|
-
|
|
243
|
+
def get_rules_tag_descriptions_by_listener_arn(self, listener_arn: str) -> list:
|
|
244
|
+
rules = self.get_listener_rules_by_listener_arn(listener_arn)
|
|
47
245
|
|
|
246
|
+
return self.get_resources_tag_descriptions(rules)
|
|
48
247
|
|
|
49
|
-
def
|
|
248
|
+
def merge_in_tags_by_resource_arn(
|
|
249
|
+
self,
|
|
250
|
+
resources: List[dict],
|
|
251
|
+
tag_descriptions: List[dict],
|
|
252
|
+
resources_identifier: str = "RuleArn",
|
|
253
|
+
):
|
|
254
|
+
tags_by_resource_arn = {
|
|
255
|
+
rule_tags.get("ResourceArn"): rule_tags for rule_tags in tag_descriptions if rule_tags
|
|
256
|
+
}
|
|
257
|
+
for resource in resources:
|
|
258
|
+
tags = tags_by_resource_arn[resource[resources_identifier]]
|
|
259
|
+
resource.update(tags)
|
|
260
|
+
return resources
|
|
50
261
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
]
|
|
262
|
+
def get_rules_with_tags_by_listener_arn(
|
|
263
|
+
self, listener_arn: str, normalise: bool = True
|
|
264
|
+
) -> list:
|
|
265
|
+
rules = self.get_listener_rules_by_listener_arn(listener_arn)
|
|
56
266
|
|
|
57
|
-
|
|
58
|
-
certificate_arn = next(c["CertificateArn"] for c in certificates if c["IsDefault"])
|
|
59
|
-
except StopIteration:
|
|
60
|
-
raise CertificateNotFoundException(env)
|
|
267
|
+
tags = self.get_resources_tag_descriptions(rules)
|
|
61
268
|
|
|
62
|
-
|
|
269
|
+
rules_with_tags = self.merge_in_tags_by_resource_arn(rules, tags)
|
|
270
|
+
|
|
271
|
+
if normalise:
|
|
272
|
+
for rule in rules_with_tags:
|
|
273
|
+
rule["Conditions"] = ALBDataNormaliser.conditions_to_dict(rule["Conditions"])
|
|
274
|
+
rule["Tags"] = ALBDataNormaliser.tags_to_dict(rule["Tags"])
|
|
275
|
+
|
|
276
|
+
return rules_with_tags
|
|
277
|
+
|
|
278
|
+
def get_listener_rules_by_listener_arn(self, listener_arn: str) -> list:
|
|
279
|
+
rules = []
|
|
280
|
+
paginator = self.evlb_client.get_paginator("describe_rules")
|
|
281
|
+
page_iterator = paginator.paginate(ListenerArn=listener_arn)
|
|
282
|
+
for page in page_iterator:
|
|
283
|
+
rules.extend(page["Rules"])
|
|
284
|
+
|
|
285
|
+
return rules
|
|
286
|
+
|
|
287
|
+
def get_resources_tag_descriptions(
|
|
288
|
+
self, resources: list, resource_identifier: str = "RuleArn"
|
|
289
|
+
) -> list:
|
|
290
|
+
tag_descriptions = []
|
|
291
|
+
chunk_size = 20
|
|
292
|
+
|
|
293
|
+
for i in range(0, len(resources), chunk_size):
|
|
294
|
+
chunk = resources[i : i + chunk_size]
|
|
295
|
+
resource_arns = [r[resource_identifier] for r in chunk]
|
|
296
|
+
response = self.evlb_client.describe_tags(
|
|
297
|
+
ResourceArns=resource_arns
|
|
298
|
+
) # describe_tags cannot be paginated - 04/04/2025
|
|
299
|
+
tag_descriptions.extend(response["TagDescriptions"])
|
|
300
|
+
|
|
301
|
+
return tag_descriptions
|
|
302
|
+
|
|
303
|
+
def create_rule(
|
|
304
|
+
self,
|
|
305
|
+
listener_arn: str,
|
|
306
|
+
actions: list,
|
|
307
|
+
conditions: list,
|
|
308
|
+
priority: int,
|
|
309
|
+
tags: list,
|
|
310
|
+
):
|
|
311
|
+
return self.evlb_client.create_rule(
|
|
312
|
+
ListenerArn=listener_arn,
|
|
313
|
+
Priority=priority,
|
|
314
|
+
Conditions=conditions,
|
|
315
|
+
Actions=actions,
|
|
316
|
+
Tags=tags,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def create_forward_rule(
|
|
320
|
+
self,
|
|
321
|
+
listener_arn: str,
|
|
322
|
+
target_group_arn: str,
|
|
323
|
+
rule_name: str,
|
|
324
|
+
priority: int,
|
|
325
|
+
conditions: list,
|
|
326
|
+
additional_tags: list = [],
|
|
327
|
+
):
|
|
328
|
+
return self.create_rule(
|
|
329
|
+
listener_arn=listener_arn,
|
|
330
|
+
priority=priority,
|
|
331
|
+
conditions=conditions,
|
|
332
|
+
actions=[{"Type": "forward", "TargetGroupArn": target_group_arn}],
|
|
333
|
+
tags=[{"Key": "name", "Value": rule_name}, *additional_tags],
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def create_header_rule(
|
|
337
|
+
self,
|
|
338
|
+
listener_arn: str,
|
|
339
|
+
target_group_arn: str,
|
|
340
|
+
header_name: str,
|
|
341
|
+
values: list,
|
|
342
|
+
rule_name: str,
|
|
343
|
+
priority: int,
|
|
344
|
+
conditions: list,
|
|
345
|
+
additional_tags: list = [],
|
|
346
|
+
):
|
|
347
|
+
|
|
348
|
+
combined_conditions = [
|
|
349
|
+
{
|
|
350
|
+
"Field": "http-header",
|
|
351
|
+
"HttpHeaderConfig": {"HttpHeaderName": header_name, "Values": values},
|
|
352
|
+
}
|
|
353
|
+
] + conditions
|
|
354
|
+
|
|
355
|
+
self.create_forward_rule(
|
|
356
|
+
listener_arn,
|
|
357
|
+
target_group_arn,
|
|
358
|
+
rule_name,
|
|
359
|
+
priority,
|
|
360
|
+
combined_conditions,
|
|
361
|
+
additional_tags,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
self.io.debug(
|
|
365
|
+
f"Creating listener rule {rule_name} for HTTPS Listener with arn {listener_arn}.\nIf request header {header_name} contains one of the values {values}, the request will be forwarded to target group with arn {target_group_arn}.\n\n",
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
def create_source_ip_rule(
|
|
369
|
+
self,
|
|
370
|
+
listener_arn: str,
|
|
371
|
+
target_group_arn: str,
|
|
372
|
+
values: list,
|
|
373
|
+
rule_name: str,
|
|
374
|
+
priority: int,
|
|
375
|
+
conditions: list,
|
|
376
|
+
additional_tags: list = [],
|
|
377
|
+
):
|
|
378
|
+
combined_conditions = [
|
|
379
|
+
{
|
|
380
|
+
"Field": "source-ip",
|
|
381
|
+
"SourceIpConfig": {"Values": [normalise_to_cidr(value) for value in values]},
|
|
382
|
+
}
|
|
383
|
+
] + conditions
|
|
384
|
+
|
|
385
|
+
self.create_forward_rule(
|
|
386
|
+
listener_arn,
|
|
387
|
+
target_group_arn,
|
|
388
|
+
rule_name,
|
|
389
|
+
priority,
|
|
390
|
+
combined_conditions,
|
|
391
|
+
additional_tags,
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
self.io.debug(
|
|
395
|
+
f"Creating listener rule {rule_name} for HTTPS Listener with arn {listener_arn}.\nIf request source ip matches one of the values {values}, the request will be forwarded to target group with arn {target_group_arn}.\n\n",
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
def delete_listener_rule_by_tags(self, tag_descriptions: list, tag_name: str) -> list:
|
|
399
|
+
deleted_rules = []
|
|
400
|
+
|
|
401
|
+
for description in tag_descriptions:
|
|
402
|
+
tags = {t["Key"]: t["Value"] for t in description["Tags"]}
|
|
403
|
+
if tags.get("name") == tag_name:
|
|
404
|
+
if description["ResourceArn"]:
|
|
405
|
+
self.evlb_client.delete_rule(RuleArn=description["ResourceArn"])
|
|
406
|
+
deleted_rules.append(description)
|
|
407
|
+
|
|
408
|
+
return deleted_rules
|
|
409
|
+
|
|
410
|
+
def delete_listener_rule_by_resource_arn(self, resource_arn: str) -> list:
|
|
411
|
+
return self.evlb_client.delete_rule(RuleArn=resource_arn)
|
|
63
412
|
|
|
64
413
|
|
|
65
414
|
class LoadBalancerException(PlatformException):
|
|
@@ -67,17 +416,28 @@ class LoadBalancerException(PlatformException):
|
|
|
67
416
|
|
|
68
417
|
|
|
69
418
|
class LoadBalancerNotFoundException(LoadBalancerException):
|
|
70
|
-
|
|
419
|
+
def __init__(self, application_name, env):
|
|
420
|
+
super().__init__(
|
|
421
|
+
f"No load balancer found for environment {env} in the application {application_name}."
|
|
422
|
+
)
|
|
71
423
|
|
|
72
424
|
|
|
73
425
|
class ListenerNotFoundException(LoadBalancerException):
|
|
74
|
-
|
|
426
|
+
def __init__(self, application_name, env):
|
|
427
|
+
super().__init__(
|
|
428
|
+
f"No HTTPS listener found for environment {env} in the application {application_name}."
|
|
429
|
+
)
|
|
75
430
|
|
|
76
431
|
|
|
77
432
|
class ListenerRuleNotFoundException(LoadBalancerException):
|
|
78
433
|
pass
|
|
79
434
|
|
|
80
435
|
|
|
436
|
+
class ListenerRuleConditionsNotFoundException(LoadBalancerException):
|
|
437
|
+
def __init__(self, listener_arn):
|
|
438
|
+
super().__init__(f"No listener rule conditions found for listener ARN: {listener_arn}")
|
|
439
|
+
|
|
440
|
+
|
|
81
441
|
class CertificateNotFoundException(PlatformException):
|
|
82
442
|
def __init__(self, environment_name: str):
|
|
83
443
|
super().__init__(
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import boto3
|
|
5
|
+
from botocore.exceptions import ClientError
|
|
6
|
+
|
|
7
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LogsProvider:
|
|
11
|
+
|
|
12
|
+
def __init__(self, client: boto3.client):
|
|
13
|
+
self.client = client
|
|
14
|
+
|
|
15
|
+
def check_log_streams_present(self, log_group: str, expected_log_streams: list[str]) -> bool:
|
|
16
|
+
"""
|
|
17
|
+
Check whether the logs streams provided exist or not.
|
|
18
|
+
|
|
19
|
+
Retry for up to 1 minute.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
found_log_streams = set()
|
|
23
|
+
expected_log_streams = set(expected_log_streams)
|
|
24
|
+
timeout_seconds = 60
|
|
25
|
+
poll_interval_seconds = 5
|
|
26
|
+
deadline_seconds = time.monotonic() + timeout_seconds
|
|
27
|
+
|
|
28
|
+
while time.monotonic() < deadline_seconds:
|
|
29
|
+
|
|
30
|
+
remaining_log_streams = expected_log_streams - found_log_streams
|
|
31
|
+
if not remaining_log_streams:
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
for log_stream in list(remaining_log_streams):
|
|
35
|
+
try:
|
|
36
|
+
response = self.client.describe_log_streams(
|
|
37
|
+
logGroupName=log_group, logStreamNamePrefix=log_stream, limit=1
|
|
38
|
+
)
|
|
39
|
+
except ClientError as e:
|
|
40
|
+
code = e.response.get("Error", {}).get("Code")
|
|
41
|
+
if code == "ResourceNotFoundException":
|
|
42
|
+
continue # Log stream not there yet, keep going
|
|
43
|
+
else:
|
|
44
|
+
raise PlatformException(
|
|
45
|
+
f"Failed to check if log stream '{log_stream}' exists due to an error {e}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
for ls in response.get("logStreams", []):
|
|
49
|
+
if ls.get("logStreamName") == log_stream:
|
|
50
|
+
found_log_streams.add(log_stream)
|
|
51
|
+
|
|
52
|
+
if expected_log_streams - found_log_streams:
|
|
53
|
+
time.sleep(poll_interval_seconds)
|
|
54
|
+
|
|
55
|
+
missing_log_streams = expected_log_streams - found_log_streams
|
|
56
|
+
raise PlatformException(
|
|
57
|
+
f"Timed out waiting for the following log streams to create: {missing_log_streams}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def get_log_stream_events(
|
|
61
|
+
self, log_group: str, log_stream: str, limit: int
|
|
62
|
+
) -> list[dict[str, Any]]:
|
|
63
|
+
"""Return events for a specific log stream."""
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
self.check_log_streams_present(log_group=log_group, expected_log_streams=[log_stream])
|
|
67
|
+
response = self.client.get_log_events(
|
|
68
|
+
logGroupName=log_group, logStreamName=log_stream, limit=limit
|
|
69
|
+
)
|
|
70
|
+
return response["events"]
|
|
71
|
+
except ClientError as err:
|
|
72
|
+
raise PlatformException(f"Error retrieving log stream events: {err}")
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from dataclasses import field
|
|
3
|
+
from typing import Literal
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from typing import Union
|
|
6
|
+
|
|
7
|
+
import boto3
|
|
8
|
+
|
|
9
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class Parameter:
|
|
14
|
+
|
|
15
|
+
name: str
|
|
16
|
+
value: str
|
|
17
|
+
arn: str = None
|
|
18
|
+
data_type: Literal["text", "aws:ec2:image"] = "text"
|
|
19
|
+
type: Literal["String", "StringList", "SecureString"] = (
|
|
20
|
+
"SecureString" # Returned as 'Type' from AWS
|
|
21
|
+
)
|
|
22
|
+
version: int = None
|
|
23
|
+
tags: Optional[dict[str, str]] = field(default_factory=dict)
|
|
24
|
+
|
|
25
|
+
def tags_to_list(self):
|
|
26
|
+
return [{"Key": tag, "Value": value} for tag, value in self.tags.items()]
|
|
27
|
+
|
|
28
|
+
def __str__(self):
|
|
29
|
+
output = f"Application {self.name} with"
|
|
30
|
+
|
|
31
|
+
return f"{output} no environments"
|
|
32
|
+
|
|
33
|
+
def __eq__(self, other: "Parameter"):
|
|
34
|
+
return str(self.name) == str(other.name)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class ParameterStore:
|
|
38
|
+
def __init__(self, ssm_client: boto3.client, with_model: bool = False):
|
|
39
|
+
self.ssm_client = ssm_client
|
|
40
|
+
self.with_model = with_model
|
|
41
|
+
|
|
42
|
+
def __fetch_tags(self, parameter: Parameter, normalise=True):
|
|
43
|
+
response = self.ssm_client.list_tags_for_resource(
|
|
44
|
+
ResourceType="Parameter", ResourceId=parameter.name
|
|
45
|
+
)["TagList"]
|
|
46
|
+
|
|
47
|
+
if normalise:
|
|
48
|
+
return {tag["Key"]: tag["Value"] for tag in response}
|
|
49
|
+
else:
|
|
50
|
+
return response
|
|
51
|
+
|
|
52
|
+
def get_ssm_parameter_by_name(
|
|
53
|
+
self, parameter_name: str, add_tags: bool = False
|
|
54
|
+
) -> Union[dict, Parameter]:
|
|
55
|
+
"""
|
|
56
|
+
Retrieves the latest version of a parameter from parameter store for a
|
|
57
|
+
given name/arn.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
parameter_name (str): The parameter name to retrieve the parameter value for.
|
|
61
|
+
add_tags (bool): Whether to retrieve the tags for the SSM parameters requested
|
|
62
|
+
Returns:
|
|
63
|
+
dict: A dictionary representation of your ssm parameter
|
|
64
|
+
"""
|
|
65
|
+
parameter = self.ssm_client.get_parameter(Name=parameter_name, WithDecryption=True)[
|
|
66
|
+
"Parameter"
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
if not self.with_model:
|
|
70
|
+
return parameter
|
|
71
|
+
|
|
72
|
+
model = Parameter(
|
|
73
|
+
name=parameter["Name"],
|
|
74
|
+
value=parameter["Value"],
|
|
75
|
+
arn=parameter["ARN"],
|
|
76
|
+
data_type=parameter["DataType"],
|
|
77
|
+
type=parameter["Type"],
|
|
78
|
+
version=parameter["Version"],
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if add_tags:
|
|
82
|
+
model.tags = self.__fetch_tags(model)
|
|
83
|
+
|
|
84
|
+
return model
|
|
85
|
+
|
|
86
|
+
def get_ssm_parameters_by_path(
|
|
87
|
+
self, path: str, add_tags: bool = False
|
|
88
|
+
) -> Union[list[dict], list[Parameter]]:
|
|
89
|
+
"""
|
|
90
|
+
Retrieves all SSM parameters for a given path from parameter store.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
path (str): The parameter path to retrieve the parameters for. e.g. /copilot/applications/
|
|
94
|
+
add_tags (bool): Whether to retrieve the tags for the SSM parameters requested
|
|
95
|
+
Returns:
|
|
96
|
+
list: A list of dictionaries containing all SSM parameters under the provided path.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
parameters = []
|
|
100
|
+
paginator = self.ssm_client.get_paginator("get_parameters_by_path")
|
|
101
|
+
page_iterator = paginator.paginate(Path=path, Recursive=True, WithDecryption=True)
|
|
102
|
+
|
|
103
|
+
for page in page_iterator:
|
|
104
|
+
parameters.extend(page.get("Parameters", []))
|
|
105
|
+
|
|
106
|
+
if not self.with_model:
|
|
107
|
+
if parameters:
|
|
108
|
+
return parameters
|
|
109
|
+
else:
|
|
110
|
+
raise ParameterNotFoundForPathException()
|
|
111
|
+
|
|
112
|
+
to_model = lambda parameter: Parameter(
|
|
113
|
+
name=parameter["Name"],
|
|
114
|
+
value=parameter["Value"],
|
|
115
|
+
arn=parameter["ARN"],
|
|
116
|
+
data_type=parameter["DataType"],
|
|
117
|
+
type=parameter["Type"],
|
|
118
|
+
version=parameter["Version"],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
models = [to_model(param) for param in parameters]
|
|
122
|
+
|
|
123
|
+
if add_tags:
|
|
124
|
+
for model in models:
|
|
125
|
+
model.tags = self.__fetch_tags(model)
|
|
126
|
+
|
|
127
|
+
return models
|
|
128
|
+
|
|
129
|
+
def put_parameter(self, data_dict: dict) -> dict:
|
|
130
|
+
return self.ssm_client.put_parameter(**data_dict)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class ParameterNotFoundForPathException(PlatformException):
|
|
134
|
+
"""Exception raised when no parameters are found for a given path."""
|