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.
Files changed (95) hide show
  1. dbt_platform_helper/COMMANDS.md +107 -27
  2. dbt_platform_helper/commands/application.py +5 -6
  3. dbt_platform_helper/commands/codebase.py +31 -10
  4. dbt_platform_helper/commands/conduit.py +3 -5
  5. dbt_platform_helper/commands/config.py +20 -311
  6. dbt_platform_helper/commands/copilot.py +18 -391
  7. dbt_platform_helper/commands/database.py +17 -9
  8. dbt_platform_helper/commands/environment.py +20 -14
  9. dbt_platform_helper/commands/generate.py +0 -3
  10. dbt_platform_helper/commands/internal.py +140 -0
  11. dbt_platform_helper/commands/notify.py +58 -78
  12. dbt_platform_helper/commands/pipeline.py +23 -19
  13. dbt_platform_helper/commands/secrets.py +39 -93
  14. dbt_platform_helper/commands/version.py +7 -12
  15. dbt_platform_helper/constants.py +52 -7
  16. dbt_platform_helper/domain/codebase.py +89 -39
  17. dbt_platform_helper/domain/conduit.py +335 -76
  18. dbt_platform_helper/domain/config.py +381 -0
  19. dbt_platform_helper/domain/copilot.py +398 -0
  20. dbt_platform_helper/domain/copilot_environment.py +8 -8
  21. dbt_platform_helper/domain/database_copy.py +2 -2
  22. dbt_platform_helper/domain/maintenance_page.py +254 -430
  23. dbt_platform_helper/domain/notify.py +64 -0
  24. dbt_platform_helper/domain/pipelines.py +43 -35
  25. dbt_platform_helper/domain/plans.py +41 -0
  26. dbt_platform_helper/domain/secrets.py +279 -0
  27. dbt_platform_helper/domain/service.py +570 -0
  28. dbt_platform_helper/domain/terraform_environment.py +14 -13
  29. dbt_platform_helper/domain/update_alb_rules.py +412 -0
  30. dbt_platform_helper/domain/versioning.py +249 -0
  31. dbt_platform_helper/{providers → entities}/platform_config_schema.py +75 -82
  32. dbt_platform_helper/entities/semantic_version.py +83 -0
  33. dbt_platform_helper/entities/service.py +339 -0
  34. dbt_platform_helper/platform_exception.py +4 -0
  35. dbt_platform_helper/providers/autoscaling.py +24 -0
  36. dbt_platform_helper/providers/aws/__init__.py +0 -0
  37. dbt_platform_helper/providers/aws/exceptions.py +70 -0
  38. dbt_platform_helper/providers/aws/interfaces.py +13 -0
  39. dbt_platform_helper/providers/aws/opensearch.py +23 -0
  40. dbt_platform_helper/providers/aws/redis.py +21 -0
  41. dbt_platform_helper/providers/aws/sso_auth.py +75 -0
  42. dbt_platform_helper/providers/cache.py +40 -4
  43. dbt_platform_helper/providers/cloudformation.py +1 -1
  44. dbt_platform_helper/providers/config.py +137 -19
  45. dbt_platform_helper/providers/config_validator.py +112 -51
  46. dbt_platform_helper/providers/copilot.py +24 -16
  47. dbt_platform_helper/providers/ecr.py +89 -7
  48. dbt_platform_helper/providers/ecs.py +228 -36
  49. dbt_platform_helper/providers/environment_variable.py +24 -0
  50. dbt_platform_helper/providers/files.py +1 -1
  51. dbt_platform_helper/providers/io.py +36 -4
  52. dbt_platform_helper/providers/kms.py +22 -0
  53. dbt_platform_helper/providers/load_balancers.py +402 -42
  54. dbt_platform_helper/providers/logs.py +72 -0
  55. dbt_platform_helper/providers/parameter_store.py +134 -0
  56. dbt_platform_helper/providers/s3.py +21 -0
  57. dbt_platform_helper/providers/schema_migrations/__init__.py +0 -0
  58. dbt_platform_helper/providers/schema_migrations/schema_v0_to_v1_migration.py +43 -0
  59. dbt_platform_helper/providers/schema_migrator.py +77 -0
  60. dbt_platform_helper/providers/secrets.py +5 -5
  61. dbt_platform_helper/providers/slack_channel_notifier.py +62 -0
  62. dbt_platform_helper/providers/terraform_manifest.py +121 -19
  63. dbt_platform_helper/providers/version.py +106 -23
  64. dbt_platform_helper/providers/version_status.py +27 -0
  65. dbt_platform_helper/providers/vpc.py +36 -5
  66. dbt_platform_helper/providers/yaml_file.py +58 -2
  67. dbt_platform_helper/templates/environment-pipelines/main.tf +4 -3
  68. dbt_platform_helper/templates/svc/overrides/cfn.patches.yml +5 -0
  69. dbt_platform_helper/utilities/decorators.py +103 -0
  70. dbt_platform_helper/utils/application.py +119 -22
  71. dbt_platform_helper/utils/aws.py +39 -150
  72. dbt_platform_helper/utils/deep_merge.py +10 -0
  73. dbt_platform_helper/utils/git.py +1 -14
  74. dbt_platform_helper/utils/validation.py +1 -1
  75. {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/METADATA +11 -20
  76. dbt_platform_helper-15.16.0.dist-info/RECORD +118 -0
  77. {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/WHEEL +1 -1
  78. platform_helper.py +3 -1
  79. terraform/elasticache-redis/plans.yml +85 -0
  80. terraform/opensearch/plans.yml +71 -0
  81. terraform/postgres/plans.yml +128 -0
  82. dbt_platform_helper/addon-plans.yml +0 -224
  83. dbt_platform_helper/providers/aws.py +0 -37
  84. dbt_platform_helper/providers/opensearch.py +0 -36
  85. dbt_platform_helper/providers/redis.py +0 -34
  86. dbt_platform_helper/providers/semantic_version.py +0 -126
  87. dbt_platform_helper/templates/svc/manifest-backend.yml +0 -69
  88. dbt_platform_helper/templates/svc/manifest-public.yml +0 -109
  89. dbt_platform_helper/utils/cloudfoundry.py +0 -14
  90. dbt_platform_helper/utils/files.py +0 -53
  91. dbt_platform_helper/utils/manifests.py +0 -18
  92. dbt_platform_helper/utils/versioning.py +0 -238
  93. dbt_platform_helper-13.1.0.dist-info/RECORD +0 -96
  94. {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/entry_points.txt +0 -0
  95. {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 boto3
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
- # 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.
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
- describe_response = lb_client.describe_load_balancers()
13
- load_balancers = [lb["LoadBalancerArn"] for lb in describe_response["LoadBalancers"]]
33
+ class LoadBalancerProvider:
14
34
 
15
- load_balancers = lb_client.describe_tags(ResourceArns=load_balancers)["TagDescriptions"]
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
- load_balancer_arn = None
18
- for lb in load_balancers:
19
- tags = {t["Key"]: t["Value"] for t in lb["Tags"]}
20
- if tags.get("copilot-application") == app and tags.get("copilot-environment") == env:
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
- if not load_balancer_arn:
24
- raise LoadBalancerNotFoundException(
25
- f"No load balancer found for {app} in the {env} environment"
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
- return load_balancer_arn
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
- def get_https_listener_for_application(session: boto3.Session, app: str, env: str) -> str:
32
- load_balancer_arn = get_load_balancer_for_application(session, app, env)
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
- listener_arn = None
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
- try:
39
- listener_arn = next(l["ListenerArn"] for l in listeners if l["Protocol"] == "HTTPS")
40
- except StopIteration:
41
- pass
236
+ # remove internal hosts
237
+ conditions[0]["HostHeaderConfig"]["Values"] = [
238
+ v for v in conditions[0]["HostHeaderConfig"]["Values"]
239
+ ]
42
240
 
43
- if not listener_arn:
44
- raise ListenerNotFoundException(f"No HTTPS listener for {app} in the {env} environment")
241
+ return conditions
45
242
 
46
- return listener_arn
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 get_https_certificate_for_application(session: boto3.Session, app: str, env: str) -> str:
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
- listener_arn = get_https_listener_for_application(session, app, env)
52
- cert_client = session.client("elbv2")
53
- certificates = cert_client.describe_listener_certificates(ListenerArn=listener_arn)[
54
- "Certificates"
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
- try:
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
- return certificate_arn
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
- pass
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
- pass
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."""