dbt-platform-helper 15.3.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 +36 -11
- dbt_platform_helper/commands/application.py +2 -1
- dbt_platform_helper/commands/conduit.py +1 -1
- dbt_platform_helper/commands/environment.py +12 -1
- dbt_platform_helper/commands/generate.py +0 -2
- dbt_platform_helper/commands/internal.py +140 -0
- dbt_platform_helper/commands/pipeline.py +15 -3
- dbt_platform_helper/commands/secrets.py +37 -89
- dbt_platform_helper/commands/version.py +3 -2
- dbt_platform_helper/constants.py +38 -2
- dbt_platform_helper/domain/conduit.py +22 -9
- dbt_platform_helper/domain/config.py +30 -1
- dbt_platform_helper/domain/database_copy.py +1 -1
- dbt_platform_helper/domain/maintenance_page.py +27 -3
- dbt_platform_helper/domain/pipelines.py +36 -60
- dbt_platform_helper/domain/secrets.py +279 -0
- dbt_platform_helper/domain/service.py +570 -0
- dbt_platform_helper/domain/terraform_environment.py +7 -29
- dbt_platform_helper/domain/update_alb_rules.py +412 -0
- dbt_platform_helper/domain/versioning.py +124 -13
- dbt_platform_helper/entities/platform_config_schema.py +31 -11
- dbt_platform_helper/entities/semantic_version.py +2 -0
- dbt_platform_helper/entities/service.py +339 -0
- dbt_platform_helper/providers/autoscaling.py +24 -0
- dbt_platform_helper/providers/aws/exceptions.py +5 -0
- dbt_platform_helper/providers/aws/sso_auth.py +14 -0
- dbt_platform_helper/providers/config.py +17 -2
- dbt_platform_helper/providers/config_validator.py +87 -2
- dbt_platform_helper/providers/ecs.py +131 -11
- dbt_platform_helper/providers/environment_variable.py +2 -2
- dbt_platform_helper/providers/io.py +9 -2
- dbt_platform_helper/providers/load_balancers.py +122 -16
- dbt_platform_helper/providers/logs.py +72 -0
- dbt_platform_helper/providers/parameter_store.py +97 -10
- dbt_platform_helper/providers/s3.py +21 -0
- dbt_platform_helper/providers/terraform_manifest.py +97 -13
- dbt_platform_helper/providers/vpc.py +36 -5
- dbt_platform_helper/providers/yaml_file.py +35 -0
- dbt_platform_helper/templates/environment-pipelines/main.tf +3 -2
- dbt_platform_helper/templates/svc/overrides/cfn.patches.yml +5 -0
- dbt_platform_helper/utils/application.py +104 -21
- dbt_platform_helper/utils/aws.py +11 -10
- dbt_platform_helper/utils/deep_merge.py +10 -0
- dbt_platform_helper/utils/git.py +1 -1
- {dbt_platform_helper-15.3.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/METADATA +8 -17
- {dbt_platform_helper-15.3.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/RECORD +50 -41
- {dbt_platform_helper-15.3.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/WHEEL +1 -1
- platform_helper.py +2 -0
- {dbt_platform_helper-15.3.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/entry_points.txt +0 -0
- {dbt_platform_helper-15.3.0.dist-info → dbt_platform_helper-15.16.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import random
|
|
2
2
|
import string
|
|
3
3
|
import subprocess
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Any
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from botocore.exceptions import ClientError
|
|
5
8
|
|
|
6
9
|
from dbt_platform_helper.platform_exception import PlatformException
|
|
7
10
|
from dbt_platform_helper.platform_exception import ValidationException
|
|
@@ -39,7 +42,7 @@ class ECS:
|
|
|
39
42
|
container_name: str,
|
|
40
43
|
task_def_arn: str,
|
|
41
44
|
vpc_config: Vpc,
|
|
42
|
-
env_vars:
|
|
45
|
+
env_vars: list[dict] = None,
|
|
43
46
|
):
|
|
44
47
|
container_override = {"name": container_name}
|
|
45
48
|
if env_vars:
|
|
@@ -107,13 +110,31 @@ class ECS:
|
|
|
107
110
|
random_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=12))
|
|
108
111
|
return f"conduit-{self.application_name}-{self.env}-{addon_name}-{random_id}"
|
|
109
112
|
|
|
110
|
-
def get_ecs_task_arns(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
113
|
+
def get_ecs_task_arns(
|
|
114
|
+
self,
|
|
115
|
+
cluster: str,
|
|
116
|
+
max_results: int = 100,
|
|
117
|
+
desired_status: str = "RUNNING",
|
|
118
|
+
service_name: Optional[str] = None,
|
|
119
|
+
started_by: Optional[str] = None,
|
|
120
|
+
task_def_family: Optional[str] = None,
|
|
121
|
+
) -> list[str]:
|
|
122
|
+
"""Returns the ECS task ARNs based on the parameters provided."""
|
|
123
|
+
|
|
124
|
+
params = {
|
|
125
|
+
"cluster": cluster,
|
|
126
|
+
"maxResults": max_results,
|
|
127
|
+
"desiredStatus": desired_status,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if service_name:
|
|
131
|
+
params["serviceName"] = service_name
|
|
132
|
+
if started_by:
|
|
133
|
+
params["startedBy"] = started_by
|
|
134
|
+
if task_def_family:
|
|
135
|
+
params["family"] = task_def_family
|
|
136
|
+
|
|
137
|
+
tasks = self.ecs_client.list_tasks(**params)
|
|
117
138
|
|
|
118
139
|
if not tasks["taskArns"]:
|
|
119
140
|
return []
|
|
@@ -137,7 +158,7 @@ class ECS:
|
|
|
137
158
|
exceptions_to_catch=(ECSException,),
|
|
138
159
|
message_on_false="ECS Agent Not running",
|
|
139
160
|
)
|
|
140
|
-
def ecs_exec_is_available(self, cluster_arn: str, task_arns:
|
|
161
|
+
def ecs_exec_is_available(self, cluster_arn: str, task_arns: list[str]) -> bool:
|
|
141
162
|
"""
|
|
142
163
|
Checks if the ExecuteCommandAgent is running on the specified ECS task.
|
|
143
164
|
|
|
@@ -168,7 +189,106 @@ class ECS:
|
|
|
168
189
|
message_on_false="ECS task did not register in time",
|
|
169
190
|
)
|
|
170
191
|
def wait_for_task_to_register(self, cluster_arn: str, task_family: str) -> list[str]:
|
|
171
|
-
task_arns = self.get_ecs_task_arns(cluster_arn, task_family)
|
|
192
|
+
task_arns = self.get_ecs_task_arns(cluster=cluster_arn, task_def_family=task_family)
|
|
172
193
|
if task_arns:
|
|
173
194
|
return task_arns
|
|
174
195
|
return False
|
|
196
|
+
|
|
197
|
+
def get_service_deployment_state(
|
|
198
|
+
self, cluster_name: str, service_name: str, start_time: float
|
|
199
|
+
) -> tuple[Optional[str], Optional[str]]:
|
|
200
|
+
"""
|
|
201
|
+
Returns status & statusReason for the deployment of an ECS service.
|
|
202
|
+
|
|
203
|
+
statusReason can be:
|
|
204
|
+
PENDING | SUCCESSFUL | STOPPED | STOP_REQUESTED |
|
|
205
|
+
IN_PROGRESS | ROLLBACK_REQUESTED | ROLLBACK_IN_PROGRESS |
|
|
206
|
+
ROLLBACK_SUCCESSFUL | ROLLBACK_FAILED
|
|
207
|
+
"""
|
|
208
|
+
resp = self.ecs_client.list_service_deployments(
|
|
209
|
+
cluster=cluster_name, service=service_name, createdAt={"after": start_time}
|
|
210
|
+
)
|
|
211
|
+
deployments = resp.get("serviceDeployments", [])
|
|
212
|
+
|
|
213
|
+
if not deployments:
|
|
214
|
+
return None, f"No deployments found for '{service_name}'"
|
|
215
|
+
|
|
216
|
+
return deployments[0].get("status"), deployments[0].get("statusReason")
|
|
217
|
+
|
|
218
|
+
def get_container_names_from_ecs_tasks(
|
|
219
|
+
self, cluster_name: str, task_ids: list[str]
|
|
220
|
+
) -> list[str]:
|
|
221
|
+
"""Retrieve container names from each ECS task provided."""
|
|
222
|
+
|
|
223
|
+
response = self.ecs_client.describe_tasks(cluster=cluster_name, tasks=task_ids)
|
|
224
|
+
|
|
225
|
+
names = []
|
|
226
|
+
for task in response.get("tasks", []):
|
|
227
|
+
for container in task.get("containers", []):
|
|
228
|
+
if container["name"] not in names:
|
|
229
|
+
names.append(container["name"])
|
|
230
|
+
return names
|
|
231
|
+
|
|
232
|
+
def register_task_definition(
|
|
233
|
+
self,
|
|
234
|
+
service: str,
|
|
235
|
+
task_definition: dict,
|
|
236
|
+
image_tag: Optional[str] = None,
|
|
237
|
+
) -> str:
|
|
238
|
+
"""Register a new task definition revision using provided model and
|
|
239
|
+
containerDefinitions."""
|
|
240
|
+
|
|
241
|
+
for container in task_definition["containerDefinitions"]:
|
|
242
|
+
if container["name"] == service:
|
|
243
|
+
container["image"] = f"{container['image']}:{image_tag}"
|
|
244
|
+
break
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
task_definition_response = self.ecs_client.register_task_definition(**task_definition)
|
|
248
|
+
return task_definition_response["taskDefinition"]["taskDefinitionArn"]
|
|
249
|
+
except ClientError as err:
|
|
250
|
+
raise PlatformException(f"Error registering task definition: {err}")
|
|
251
|
+
|
|
252
|
+
def update_service(
|
|
253
|
+
self,
|
|
254
|
+
service: str,
|
|
255
|
+
task_def_arn: str,
|
|
256
|
+
environment: str,
|
|
257
|
+
application: str,
|
|
258
|
+
desired_count: int,
|
|
259
|
+
) -> dict[str, Any]:
|
|
260
|
+
"""Update an ECS service and return the response."""
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
service_response = self.ecs_client.update_service(
|
|
264
|
+
cluster=f"{application}-{environment}-cluster",
|
|
265
|
+
service=f"{application}-{environment}-{service}",
|
|
266
|
+
taskDefinition=task_def_arn,
|
|
267
|
+
desiredCount=desired_count,
|
|
268
|
+
)
|
|
269
|
+
return service_response["service"]
|
|
270
|
+
except ClientError as err:
|
|
271
|
+
raise PlatformException(f"Error updating ECS service: {err}")
|
|
272
|
+
|
|
273
|
+
def describe_service(self, service: str, environment: str, application: str) -> dict[str, Any]:
|
|
274
|
+
"""Return information about an ECS service."""
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
service_response = self.ecs_client.describe_services(
|
|
278
|
+
cluster=f"{application}-{environment}-cluster",
|
|
279
|
+
services=[
|
|
280
|
+
f"{application}-{environment}-{service}",
|
|
281
|
+
],
|
|
282
|
+
)
|
|
283
|
+
return service_response["services"][0]
|
|
284
|
+
except ClientError as err:
|
|
285
|
+
raise PlatformException(f"Error retrieving ECS service: {err}")
|
|
286
|
+
|
|
287
|
+
def describe_tasks(self, cluster_name: str, task_ids: list[str]) -> list[dict[str, Any]]:
|
|
288
|
+
"""Return information about ECS tasks."""
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
response = self.ecs_client.describe_tasks(cluster=cluster_name, tasks=task_ids)
|
|
292
|
+
return response["tasks"]
|
|
293
|
+
except ClientError as err:
|
|
294
|
+
raise PlatformException(f"Error retrieving ECS tasks: {err}")
|
|
@@ -7,12 +7,12 @@ from dbt_platform_helper.platform_exception import PlatformException
|
|
|
7
7
|
class EnvironmentVariableProvider:
|
|
8
8
|
|
|
9
9
|
@staticmethod
|
|
10
|
-
def get(env_var: str) -> Optional[str]:
|
|
10
|
+
def get(env_var: str, default=None) -> Optional[str]:
|
|
11
11
|
"""Returns the stripped value or None if not set or empty."""
|
|
12
12
|
value = os.environ.get(env_var)
|
|
13
13
|
if value and value.strip():
|
|
14
14
|
return value.strip()
|
|
15
|
-
return
|
|
15
|
+
return default
|
|
16
16
|
|
|
17
17
|
@staticmethod
|
|
18
18
|
def get_required(env_var: str) -> str:
|
|
@@ -24,8 +24,12 @@ class ClickIOProvider:
|
|
|
24
24
|
def info(self, message: str, **kwargs):
|
|
25
25
|
click.secho(message, **kwargs)
|
|
26
26
|
|
|
27
|
-
def input(
|
|
28
|
-
|
|
27
|
+
def input(
|
|
28
|
+
self, message: str, hide_input=False, confirmation_prompt=False, input_type=str
|
|
29
|
+
) -> str:
|
|
30
|
+
return click.prompt(
|
|
31
|
+
message, hide_input=hide_input, confirmation_prompt=confirmation_prompt, type=input_type
|
|
32
|
+
)
|
|
29
33
|
|
|
30
34
|
def confirm(self, message: str) -> bool:
|
|
31
35
|
try:
|
|
@@ -37,6 +41,9 @@ class ClickIOProvider:
|
|
|
37
41
|
click.secho(f"Error: {message}", err=True, fg="red")
|
|
38
42
|
exit(1)
|
|
39
43
|
|
|
44
|
+
def deploy_error(self, message: str):
|
|
45
|
+
click.secho(message, fg="red")
|
|
46
|
+
|
|
40
47
|
# TODO: DBTP-1979: messages will be a ValidationMessages class rather than a free-rein dictionary
|
|
41
48
|
def process_messages(self, messages: dict):
|
|
42
49
|
if not messages:
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Dict
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
1
5
|
from boto3 import Session
|
|
2
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
|
|
4
10
|
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
11
|
+
from dbt_platform_helper.providers.parameter_store import ParameterStore
|
|
5
12
|
from dbt_platform_helper.utils.aws import get_aws_session_or_abort
|
|
6
13
|
|
|
7
14
|
|
|
@@ -12,12 +19,24 @@ def normalise_to_cidr(ip: str):
|
|
|
12
19
|
return f"{ip}/{SINGLE_IPV4_CIDR_PREFIX_LENGTH}"
|
|
13
20
|
|
|
14
21
|
|
|
22
|
+
class ALBDataNormaliser:
|
|
23
|
+
|
|
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}
|
|
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}
|
|
31
|
+
|
|
32
|
+
|
|
15
33
|
class LoadBalancerProvider:
|
|
16
34
|
|
|
17
35
|
def __init__(self, session: Session = None, io: ClickIOProvider = ClickIOProvider()):
|
|
18
36
|
self.session = session
|
|
19
37
|
self.evlb_client = self._get_client("elbv2")
|
|
20
38
|
self.rg_tagging_client = self._get_client("resourcegroupstaggingapi")
|
|
39
|
+
self.parameter_store_provider = ParameterStore(self._get_client("ssm"))
|
|
21
40
|
self.io = io
|
|
22
41
|
|
|
23
42
|
def _get_client(self, client: str):
|
|
@@ -26,21 +45,44 @@ class LoadBalancerProvider:
|
|
|
26
45
|
return self.session.client(client)
|
|
27
46
|
|
|
28
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"
|
|
29
67
|
target_group_arn = None
|
|
30
68
|
|
|
31
69
|
paginator = self.rg_tagging_client.get_paginator("get_resources")
|
|
32
70
|
page_iterator = paginator.paginate(
|
|
33
71
|
TagFilters=[
|
|
34
72
|
{
|
|
35
|
-
"Key":
|
|
73
|
+
"Key": application_key,
|
|
36
74
|
"Values": [
|
|
37
75
|
app,
|
|
38
76
|
],
|
|
39
|
-
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"Key": environment_key,
|
|
40
80
|
"Values": [
|
|
41
81
|
env,
|
|
42
82
|
],
|
|
43
|
-
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"Key": service_key,
|
|
44
86
|
"Values": [
|
|
45
87
|
svc,
|
|
46
88
|
],
|
|
@@ -56,9 +98,9 @@ class LoadBalancerProvider:
|
|
|
56
98
|
tags = {tag["Key"]: tag["Value"] for tag in resource["Tags"]}
|
|
57
99
|
|
|
58
100
|
if (
|
|
59
|
-
tags.get(
|
|
60
|
-
and tags.get(
|
|
61
|
-
and tags.get(
|
|
101
|
+
tags.get(service_key) == svc
|
|
102
|
+
and tags.get(environment_key) == env
|
|
103
|
+
and tags.get(application_key) == app
|
|
62
104
|
):
|
|
63
105
|
target_group_arn = resource["ResourceARN"]
|
|
64
106
|
|
|
@@ -69,6 +111,29 @@ class LoadBalancerProvider:
|
|
|
69
111
|
|
|
70
112
|
return target_group_arn
|
|
71
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
|
+
|
|
72
137
|
def get_https_certificate_for_listener(self, listener_arn: str, env: str):
|
|
73
138
|
certificates = []
|
|
74
139
|
paginator = self.evlb_client.get_paginator("describe_listener_certificates")
|
|
@@ -87,7 +152,7 @@ class LoadBalancerProvider:
|
|
|
87
152
|
listener_arn = self.get_https_listener_for_application(app, env)
|
|
88
153
|
return self.get_https_certificate_for_listener(listener_arn, env)
|
|
89
154
|
|
|
90
|
-
def get_listeners_for_load_balancer(self, load_balancer_arn):
|
|
155
|
+
def get_listeners_for_load_balancer(self, load_balancer_arn: str) -> List[dict]:
|
|
91
156
|
listeners = []
|
|
92
157
|
paginator = self.evlb_client.get_paginator("describe_listeners")
|
|
93
158
|
page_iterator = paginator.paginate(LoadBalancerArn=load_balancer_arn)
|
|
@@ -98,6 +163,7 @@ class LoadBalancerProvider:
|
|
|
98
163
|
|
|
99
164
|
def get_https_listener_for_application(self, app: str, env: str) -> str:
|
|
100
165
|
load_balancer_arn = self.get_load_balancer_for_application(app, env)
|
|
166
|
+
self.io.debug(f"Load Balancer ARN: {load_balancer_arn}")
|
|
101
167
|
listeners = self.get_listeners_for_load_balancer(load_balancer_arn)
|
|
102
168
|
|
|
103
169
|
listener_arn = None
|
|
@@ -112,7 +178,7 @@ class LoadBalancerProvider:
|
|
|
112
178
|
|
|
113
179
|
return listener_arn
|
|
114
180
|
|
|
115
|
-
def get_load_balancers(self):
|
|
181
|
+
def get_load_balancers(self) -> List[dict]:
|
|
116
182
|
load_balancers = []
|
|
117
183
|
paginator = self.evlb_client.get_paginator("describe_load_balancers")
|
|
118
184
|
page_iterator = paginator.paginate()
|
|
@@ -133,7 +199,11 @@ class LoadBalancerProvider:
|
|
|
133
199
|
for lb in tag_descriptions:
|
|
134
200
|
tags = {t["Key"]: t["Value"] for t in lb["Tags"]}
|
|
135
201
|
# TODO: DBTP-1967: copilot hangover, creates coupling to specific tags could update to check application and environment
|
|
136
|
-
if
|
|
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
|
+
):
|
|
137
207
|
return lb["ResourceArn"]
|
|
138
208
|
|
|
139
209
|
raise LoadBalancerNotFoundException(app, env)
|
|
@@ -149,8 +219,9 @@ class LoadBalancerProvider:
|
|
|
149
219
|
|
|
150
220
|
for rule in rules:
|
|
151
221
|
for action in rule["Actions"]:
|
|
152
|
-
if
|
|
153
|
-
|
|
222
|
+
if "TargetGroupArn" in action:
|
|
223
|
+
if action["Type"] == "forward" and action["TargetGroupArn"] == target_group_arn:
|
|
224
|
+
conditions = rule["Conditions"]
|
|
154
225
|
|
|
155
226
|
if not conditions:
|
|
156
227
|
raise ListenerRuleConditionsNotFoundException(listener_arn)
|
|
@@ -172,7 +243,37 @@ class LoadBalancerProvider:
|
|
|
172
243
|
def get_rules_tag_descriptions_by_listener_arn(self, listener_arn: str) -> list:
|
|
173
244
|
rules = self.get_listener_rules_by_listener_arn(listener_arn)
|
|
174
245
|
|
|
175
|
-
return self.
|
|
246
|
+
return self.get_resources_tag_descriptions(rules)
|
|
247
|
+
|
|
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
|
|
261
|
+
|
|
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)
|
|
266
|
+
|
|
267
|
+
tags = self.get_resources_tag_descriptions(rules)
|
|
268
|
+
|
|
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
|
|
176
277
|
|
|
177
278
|
def get_listener_rules_by_listener_arn(self, listener_arn: str) -> list:
|
|
178
279
|
rules = []
|
|
@@ -183,13 +284,15 @@ class LoadBalancerProvider:
|
|
|
183
284
|
|
|
184
285
|
return rules
|
|
185
286
|
|
|
186
|
-
def
|
|
287
|
+
def get_resources_tag_descriptions(
|
|
288
|
+
self, resources: list, resource_identifier: str = "RuleArn"
|
|
289
|
+
) -> list:
|
|
187
290
|
tag_descriptions = []
|
|
188
291
|
chunk_size = 20
|
|
189
292
|
|
|
190
|
-
for i in range(0, len(
|
|
191
|
-
chunk =
|
|
192
|
-
resource_arns = [r[
|
|
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]
|
|
193
296
|
response = self.evlb_client.describe_tags(
|
|
194
297
|
ResourceArns=resource_arns
|
|
195
298
|
) # describe_tags cannot be paginated - 04/04/2025
|
|
@@ -304,6 +407,9 @@ class LoadBalancerProvider:
|
|
|
304
407
|
|
|
305
408
|
return deleted_rules
|
|
306
409
|
|
|
410
|
+
def delete_listener_rule_by_resource_arn(self, resource_arn: str) -> list:
|
|
411
|
+
return self.evlb_client.delete_rule(RuleArn=resource_arn)
|
|
412
|
+
|
|
307
413
|
|
|
308
414
|
class LoadBalancerException(PlatformException):
|
|
309
415
|
pass
|
|
@@ -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}")
|
|
@@ -1,46 +1,133 @@
|
|
|
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
|
+
|
|
1
7
|
import boto3
|
|
2
8
|
|
|
3
9
|
from dbt_platform_helper.platform_exception import PlatformException
|
|
4
10
|
|
|
5
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
|
+
|
|
6
37
|
class ParameterStore:
|
|
7
|
-
def __init__(self, ssm_client: boto3.client):
|
|
38
|
+
def __init__(self, ssm_client: boto3.client, with_model: bool = False):
|
|
8
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
|
|
9
51
|
|
|
10
|
-
def get_ssm_parameter_by_name(
|
|
52
|
+
def get_ssm_parameter_by_name(
|
|
53
|
+
self, parameter_name: str, add_tags: bool = False
|
|
54
|
+
) -> Union[dict, Parameter]:
|
|
11
55
|
"""
|
|
12
56
|
Retrieves the latest version of a parameter from parameter store for a
|
|
13
57
|
given name/arn.
|
|
14
58
|
|
|
15
59
|
Args:
|
|
16
|
-
|
|
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
|
|
17
62
|
Returns:
|
|
18
63
|
dict: A dictionary representation of your ssm parameter
|
|
19
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
|
+
)
|
|
20
80
|
|
|
21
|
-
|
|
81
|
+
if add_tags:
|
|
82
|
+
model.tags = self.__fetch_tags(model)
|
|
22
83
|
|
|
23
|
-
|
|
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]]:
|
|
24
89
|
"""
|
|
25
90
|
Retrieves all SSM parameters for a given path from parameter store.
|
|
26
91
|
|
|
27
92
|
Args:
|
|
28
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
|
|
29
95
|
Returns:
|
|
30
96
|
list: A list of dictionaries containing all SSM parameters under the provided path.
|
|
31
97
|
"""
|
|
32
98
|
|
|
33
99
|
parameters = []
|
|
34
100
|
paginator = self.ssm_client.get_paginator("get_parameters_by_path")
|
|
35
|
-
page_iterator = paginator.paginate(Path=path, Recursive=True)
|
|
101
|
+
page_iterator = paginator.paginate(Path=path, Recursive=True, WithDecryption=True)
|
|
36
102
|
|
|
37
103
|
for page in page_iterator:
|
|
38
104
|
parameters.extend(page.get("Parameters", []))
|
|
39
105
|
|
|
40
|
-
if
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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)
|
|
44
131
|
|
|
45
132
|
|
|
46
133
|
class ParameterNotFoundForPathException(PlatformException):
|