dbt-platform-helper 15.0.0__py3-none-any.whl → 15.2.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/domain/codebase.py +12 -3
- dbt_platform_helper/domain/conduit.py +322 -76
- dbt_platform_helper/domain/config.py +4 -4
- dbt_platform_helper/domain/versioning.py +6 -6
- dbt_platform_helper/platform_exception.py +4 -0
- dbt_platform_helper/providers/aws/exceptions.py +20 -2
- dbt_platform_helper/providers/config.py +2 -2
- dbt_platform_helper/providers/copilot.py +20 -12
- dbt_platform_helper/providers/ecr.py +62 -37
- dbt_platform_helper/providers/ecs.py +112 -40
- dbt_platform_helper/providers/io.py +6 -1
- dbt_platform_helper/providers/terraform_manifest.py +7 -1
- dbt_platform_helper/providers/version.py +1 -1
- dbt_platform_helper/providers/version_status.py +1 -1
- dbt_platform_helper/providers/vpc.py +1 -1
- dbt_platform_helper/utilities/decorators.py +103 -0
- dbt_platform_helper/utils/validation.py +1 -1
- {dbt_platform_helper-15.0.0.dist-info → dbt_platform_helper-15.2.0.dist-info}/METADATA +1 -1
- {dbt_platform_helper-15.0.0.dist-info → dbt_platform_helper-15.2.0.dist-info}/RECORD +24 -23
- {dbt_platform_helper-15.0.0.dist-info → dbt_platform_helper-15.2.0.dist-info}/WHEEL +1 -1
- /dbt_platform_helper/{providers → entities}/platform_config_schema.py +0 -0
- /dbt_platform_helper/{providers → entities}/semantic_version.py +0 -0
- {dbt_platform_helper-15.0.0.dist-info → dbt_platform_helper-15.2.0.dist-info}/LICENSE +0 -0
- {dbt_platform_helper-15.0.0.dist-info → dbt_platform_helper-15.2.0.dist-info}/entry_points.txt +0 -0
|
@@ -167,13 +167,16 @@ class Codebase:
|
|
|
167
167
|
|
|
168
168
|
image_ref = None
|
|
169
169
|
if commit:
|
|
170
|
-
|
|
170
|
+
self._validate_sha_length(commit)
|
|
171
|
+
image_ref = f"commit-{commit}"
|
|
171
172
|
elif tag:
|
|
172
173
|
image_ref = f"tag-{tag}"
|
|
173
174
|
elif branch:
|
|
174
175
|
image_ref = f"branch-{branch}"
|
|
175
|
-
|
|
176
|
-
image_ref = self.ecr_provider.
|
|
176
|
+
|
|
177
|
+
image_ref = self.ecr_provider.get_commit_tag_for_reference(
|
|
178
|
+
application.name, codebase, image_ref
|
|
179
|
+
)
|
|
177
180
|
|
|
178
181
|
codepipeline_client = session.client("codepipeline")
|
|
179
182
|
pipeline_name = self.get_manual_release_pipeline(codepipeline_client, app, codebase)
|
|
@@ -284,6 +287,12 @@ class Codebase:
|
|
|
284
287
|
return get_build_url_from_pipeline_execution_id(execution_id, build_options["name"])
|
|
285
288
|
return None
|
|
286
289
|
|
|
290
|
+
def _validate_sha_length(self, commit):
|
|
291
|
+
if len(commit) < 7:
|
|
292
|
+
self.io.abort_with_error(
|
|
293
|
+
"Your commit reference is too short. Commit sha hashes specified by '--commit' must be at least 7 characters long."
|
|
294
|
+
)
|
|
295
|
+
|
|
287
296
|
|
|
288
297
|
class ApplicationDeploymentNotTriggered(PlatformException):
|
|
289
298
|
def __init__(self, codebase: str):
|
|
@@ -1,119 +1,365 @@
|
|
|
1
|
-
import
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
from typing import Callable
|
|
4
|
+
from typing import Optional
|
|
2
5
|
|
|
3
6
|
from dbt_platform_helper.providers.cloudformation import CloudFormation
|
|
7
|
+
from dbt_platform_helper.providers.copilot import _normalise_secret_name
|
|
4
8
|
from dbt_platform_helper.providers.copilot import connect_to_addon_client_task
|
|
5
9
|
from dbt_platform_helper.providers.copilot import create_addon_client_task
|
|
10
|
+
from dbt_platform_helper.providers.copilot import get_postgres_admin_connection_string
|
|
6
11
|
from dbt_platform_helper.providers.ecs import ECS
|
|
7
12
|
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
8
13
|
from dbt_platform_helper.providers.secrets import Secrets
|
|
14
|
+
from dbt_platform_helper.providers.vpc import VpcProvider
|
|
9
15
|
from dbt_platform_helper.utils.application import Application
|
|
10
16
|
|
|
11
17
|
|
|
12
|
-
class
|
|
18
|
+
class ConduitECSStrategy(ABC):
|
|
19
|
+
@abstractmethod
|
|
20
|
+
def get_data(self):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def start_task(self, data_context: dict):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def exec_task(self, data_context: dict):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TerraformConduitStrategy(ConduitECSStrategy):
|
|
13
33
|
def __init__(
|
|
14
34
|
self,
|
|
35
|
+
clients,
|
|
36
|
+
ecs_provider: ECS,
|
|
15
37
|
application: Application,
|
|
38
|
+
addon_name: str,
|
|
39
|
+
addon_type: str,
|
|
40
|
+
access: str,
|
|
41
|
+
env: str,
|
|
42
|
+
io: ClickIOProvider,
|
|
43
|
+
vpc_provider: Callable,
|
|
44
|
+
get_postgres_admin_connection_string: Callable,
|
|
45
|
+
):
|
|
46
|
+
self.clients = clients
|
|
47
|
+
self.ecs_provider = ecs_provider
|
|
48
|
+
self.io = io
|
|
49
|
+
self.vpc_provider = vpc_provider
|
|
50
|
+
self.access = access
|
|
51
|
+
self.addon_name = addon_name
|
|
52
|
+
self.addon_type = addon_type
|
|
53
|
+
self.application = application
|
|
54
|
+
self.env = env
|
|
55
|
+
self.get_postgres_admin_connection_string = get_postgres_admin_connection_string
|
|
56
|
+
|
|
57
|
+
def get_data(self):
|
|
58
|
+
self.io.info("Starting conduit in Terraform mode.")
|
|
59
|
+
return {
|
|
60
|
+
"cluster_arn": self.ecs_provider.get_cluster_arn_by_name(
|
|
61
|
+
f"{self.application.name}-{self.env}"
|
|
62
|
+
),
|
|
63
|
+
"task_def_family": self._generate_container_name(),
|
|
64
|
+
"vpc_name": self._resolve_vpc_name(),
|
|
65
|
+
"addon_type": self.addon_type,
|
|
66
|
+
"access": self.access,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def start_task(self, data_context: dict):
|
|
70
|
+
|
|
71
|
+
environments = self.application.environments
|
|
72
|
+
environment = environments.get(self.env)
|
|
73
|
+
env_session = environment.session
|
|
74
|
+
|
|
75
|
+
vpc_provider = self.vpc_provider(env_session)
|
|
76
|
+
vpc_config = vpc_provider.get_vpc(
|
|
77
|
+
self.application.name,
|
|
78
|
+
self.env,
|
|
79
|
+
data_context["vpc_name"],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
postgres_admin_env_vars = None
|
|
83
|
+
if data_context["addon_type"] == "postgres" and data_context["access"] == "admin":
|
|
84
|
+
postgres_admin_env_vars = [
|
|
85
|
+
{
|
|
86
|
+
"name": "CONNECTION_SECRET",
|
|
87
|
+
"value": self.get_postgres_admin_connection_string(
|
|
88
|
+
self.clients.get("ssm"),
|
|
89
|
+
f"/copilot/{self.application.name}/{self.env}/secrets/{_normalise_secret_name(self.addon_name)}",
|
|
90
|
+
self.application,
|
|
91
|
+
self.env,
|
|
92
|
+
self.addon_name,
|
|
93
|
+
),
|
|
94
|
+
},
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
self.ecs_provider.start_ecs_task(
|
|
98
|
+
f"{self.application.name}-{self.env}",
|
|
99
|
+
self._generate_container_name(),
|
|
100
|
+
data_context["task_def_family"],
|
|
101
|
+
vpc_config,
|
|
102
|
+
postgres_admin_env_vars,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def exec_task(self, data_context: dict):
|
|
106
|
+
self.ecs_provider.exec_task(data_context["cluster_arn"], data_context["task_arns"][0])
|
|
107
|
+
|
|
108
|
+
def _generate_container_name(self):
|
|
109
|
+
return f"conduit-{self.addon_type}-{self.access}-{self.application.name}-{self.env}-{self.addon_name}"
|
|
110
|
+
|
|
111
|
+
def _resolve_vpc_name(self):
|
|
112
|
+
ssm_client = self.clients["ssm"]
|
|
113
|
+
parameter_key = f"/conduit/{self.application.name}/{self.env}/{_normalise_secret_name(self.addon_name)}_VPC_NAME"
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
response = ssm_client.get_parameter(Name=parameter_key)
|
|
117
|
+
return response["Parameter"]["Value"]
|
|
118
|
+
except ssm_client.exceptions.ParameterNotFound:
|
|
119
|
+
self.io.abort_with_error(
|
|
120
|
+
f"Could not find VPC name for {self.addon_name}. Missing SSM param: {parameter_key}"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class CopilotConduitStrategy(ConduitECSStrategy):
|
|
125
|
+
def __init__(
|
|
126
|
+
self,
|
|
127
|
+
clients,
|
|
128
|
+
ecs_provider: ECS,
|
|
16
129
|
secrets_provider: Secrets,
|
|
17
130
|
cloudformation_provider: CloudFormation,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
131
|
+
application: Application,
|
|
132
|
+
addon_name: str,
|
|
133
|
+
access: str,
|
|
134
|
+
env: str,
|
|
135
|
+
io: ClickIOProvider,
|
|
136
|
+
connect_to_addon_client_task: Callable,
|
|
137
|
+
create_addon_client_task: Callable,
|
|
23
138
|
):
|
|
24
|
-
|
|
25
|
-
self.application = application
|
|
26
|
-
self.secrets_provider = secrets_provider
|
|
139
|
+
self.clients = clients
|
|
27
140
|
self.cloudformation_provider = cloudformation_provider
|
|
28
141
|
self.ecs_provider = ecs_provider
|
|
29
|
-
self.
|
|
142
|
+
self.secrets_provider = secrets_provider
|
|
143
|
+
|
|
30
144
|
self.io = io
|
|
145
|
+
self.access = access
|
|
146
|
+
self.addon_name = addon_name
|
|
147
|
+
self.application = application
|
|
148
|
+
self.env = env
|
|
31
149
|
self.connect_to_addon_client_task = connect_to_addon_client_task
|
|
32
150
|
self.create_addon_client_task = create_addon_client_task
|
|
33
151
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
addon_type
|
|
37
|
-
|
|
152
|
+
def get_data(self):
|
|
153
|
+
|
|
154
|
+
addon_type = self.secrets_provider.get_addon_type(self.addon_name)
|
|
155
|
+
parameter_name = self.secrets_provider.get_parameter_name(
|
|
156
|
+
addon_type, self.addon_name, self.access
|
|
38
157
|
)
|
|
158
|
+
task_name = self.ecs_provider.get_or_create_task_name(self.addon_name, parameter_name)
|
|
39
159
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
160
|
+
return {
|
|
161
|
+
"cluster_arn": self.ecs_provider.get_cluster_arn_by_copilot_tag(),
|
|
162
|
+
"addon_type": addon_type,
|
|
163
|
+
"task_def_family": f"copilot-{task_name}",
|
|
164
|
+
"parameter_name": parameter_name,
|
|
165
|
+
"task_name": task_name,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
def start_task(self, data_context: dict):
|
|
169
|
+
self.create_addon_client_task(
|
|
170
|
+
self.clients["iam"],
|
|
171
|
+
self.clients["ssm"],
|
|
172
|
+
self.application,
|
|
173
|
+
self.env,
|
|
174
|
+
data_context["addon_type"],
|
|
175
|
+
self.addon_name,
|
|
176
|
+
data_context["task_name"],
|
|
177
|
+
self.access,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
self.io.info("Updating conduit task")
|
|
181
|
+
self.cloudformation_provider.add_stack_delete_policy_to_task_role(data_context["task_name"])
|
|
182
|
+
stack_name = self.cloudformation_provider.update_conduit_stack_resources(
|
|
183
|
+
self.application.name,
|
|
184
|
+
self.env,
|
|
185
|
+
data_context["addon_type"],
|
|
186
|
+
self.addon_name,
|
|
187
|
+
data_context["task_name"],
|
|
188
|
+
data_context["parameter_name"],
|
|
189
|
+
self.access,
|
|
190
|
+
)
|
|
191
|
+
self.io.info("Waiting for conduit task update to complete...")
|
|
192
|
+
self.cloudformation_provider.wait_for_cloudformation_to_reach_status(
|
|
193
|
+
"stack_update_complete", stack_name
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def exec_task(self, data_context: dict):
|
|
197
|
+
self.connect_to_addon_client_task(
|
|
198
|
+
self.clients["ecs"],
|
|
199
|
+
self.application.name,
|
|
200
|
+
self.env,
|
|
201
|
+
data_context["cluster_arn"],
|
|
202
|
+
data_context["task_name"],
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class ConduitStrategyFactory:
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def detect_mode(
|
|
210
|
+
ecs_client,
|
|
211
|
+
application,
|
|
212
|
+
environment,
|
|
213
|
+
addon_name: str,
|
|
214
|
+
addon_type: str,
|
|
215
|
+
access: str,
|
|
216
|
+
io: ClickIOProvider,
|
|
217
|
+
) -> str:
|
|
218
|
+
"""Detect if Terraform-based conduit task definitions are present,
|
|
219
|
+
otherwise default to Copilot mode."""
|
|
220
|
+
paginator = ecs_client.get_paginator("list_task_definitions")
|
|
221
|
+
prefix = f"conduit-{addon_type}-{access}-{application}-{environment}-{addon_name}"
|
|
222
|
+
|
|
223
|
+
for page in paginator.paginate():
|
|
224
|
+
for arn in page["taskDefinitionArns"]:
|
|
225
|
+
if arn.split("/")[-1].startswith(prefix):
|
|
226
|
+
return "terraform"
|
|
227
|
+
|
|
228
|
+
io.info("Defaulting to copilot mode.")
|
|
229
|
+
return "copilot"
|
|
230
|
+
|
|
231
|
+
@staticmethod
|
|
232
|
+
def create_strategy(
|
|
233
|
+
mode: str,
|
|
234
|
+
clients,
|
|
235
|
+
ecs_provider: ECS,
|
|
236
|
+
secrets_provider: Secrets,
|
|
237
|
+
cloudformation_provider: CloudFormation,
|
|
238
|
+
application: Application,
|
|
239
|
+
addon_name: str,
|
|
240
|
+
addon_type: str,
|
|
241
|
+
access: str,
|
|
242
|
+
env: str,
|
|
243
|
+
io: ClickIOProvider,
|
|
244
|
+
):
|
|
245
|
+
|
|
246
|
+
if mode == "terraform":
|
|
247
|
+
return TerraformConduitStrategy(
|
|
248
|
+
clients,
|
|
249
|
+
ecs_provider,
|
|
250
|
+
application,
|
|
51
251
|
addon_name,
|
|
52
|
-
|
|
252
|
+
addon_type,
|
|
53
253
|
access,
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
self.io.info("Updating conduit task")
|
|
57
|
-
self._update_stack_resources(
|
|
58
|
-
self.application.name,
|
|
59
254
|
env,
|
|
60
|
-
|
|
255
|
+
io,
|
|
256
|
+
vpc_provider=VpcProvider,
|
|
257
|
+
get_postgres_admin_connection_string=get_postgres_admin_connection_string,
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
return CopilotConduitStrategy(
|
|
261
|
+
clients,
|
|
262
|
+
ecs_provider,
|
|
263
|
+
secrets_provider,
|
|
264
|
+
cloudformation_provider,
|
|
265
|
+
application,
|
|
61
266
|
addon_name,
|
|
62
|
-
task_name,
|
|
63
|
-
parameter_name,
|
|
64
267
|
access,
|
|
268
|
+
env,
|
|
269
|
+
io,
|
|
270
|
+
connect_to_addon_client_task=connect_to_addon_client_task,
|
|
271
|
+
create_addon_client_task=create_addon_client_task,
|
|
65
272
|
)
|
|
66
273
|
|
|
67
|
-
task_arns = self.ecs_provider.get_ecs_task_arns(cluster_arn, task_name)
|
|
68
274
|
|
|
69
|
-
|
|
70
|
-
|
|
275
|
+
class Conduit:
|
|
276
|
+
def __init__(
|
|
277
|
+
self,
|
|
278
|
+
application: Application,
|
|
279
|
+
secrets_provider: Secrets,
|
|
280
|
+
cloudformation_provider: CloudFormation,
|
|
281
|
+
ecs_provider: ECS,
|
|
282
|
+
io: ClickIOProvider = ClickIOProvider(),
|
|
283
|
+
vpc_provider=VpcProvider,
|
|
284
|
+
strategy_factory: Optional[ConduitStrategyFactory] = None,
|
|
285
|
+
):
|
|
286
|
+
|
|
287
|
+
self.application = application
|
|
288
|
+
self.secrets_provider = secrets_provider
|
|
289
|
+
self.cloudformation_provider = cloudformation_provider
|
|
290
|
+
self.ecs_provider = ecs_provider
|
|
291
|
+
self.io = io
|
|
292
|
+
self.vpc_provider = vpc_provider
|
|
293
|
+
self.strategy_factory = strategy_factory or ConduitStrategyFactory()
|
|
71
294
|
|
|
72
|
-
|
|
295
|
+
def start(self, env: str, addon_name: str, access: str = "read"):
|
|
296
|
+
self.clients = self._initialise_clients(env)
|
|
297
|
+
addon_type = self.secrets_provider.get_addon_type(addon_name)
|
|
73
298
|
|
|
74
|
-
|
|
299
|
+
if (addon_type == "opensearch" or addon_type == "redis") and (access != "read"):
|
|
300
|
+
access = "read"
|
|
75
301
|
|
|
76
|
-
self.
|
|
77
|
-
|
|
78
|
-
|
|
302
|
+
mode = self.strategy_factory.detect_mode(
|
|
303
|
+
self.clients.get("ecs"),
|
|
304
|
+
self.application.name,
|
|
305
|
+
env,
|
|
306
|
+
addon_name,
|
|
307
|
+
addon_type,
|
|
308
|
+
access,
|
|
309
|
+
self.io,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
strategy = self.strategy_factory.create_strategy(
|
|
313
|
+
mode=mode,
|
|
314
|
+
clients=self.clients,
|
|
315
|
+
ecs_provider=self.ecs_provider,
|
|
316
|
+
secrets_provider=self.secrets_provider,
|
|
317
|
+
cloudformation_provider=self.cloudformation_provider,
|
|
318
|
+
application=self.application,
|
|
319
|
+
addon_name=addon_name,
|
|
320
|
+
addon_type=addon_type,
|
|
321
|
+
access=access,
|
|
322
|
+
env=env,
|
|
323
|
+
io=self.io,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
data_context = strategy.get_data()
|
|
327
|
+
|
|
328
|
+
data_context["task_arns"] = self.ecs_provider.get_ecs_task_arns(
|
|
329
|
+
data_context["cluster_arn"], data_context["task_def_family"]
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
info_log = (
|
|
333
|
+
f"Checking if a conduit ECS task is already running for:\n"
|
|
334
|
+
f" Addon Name : {addon_name}\n"
|
|
335
|
+
f" Addon Type : {addon_type}"
|
|
79
336
|
)
|
|
80
337
|
|
|
338
|
+
if addon_type == "postgres":
|
|
339
|
+
info_log += f"\n Access Level : {access}"
|
|
340
|
+
|
|
341
|
+
self.io.info(info_log)
|
|
342
|
+
|
|
343
|
+
if not data_context["task_arns"]:
|
|
344
|
+
self.io.info("Creating conduit ECS task...")
|
|
345
|
+
strategy.start_task(data_context)
|
|
346
|
+
data_context["task_arns"] = self.ecs_provider.wait_for_task_to_register(
|
|
347
|
+
data_context["cluster_arn"], data_context["task_def_family"]
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
self.io.info(f"Found a task already running: {data_context['task_arns'][0]}")
|
|
351
|
+
|
|
352
|
+
self.io.info(f"Waiting for ECS Exec agent to become available on the conduit task...")
|
|
353
|
+
self.ecs_provider.ecs_exec_is_available(
|
|
354
|
+
data_context["cluster_arn"], data_context["task_arns"]
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
self.io.info("Connecting to conduit task...")
|
|
358
|
+
strategy.exec_task(data_context)
|
|
359
|
+
|
|
81
360
|
def _initialise_clients(self, env):
|
|
82
361
|
return {
|
|
83
362
|
"ecs": self.application.environments[env].session.client("ecs"),
|
|
84
363
|
"iam": self.application.environments[env].session.client("iam"),
|
|
85
364
|
"ssm": self.application.environments[env].session.client("ssm"),
|
|
86
365
|
}
|
|
87
|
-
|
|
88
|
-
def _get_addon_details(self, addon_name, access):
|
|
89
|
-
addon_type = self.secrets_provider.get_addon_type(addon_name)
|
|
90
|
-
cluster_arn = self.ecs_provider.get_cluster_arn()
|
|
91
|
-
parameter_name = self.secrets_provider.get_parameter_name(addon_type, addon_name, access)
|
|
92
|
-
task_name = self.ecs_provider.get_or_create_task_name(addon_name, parameter_name)
|
|
93
|
-
|
|
94
|
-
return addon_type, cluster_arn, parameter_name, task_name
|
|
95
|
-
|
|
96
|
-
def _update_stack_resources(
|
|
97
|
-
self,
|
|
98
|
-
app_name,
|
|
99
|
-
env,
|
|
100
|
-
addon_type,
|
|
101
|
-
addon_name,
|
|
102
|
-
task_name,
|
|
103
|
-
parameter_name,
|
|
104
|
-
access,
|
|
105
|
-
):
|
|
106
|
-
self.cloudformation_provider.add_stack_delete_policy_to_task_role(task_name)
|
|
107
|
-
stack_name = self.cloudformation_provider.update_conduit_stack_resources(
|
|
108
|
-
app_name,
|
|
109
|
-
env,
|
|
110
|
-
addon_type,
|
|
111
|
-
addon_name,
|
|
112
|
-
task_name,
|
|
113
|
-
parameter_name,
|
|
114
|
-
access,
|
|
115
|
-
)
|
|
116
|
-
self.io.info("Waiting for conduit task update to complete...")
|
|
117
|
-
self.cloudformation_provider.wait_for_cloudformation_to_reach_status(
|
|
118
|
-
"stack_update_complete", stack_name
|
|
119
|
-
)
|
|
@@ -10,16 +10,16 @@ from dbt_platform_helper.constants import PLATFORM_CONFIG_FILE
|
|
|
10
10
|
from dbt_platform_helper.domain.versioning import AWSVersioning
|
|
11
11
|
from dbt_platform_helper.domain.versioning import CopilotVersioning
|
|
12
12
|
from dbt_platform_helper.domain.versioning import PlatformHelperVersioning
|
|
13
|
+
from dbt_platform_helper.entities.semantic_version import (
|
|
14
|
+
IncompatibleMajorVersionException,
|
|
15
|
+
)
|
|
16
|
+
from dbt_platform_helper.entities.semantic_version import SemanticVersion
|
|
13
17
|
from dbt_platform_helper.platform_exception import PlatformException
|
|
14
18
|
from dbt_platform_helper.providers.aws.sso_auth import SSOAuthProvider
|
|
15
19
|
from dbt_platform_helper.providers.config import ConfigProvider
|
|
16
20
|
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
17
21
|
from dbt_platform_helper.providers.schema_migrator import ALL_MIGRATIONS
|
|
18
22
|
from dbt_platform_helper.providers.schema_migrator import Migrator
|
|
19
|
-
from dbt_platform_helper.providers.semantic_version import (
|
|
20
|
-
IncompatibleMajorVersionException,
|
|
21
|
-
)
|
|
22
|
-
from dbt_platform_helper.providers.semantic_version import SemanticVersion
|
|
23
23
|
from dbt_platform_helper.providers.validation import ValidationException
|
|
24
24
|
from dbt_platform_helper.providers.version_status import VersionStatus
|
|
25
25
|
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
|
-
from dbt_platform_helper.
|
|
4
|
-
from dbt_platform_helper.providers.config import ConfigProvider
|
|
5
|
-
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
6
|
-
from dbt_platform_helper.providers.semantic_version import (
|
|
3
|
+
from dbt_platform_helper.entities.semantic_version import (
|
|
7
4
|
IncompatibleMajorVersionException,
|
|
8
5
|
)
|
|
9
|
-
from dbt_platform_helper.
|
|
6
|
+
from dbt_platform_helper.entities.semantic_version import (
|
|
10
7
|
IncompatibleMinorVersionException,
|
|
11
8
|
)
|
|
12
|
-
from dbt_platform_helper.
|
|
9
|
+
from dbt_platform_helper.entities.semantic_version import SemanticVersion
|
|
10
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
11
|
+
from dbt_platform_helper.providers.config import ConfigProvider
|
|
12
|
+
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
13
13
|
from dbt_platform_helper.providers.version import AWSCLIInstalledVersionProvider
|
|
14
14
|
from dbt_platform_helper.providers.version import CopilotInstalledVersionProvider
|
|
15
15
|
from dbt_platform_helper.providers.version import GithubLatestVersionProvider
|
|
@@ -12,16 +12,34 @@ class CreateTaskTimeoutException(AWSException):
|
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
|
|
15
|
+
IMAGE_NOT_FOUND_TEMPLATE = """An image labelled "{image_ref}" could not be found in your image repository. Try the `platform-helper codebase build` command first."""
|
|
16
|
+
|
|
17
|
+
|
|
15
18
|
class ImageNotFoundException(AWSException):
|
|
16
19
|
def __init__(self, image_ref: str):
|
|
20
|
+
super().__init__(IMAGE_NOT_FOUND_TEMPLATE.format(image_ref=image_ref))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
MULTIPLE_IMAGES_FOUND_TEMPLATE = (
|
|
24
|
+
'Image reference "{image_ref}" is matched by the following images: {matching_images}'
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MultipleImagesFoundException(AWSException):
|
|
29
|
+
def __init__(self, image_ref: str, matching_images: list[str]):
|
|
17
30
|
super().__init__(
|
|
18
|
-
|
|
31
|
+
MULTIPLE_IMAGES_FOUND_TEMPLATE.format(
|
|
32
|
+
image_ref=image_ref, matching_images=", ".join(sorted(matching_images))
|
|
33
|
+
)
|
|
19
34
|
)
|
|
20
35
|
|
|
21
36
|
|
|
37
|
+
REPOSITORY_NOT_FOUND_TEMPLATE = """The ECR repository "{repository}" could not be found."""
|
|
38
|
+
|
|
39
|
+
|
|
22
40
|
class RepositoryNotFoundException(AWSException):
|
|
23
41
|
def __init__(self, repository: str):
|
|
24
|
-
super().__init__(
|
|
42
|
+
super().__init__(REPOSITORY_NOT_FOUND_TEMPLATE.format(repository=repository))
|
|
25
43
|
|
|
26
44
|
|
|
27
45
|
class LogGroupNotFoundException(AWSException):
|
|
@@ -8,11 +8,11 @@ from dbt_platform_helper.constants import FIRST_UPGRADABLE_PLATFORM_HELPER_MAJOR
|
|
|
8
8
|
from dbt_platform_helper.constants import PLATFORM_CONFIG_FILE
|
|
9
9
|
from dbt_platform_helper.constants import PLATFORM_CONFIG_SCHEMA_VERSION
|
|
10
10
|
from dbt_platform_helper.constants import PLATFORM_HELPER_PACKAGE_NAME
|
|
11
|
+
from dbt_platform_helper.entities.platform_config_schema import PlatformConfigSchema
|
|
12
|
+
from dbt_platform_helper.entities.semantic_version import SemanticVersion
|
|
11
13
|
from dbt_platform_helper.providers.config_validator import ConfigValidator
|
|
12
14
|
from dbt_platform_helper.providers.config_validator import ConfigValidatorError
|
|
13
15
|
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
14
|
-
from dbt_platform_helper.providers.platform_config_schema import PlatformConfigSchema
|
|
15
|
-
from dbt_platform_helper.providers.semantic_version import SemanticVersion
|
|
16
16
|
from dbt_platform_helper.providers.version import InstalledVersionProvider
|
|
17
17
|
from dbt_platform_helper.providers.yaml_file import FileNotFoundException
|
|
18
18
|
from dbt_platform_helper.providers.yaml_file import FileProviderException
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import subprocess
|
|
2
3
|
import time
|
|
3
4
|
|
|
4
5
|
from botocore.exceptions import ClientError
|
|
@@ -13,7 +14,6 @@ from dbt_platform_helper.utils.messages import abort_with_error
|
|
|
13
14
|
def create_addon_client_task(
|
|
14
15
|
iam_client,
|
|
15
16
|
ssm_client,
|
|
16
|
-
subprocess,
|
|
17
17
|
application: Application,
|
|
18
18
|
env: str,
|
|
19
19
|
addon_type: str,
|
|
@@ -31,7 +31,6 @@ def create_addon_client_task(
|
|
|
31
31
|
elif access == "admin":
|
|
32
32
|
create_postgres_admin_task(
|
|
33
33
|
ssm_client,
|
|
34
|
-
subprocess,
|
|
35
34
|
application,
|
|
36
35
|
addon_name,
|
|
37
36
|
addon_type,
|
|
@@ -71,15 +70,8 @@ def create_addon_client_task(
|
|
|
71
70
|
)
|
|
72
71
|
|
|
73
72
|
|
|
74
|
-
def
|
|
75
|
-
ssm_client,
|
|
76
|
-
subprocess,
|
|
77
|
-
app: Application,
|
|
78
|
-
addon_name: str,
|
|
79
|
-
addon_type: str,
|
|
80
|
-
env: str,
|
|
81
|
-
secret_name: str,
|
|
82
|
-
task_name: str,
|
|
73
|
+
def get_postgres_admin_connection_string(
|
|
74
|
+
ssm_client, secret_name: str, app: Application, env: str, addon_name: str
|
|
83
75
|
):
|
|
84
76
|
read_only_secret_name = secret_name + "_READ_ONLY_USER"
|
|
85
77
|
master_secret_name = (
|
|
@@ -94,6 +86,23 @@ def create_postgres_admin_task(
|
|
|
94
86
|
)
|
|
95
87
|
)
|
|
96
88
|
|
|
89
|
+
return connection_string
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def create_postgres_admin_task(
|
|
93
|
+
ssm_client,
|
|
94
|
+
app: Application,
|
|
95
|
+
addon_name: str,
|
|
96
|
+
addon_type: str,
|
|
97
|
+
env: str,
|
|
98
|
+
secret_name: str,
|
|
99
|
+
task_name: str,
|
|
100
|
+
):
|
|
101
|
+
|
|
102
|
+
connection_string = get_postgres_admin_connection_string(
|
|
103
|
+
ssm_client, secret_name, app, env, addon_name
|
|
104
|
+
)
|
|
105
|
+
|
|
97
106
|
subprocess.call(
|
|
98
107
|
f"copilot task run --app {app.name} --env {env} "
|
|
99
108
|
f"--task-group-name {task_name} "
|
|
@@ -121,7 +130,6 @@ def _temp_until_refactor_get_ecs_task_arns(ecs_client, cluster_arn: str, task_na
|
|
|
121
130
|
|
|
122
131
|
def connect_to_addon_client_task(
|
|
123
132
|
ecs_client,
|
|
124
|
-
subprocess,
|
|
125
133
|
application_name,
|
|
126
134
|
env,
|
|
127
135
|
cluster_arn,
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
|
|
1
3
|
import botocore
|
|
2
4
|
from boto3 import Session
|
|
3
5
|
|
|
6
|
+
from dbt_platform_helper.providers.aws.exceptions import AWSException
|
|
4
7
|
from dbt_platform_helper.providers.aws.exceptions import ImageNotFoundException
|
|
8
|
+
from dbt_platform_helper.providers.aws.exceptions import MultipleImagesFoundException
|
|
5
9
|
from dbt_platform_helper.providers.aws.exceptions import RepositoryNotFoundException
|
|
6
10
|
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
7
|
-
from dbt_platform_helper.utils.application import Application
|
|
8
11
|
from dbt_platform_helper.utils.aws import get_aws_session_or_abort
|
|
9
12
|
|
|
13
|
+
NOT_A_UNIQUE_TAG_INFO = 'INFO: The tag "{image_ref}" is not a unique, commit-specific tag. Deploying the corresponding commit tag "{commit_tag}" instead.'
|
|
14
|
+
NO_ASSOCIATED_COMMIT_TAG_WARNING = 'WARNING: The AWS ECR image "{image_ref}" has no associated commit tag so deploying "{image_ref}". Note this could result in images with unintended or incompatible changes being deployed in new ECS Tasks for your service.'
|
|
15
|
+
|
|
10
16
|
|
|
11
17
|
class ECRProvider:
|
|
12
18
|
def __init__(self, session: Session = None, click_io: ClickIOProvider = ClickIOProvider()):
|
|
@@ -19,49 +25,68 @@ class ECRProvider:
|
|
|
19
25
|
out.extend([repo["repositoryName"] for repo in page.get("repositories", {})])
|
|
20
26
|
return out
|
|
21
27
|
|
|
22
|
-
def
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
def get_commit_tag_for_reference(self, application_name: str, codebase: str, image_ref: str):
|
|
29
|
+
repository = f"{application_name}/{codebase}"
|
|
30
|
+
next_page_token = None
|
|
31
|
+
tag_map = {}
|
|
32
|
+
digest_map = defaultdict(dict)
|
|
27
33
|
|
|
28
|
-
|
|
34
|
+
while True:
|
|
35
|
+
image_list = self._get_ecr_images(repository, image_ref, next_page_token)
|
|
36
|
+
next_page_token = image_list.get("nextToken")
|
|
29
37
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
)
|
|
38
|
+
for image in image_list["imageIds"]:
|
|
39
|
+
digest, tag = image["imageDigest"], image["imageTag"]
|
|
40
|
+
digest_map[digest][tag.split("-")[0]] = tag
|
|
41
|
+
tag_map[tag] = digest
|
|
35
42
|
|
|
36
|
-
|
|
43
|
+
if not next_page_token:
|
|
44
|
+
break
|
|
37
45
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
if image_ref.startswith("commit-"):
|
|
47
|
+
if image_ref in tag_map:
|
|
48
|
+
return image_ref
|
|
49
|
+
else:
|
|
50
|
+
candidates = [
|
|
51
|
+
tag
|
|
52
|
+
for tag in tag_map.keys()
|
|
53
|
+
if image_ref.startswith(tag) or tag.startswith(image_ref)
|
|
54
|
+
]
|
|
55
|
+
if not candidates:
|
|
56
|
+
raise ImageNotFoundException(image_ref)
|
|
57
|
+
if len(candidates) > 1:
|
|
58
|
+
raise MultipleImagesFoundException(image_ref, candidates)
|
|
59
|
+
return candidates[0]
|
|
60
|
+
else:
|
|
61
|
+
digest = tag_map.get(image_ref)
|
|
62
|
+
if not digest:
|
|
41
63
|
raise ImageNotFoundException(image_ref)
|
|
42
|
-
if e.response["Error"]["Code"] == "RepositoryNotFoundException":
|
|
43
|
-
raise RepositoryNotFoundException(repository)
|
|
44
64
|
|
|
45
|
-
|
|
46
|
-
"""Loop through imageTags list to query for an image tag starting with
|
|
47
|
-
'commit-', and return that value if found."""
|
|
65
|
+
commit_tag = digest_map.get(digest, dict()).get("commit")
|
|
48
66
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
if commit_tag:
|
|
68
|
+
self.click_io.info(
|
|
69
|
+
NOT_A_UNIQUE_TAG_INFO.format(image_ref=image_ref, commit_tag=commit_tag)
|
|
70
|
+
)
|
|
71
|
+
return commit_tag
|
|
72
|
+
else:
|
|
73
|
+
self.click_io.warn(NO_ASSOCIATED_COMMIT_TAG_WARNING.format(image_ref=image_ref))
|
|
74
|
+
return image_ref
|
|
75
|
+
|
|
76
|
+
def _get_ecr_images(self, repository, image_ref, next_page_token):
|
|
77
|
+
params = {"repositoryName": repository, "filter": {"tagStatus": "TAGGED"}}
|
|
78
|
+
if next_page_token:
|
|
79
|
+
params["nextToken"] = next_page_token
|
|
80
|
+
try:
|
|
81
|
+
image_list = self._get_client().list_images(**params)
|
|
82
|
+
return image_list
|
|
83
|
+
except botocore.exceptions.ClientError as e:
|
|
84
|
+
if e.response["Error"]["Code"] == "RepositoryNotFoundException":
|
|
85
|
+
raise RepositoryNotFoundException(repository)
|
|
86
|
+
else:
|
|
87
|
+
raise AWSException(
|
|
88
|
+
f"Unexpected error for repo '{repository}' and image reference '{image_ref}': {e}"
|
|
89
|
+
)
|
|
65
90
|
|
|
66
91
|
@staticmethod
|
|
67
92
|
def _check_image_details_exists(image_info: dict, image_ref: str):
|
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import random
|
|
2
2
|
import string
|
|
3
|
-
import
|
|
3
|
+
import subprocess
|
|
4
4
|
from typing import List
|
|
5
5
|
|
|
6
6
|
from dbt_platform_helper.platform_exception import PlatformException
|
|
7
|
+
from dbt_platform_helper.platform_exception import ValidationException
|
|
8
|
+
from dbt_platform_helper.providers.vpc import Vpc
|
|
9
|
+
from dbt_platform_helper.utilities.decorators import retry
|
|
10
|
+
from dbt_platform_helper.utilities.decorators import wait_until
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ECSException(PlatformException):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ECSAgentNotRunningException(ECSException):
|
|
18
|
+
def __init__(self):
|
|
19
|
+
super().__init__("""ECS exec agent never reached "RUNNING" status""")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NoClusterException(ECSException):
|
|
23
|
+
def __init__(self, application_name: str, environment: str):
|
|
24
|
+
super().__init__(
|
|
25
|
+
f"""No ECS cluster found for "{application_name}" in "{environment}" environment."""
|
|
26
|
+
)
|
|
7
27
|
|
|
8
28
|
|
|
9
29
|
class ECS:
|
|
@@ -13,7 +33,49 @@ class ECS:
|
|
|
13
33
|
self.application_name = application_name
|
|
14
34
|
self.env = env
|
|
15
35
|
|
|
16
|
-
def
|
|
36
|
+
def start_ecs_task(
|
|
37
|
+
self,
|
|
38
|
+
cluster_name: str,
|
|
39
|
+
container_name: str,
|
|
40
|
+
task_def_arn: str,
|
|
41
|
+
vpc_config: Vpc,
|
|
42
|
+
env_vars: List[dict] = None,
|
|
43
|
+
):
|
|
44
|
+
container_override = {"name": container_name}
|
|
45
|
+
if env_vars:
|
|
46
|
+
container_override["environment"] = env_vars
|
|
47
|
+
|
|
48
|
+
response = self.ecs_client.run_task(
|
|
49
|
+
taskDefinition=task_def_arn,
|
|
50
|
+
cluster=cluster_name,
|
|
51
|
+
capacityProviderStrategy=[
|
|
52
|
+
{"capacityProvider": "FARGATE", "weight": 1, "base": 0},
|
|
53
|
+
],
|
|
54
|
+
enableExecuteCommand=True,
|
|
55
|
+
networkConfiguration={
|
|
56
|
+
"awsvpcConfiguration": {
|
|
57
|
+
"subnets": vpc_config.public_subnets,
|
|
58
|
+
"securityGroups": vpc_config.security_groups,
|
|
59
|
+
"assignPublicIp": "ENABLED",
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
overrides={"containerOverrides": [container_override]},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return response.get("tasks", [{}])[0].get("taskArn")
|
|
66
|
+
|
|
67
|
+
def get_cluster_arn_by_name(self, cluster_name: str) -> str:
|
|
68
|
+
clusters = self.ecs_client.describe_clusters(
|
|
69
|
+
clusters=[
|
|
70
|
+
cluster_name,
|
|
71
|
+
],
|
|
72
|
+
)["clusters"]
|
|
73
|
+
if len(clusters) == 1 and "clusterArn" in clusters[0]:
|
|
74
|
+
return clusters[0]["clusterArn"]
|
|
75
|
+
|
|
76
|
+
raise NoClusterException(self.application_name, self.env)
|
|
77
|
+
|
|
78
|
+
def get_cluster_arn_by_copilot_tag(self) -> str:
|
|
17
79
|
"""Returns the ARN of the ECS cluster for the given application and
|
|
18
80
|
environment."""
|
|
19
81
|
for cluster_arn in self.ecs_client.list_clusters()["clusterArns"]:
|
|
@@ -45,12 +107,12 @@ class ECS:
|
|
|
45
107
|
random_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=12))
|
|
46
108
|
return f"conduit-{self.application_name}-{self.env}-{addon_name}-{random_id}"
|
|
47
109
|
|
|
48
|
-
def get_ecs_task_arns(self, cluster_arn: str,
|
|
110
|
+
def get_ecs_task_arns(self, cluster_arn: str, task_def_family: str):
|
|
49
111
|
"""Gets the ECS task ARNs for a given task name and cluster ARN."""
|
|
50
112
|
tasks = self.ecs_client.list_tasks(
|
|
51
113
|
cluster=cluster_arn,
|
|
52
114
|
desiredStatus="RUNNING",
|
|
53
|
-
family=
|
|
115
|
+
family=task_def_family,
|
|
54
116
|
)
|
|
55
117
|
|
|
56
118
|
if not tasks["taskArns"]:
|
|
@@ -58,45 +120,55 @@ class ECS:
|
|
|
58
120
|
|
|
59
121
|
return tasks["taskArns"]
|
|
60
122
|
|
|
61
|
-
|
|
123
|
+
@retry()
|
|
124
|
+
def exec_task(self, cluster_arn: str, task_arn: str, subprocess_call=subprocess.call):
|
|
125
|
+
result = subprocess_call(
|
|
126
|
+
f"aws ecs execute-command --cluster {cluster_arn} "
|
|
127
|
+
f"--task {task_arn} "
|
|
128
|
+
f"--interactive --command bash ",
|
|
129
|
+
shell=True,
|
|
130
|
+
)
|
|
131
|
+
if result != 0:
|
|
132
|
+
raise PlatformException("Failed to exec into ECS task.")
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
@wait_until(
|
|
136
|
+
max_attempts=25,
|
|
137
|
+
exceptions_to_catch=(ECSException,),
|
|
138
|
+
message_on_false="ECS Agent Not running",
|
|
139
|
+
)
|
|
140
|
+
def ecs_exec_is_available(self, cluster_arn: str, task_arns: List[str]) -> bool:
|
|
62
141
|
"""
|
|
63
142
|
Checks if the ExecuteCommandAgent is running on the specified ECS task.
|
|
64
143
|
|
|
65
144
|
Waits for up to 25 attempts, then raises ECSAgentNotRunning if still not
|
|
66
145
|
running.
|
|
67
146
|
"""
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
managed_agents =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
class NoClusterException(ECSException):
|
|
99
|
-
def __init__(self, application_name: str, environment: str):
|
|
100
|
-
super().__init__(
|
|
101
|
-
f"""No ECS cluster found for "{application_name}" in "{environment}" environment."""
|
|
102
|
-
)
|
|
147
|
+
if not task_arns:
|
|
148
|
+
raise ValidationException("No task ARNs provided")
|
|
149
|
+
task_details = self.ecs_client.describe_tasks(cluster=cluster_arn, tasks=task_arns)
|
|
150
|
+
|
|
151
|
+
if not task_details["tasks"]:
|
|
152
|
+
raise ECSException("No ECS tasks returned.")
|
|
153
|
+
container_details = task_details["tasks"][0]["containers"][0]
|
|
154
|
+
if container_details.get("managedAgents", None):
|
|
155
|
+
managed_agents = container_details["managedAgents"]
|
|
156
|
+
else:
|
|
157
|
+
raise ECSException("No managed agent on ecs task.")
|
|
158
|
+
|
|
159
|
+
execute_command_agent = [
|
|
160
|
+
agent for agent in managed_agents if agent["name"] == "ExecuteCommandAgent"
|
|
161
|
+
]
|
|
162
|
+
if not execute_command_agent:
|
|
163
|
+
raise ECSException("No ExecuteCommandAgent on ecs task.")
|
|
164
|
+
return execute_command_agent[0]["lastStatus"] == "RUNNING"
|
|
165
|
+
|
|
166
|
+
@wait_until(
|
|
167
|
+
max_attempts=20,
|
|
168
|
+
message_on_false="ECS task did not register in time",
|
|
169
|
+
)
|
|
170
|
+
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)
|
|
172
|
+
if task_arns:
|
|
173
|
+
return task_arns
|
|
174
|
+
return False
|
|
@@ -1,14 +1,19 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
1
3
|
import click
|
|
2
4
|
|
|
3
5
|
from dbt_platform_helper.platform_exception import PlatformException
|
|
4
6
|
|
|
7
|
+
DEBUG = os.environ.get("DEBUG", False)
|
|
8
|
+
|
|
5
9
|
|
|
6
10
|
class ClickIOProvider:
|
|
7
11
|
def warn(self, message: str):
|
|
8
12
|
click.secho(message, fg="magenta")
|
|
9
13
|
|
|
10
14
|
def debug(self, message: str):
|
|
11
|
-
|
|
15
|
+
if DEBUG == "True":
|
|
16
|
+
click.secho(message, fg="green")
|
|
12
17
|
|
|
13
18
|
def error(self, message: str):
|
|
14
19
|
click.secho(f"Error: {message}", fg="red")
|
|
@@ -140,7 +140,12 @@ class TerraformManifestProvider:
|
|
|
140
140
|
def _add_extensions_module(terraform: dict, platform_helper_version: str, env: str):
|
|
141
141
|
source = f"git::https://github.com/uktrade/platform-tools.git//terraform/extensions?depth=1&ref={platform_helper_version}"
|
|
142
142
|
terraform["module"] = {
|
|
143
|
-
"extensions": {
|
|
143
|
+
"extensions": {
|
|
144
|
+
"source": source,
|
|
145
|
+
"args": "${local.args}",
|
|
146
|
+
"environment": env,
|
|
147
|
+
"repos": "${local.codebase_pipeline_repos != null ? (distinct(values(local.codebase_pipeline_repos))) : null}",
|
|
148
|
+
}
|
|
144
149
|
}
|
|
145
150
|
|
|
146
151
|
@staticmethod
|
|
@@ -163,6 +168,7 @@ class TerraformManifestProvider:
|
|
|
163
168
|
"services": '${local.config["extensions"]}',
|
|
164
169
|
"env_config": "${local.env_config}",
|
|
165
170
|
},
|
|
171
|
+
"codebase_pipeline_repos": '${try({for k, v in local.config["codebase_pipelines"]: k => v.repository}, null)}',
|
|
166
172
|
}
|
|
167
173
|
|
|
168
174
|
@staticmethod
|
|
@@ -10,9 +10,9 @@ from requests import Session
|
|
|
10
10
|
from requests.adapters import HTTPAdapter
|
|
11
11
|
from urllib3.util import Retry
|
|
12
12
|
|
|
13
|
+
from dbt_platform_helper.entities.semantic_version import SemanticVersion
|
|
13
14
|
from dbt_platform_helper.platform_exception import PlatformException
|
|
14
15
|
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
15
|
-
from dbt_platform_helper.providers.semantic_version import SemanticVersion
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def set_up_retry():
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
|
|
3
|
+
from dbt_platform_helper.entities.semantic_version import SemanticVersion
|
|
3
4
|
from dbt_platform_helper.platform_exception import PlatformException
|
|
4
|
-
from dbt_platform_helper.providers.semantic_version import SemanticVersion
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class UnsupportedVersionException(PlatformException):
|
|
@@ -77,8 +77,8 @@ class VpcProvider:
|
|
|
77
77
|
return vpc_id
|
|
78
78
|
|
|
79
79
|
def _get_security_groups(self, app: str, env: str, vpc_id: str) -> list:
|
|
80
|
-
|
|
81
80
|
vpc_filter = {"Name": "vpc-id", "Values": [vpc_id]}
|
|
81
|
+
# TODO Handle terraformed environment SG https://uktrade.atlassian.net/browse/DBTP-2074
|
|
82
82
|
tag_filter = {"Name": f"tag:Name", "Values": [f"copilot-{app}-{env}-env"]}
|
|
83
83
|
response = self.ec2_client.describe_security_groups(Filters=[vpc_filter, tag_filter])
|
|
84
84
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import time
|
|
3
|
+
from typing import Callable
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
7
|
+
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
8
|
+
|
|
9
|
+
SECONDS_BEFORE_RETRY = 3
|
|
10
|
+
RETRY_MAX_ATTEMPTS = 3
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RetryException(PlatformException):
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self, function_name: str, max_attempts: int, original_exception: Optional[Exception] = None
|
|
17
|
+
):
|
|
18
|
+
message = f"Function: {function_name} failed after {max_attempts} attempts"
|
|
19
|
+
self.original_exception = original_exception
|
|
20
|
+
if original_exception:
|
|
21
|
+
message += f": \n{str(original_exception)}"
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def retry(
|
|
26
|
+
exceptions_to_catch: tuple = (Exception,),
|
|
27
|
+
max_attempts: int = RETRY_MAX_ATTEMPTS,
|
|
28
|
+
delay: float = SECONDS_BEFORE_RETRY,
|
|
29
|
+
raise_custom_exception: bool = True,
|
|
30
|
+
custom_exception: type = RetryException,
|
|
31
|
+
io: ClickIOProvider = ClickIOProvider(),
|
|
32
|
+
):
|
|
33
|
+
def decorator(func):
|
|
34
|
+
func.__wrapped_by__ = "retry"
|
|
35
|
+
|
|
36
|
+
@functools.wraps(func)
|
|
37
|
+
def wrapper(*args, **kwargs):
|
|
38
|
+
last_exception = None
|
|
39
|
+
for attempt in range(max_attempts):
|
|
40
|
+
try:
|
|
41
|
+
return func(*args, **kwargs)
|
|
42
|
+
except exceptions_to_catch as e:
|
|
43
|
+
last_exception = e
|
|
44
|
+
io.debug(
|
|
45
|
+
f"Attempt {attempt+1}/{max_attempts} for {func.__name__} failed with exception {str(last_exception)}"
|
|
46
|
+
)
|
|
47
|
+
if attempt < max_attempts - 1:
|
|
48
|
+
time.sleep(delay)
|
|
49
|
+
if raise_custom_exception:
|
|
50
|
+
raise custom_exception(func.__name__, max_attempts, last_exception)
|
|
51
|
+
raise last_exception
|
|
52
|
+
|
|
53
|
+
return wrapper
|
|
54
|
+
|
|
55
|
+
return decorator
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def wait_until(
|
|
59
|
+
exceptions_to_catch: tuple = (PlatformException,),
|
|
60
|
+
max_attempts: int = RETRY_MAX_ATTEMPTS,
|
|
61
|
+
delay: float = SECONDS_BEFORE_RETRY,
|
|
62
|
+
raise_custom_exception: bool = True,
|
|
63
|
+
custom_exception=RetryException,
|
|
64
|
+
message_on_false="Condition not met",
|
|
65
|
+
io: ClickIOProvider = ClickIOProvider(),
|
|
66
|
+
):
|
|
67
|
+
"""Wrap a function which returns a boolean."""
|
|
68
|
+
|
|
69
|
+
def decorator(func: Callable[..., bool]):
|
|
70
|
+
func.__wrapped_by__ = "wait_until"
|
|
71
|
+
|
|
72
|
+
@functools.wraps(func)
|
|
73
|
+
def wrapper(*args, **kwargs):
|
|
74
|
+
last_exception = None
|
|
75
|
+
for attempt in range(max_attempts):
|
|
76
|
+
try:
|
|
77
|
+
result = func(*args, **kwargs)
|
|
78
|
+
if result:
|
|
79
|
+
return result
|
|
80
|
+
io.debug(
|
|
81
|
+
f"Attempt {attempt+1}/{max_attempts} for {func.__name__} returned falsy"
|
|
82
|
+
)
|
|
83
|
+
except exceptions_to_catch as e:
|
|
84
|
+
last_exception = e
|
|
85
|
+
io.debug(
|
|
86
|
+
f"Attempt {attempt+1}/{max_attempts} for {func.__name__} failed with exception {str(last_exception)}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if attempt < max_attempts - 1:
|
|
90
|
+
time.sleep(delay)
|
|
91
|
+
|
|
92
|
+
if not last_exception: # If func returns false set last_exception
|
|
93
|
+
last_exception = PlatformException(message_on_false)
|
|
94
|
+
if (
|
|
95
|
+
not raise_custom_exception
|
|
96
|
+
): # Raise last_exception when you don't want custom exception
|
|
97
|
+
raise last_exception
|
|
98
|
+
else:
|
|
99
|
+
raise custom_exception(func.__name__, max_attempts, last_exception)
|
|
100
|
+
|
|
101
|
+
return wrapper
|
|
102
|
+
|
|
103
|
+
return decorator
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from schema import SchemaError
|
|
2
2
|
|
|
3
|
+
from dbt_platform_helper.entities.platform_config_schema import PlatformConfigSchema
|
|
3
4
|
from dbt_platform_helper.providers.config_validator import ConfigValidator
|
|
4
|
-
from dbt_platform_helper.providers.platform_config_schema import PlatformConfigSchema
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def validate_addons(addons: dict):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: dbt-platform-helper
|
|
3
|
-
Version: 15.
|
|
3
|
+
Version: 15.2.0
|
|
4
4
|
Summary: Set of tools to help transfer applications/services from GOV.UK PaaS to DBT PaaS augmenting AWS Copilot.
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Department for Business and Trade Platform Team
|
|
@@ -18,9 +18,9 @@ dbt_platform_helper/commands/version.py,sha256=2GltWeeN7cqhVj9FhYWSbXSQSyNILHVNO
|
|
|
18
18
|
dbt_platform_helper/constants.py,sha256=Ao2uvVRcxRN5SXqvW6Jq2srd7LuyGz1jPy4fg2N6XSk,1153
|
|
19
19
|
dbt_platform_helper/default-extensions.yml,sha256=SU1ZitskbuEBpvE7efc3s56eAUF11j70brhj_XrNMMo,493
|
|
20
20
|
dbt_platform_helper/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
dbt_platform_helper/domain/codebase.py,sha256=
|
|
22
|
-
dbt_platform_helper/domain/conduit.py,sha256=
|
|
23
|
-
dbt_platform_helper/domain/config.py,sha256=
|
|
21
|
+
dbt_platform_helper/domain/codebase.py,sha256=2hJoBiDB2ciOudT_YUR44XV0ZQPWUJld_UIuds4XOt8,12481
|
|
22
|
+
dbt_platform_helper/domain/conduit.py,sha256=0aX5rhynkkJj8rJUwfyLENyCwlAI67_Vkky1lOEl6rw,12496
|
|
23
|
+
dbt_platform_helper/domain/config.py,sha256=Iyf-lV4YDD6BHH-RRaTvp-7qPS8BYeHM_SkSfeU7si4,13802
|
|
24
24
|
dbt_platform_helper/domain/copilot.py,sha256=9L4h-WFwgRU8AMjf14PlDqwLqOpIRinkuPvhe-8Uk3c,15034
|
|
25
25
|
dbt_platform_helper/domain/copilot_environment.py,sha256=fL3XJCOfO0BJRCrCoBPFCcshrQoX1FeSYNTziOEaH4A,9093
|
|
26
26
|
dbt_platform_helper/domain/database_copy.py,sha256=AedcBTfKDod0OlMqVP6zb9c_9VIc3vqro0oUUhh7nwc,9497
|
|
@@ -28,40 +28,40 @@ dbt_platform_helper/domain/maintenance_page.py,sha256=0_dgM5uZvjVNBKcqScspjutinM
|
|
|
28
28
|
dbt_platform_helper/domain/notify.py,sha256=_BWj5znDWtrSdJ5xzDBgnao4ukliBA5wiUZGobIDyiI,1894
|
|
29
29
|
dbt_platform_helper/domain/pipelines.py,sha256=BUoXlV4pIKSw3Ry6oVMzd0mBU6tfl_tvqp-1zxHrQdk,6552
|
|
30
30
|
dbt_platform_helper/domain/terraform_environment.py,sha256=kPfA44KCNnF_7ihQPuxaShLjEnVShrbruLwr5xoCeRc,1825
|
|
31
|
-
dbt_platform_helper/domain/versioning.py,sha256=
|
|
31
|
+
dbt_platform_helper/domain/versioning.py,sha256=pIL8VPAJHqX5kJBp3QIxII5vmUo4aIYW_U9u_KxUJd0,5494
|
|
32
|
+
dbt_platform_helper/entities/platform_config_schema.py,sha256=ADkEP5PEjZswBKuPvpi1QHW_dXiC-CIAx730c11Uio0,27544
|
|
33
|
+
dbt_platform_helper/entities/semantic_version.py,sha256=VgQ6V6OgSaleuVmMB8Kl_yLoakXl2auapJTDbK00mfc,2679
|
|
32
34
|
dbt_platform_helper/jinja2_tags.py,sha256=hKG6RS3zlxJHQ-Op9r2U2-MhWp4s3lZir4Ihe24ApJ0,540
|
|
33
|
-
dbt_platform_helper/platform_exception.py,sha256=
|
|
35
|
+
dbt_platform_helper/platform_exception.py,sha256=HGfCYRD20REsynqMKmyZndTfdkMd5dLSIEB2qGGCeP8,244
|
|
34
36
|
dbt_platform_helper/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
37
|
dbt_platform_helper/providers/aws/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
|
-
dbt_platform_helper/providers/aws/exceptions.py,sha256=
|
|
38
|
+
dbt_platform_helper/providers/aws/exceptions.py,sha256=7zrbzGuZhZypmT7LB4pK7TrYD4kn5H0L_lgx07o2VPg,2332
|
|
37
39
|
dbt_platform_helper/providers/aws/interfaces.py,sha256=0JFggcUTJ8zERdxNVVpIiKvaaZeT2c-VECDG--MOi8E,285
|
|
38
40
|
dbt_platform_helper/providers/aws/opensearch.py,sha256=Qne2SoPllmacVSc7AxtjBlEbSBsRMbR_ySEkEymSF9k,581
|
|
39
41
|
dbt_platform_helper/providers/aws/redis.py,sha256=i3Kb00_BdqssjQg1wgZ-8GRXcEWQiORWnIEq6qkAXjQ,551
|
|
40
42
|
dbt_platform_helper/providers/aws/sso_auth.py,sha256=1cE9gVu0XZoE3Nycs1anShistRU_CZCOGMFzFpaAq0w,2275
|
|
41
43
|
dbt_platform_helper/providers/cache.py,sha256=1hEwp0y9WYbEfgsp-RU9MyzIgCt1-4BxApgd_0uVweE,3615
|
|
42
44
|
dbt_platform_helper/providers/cloudformation.py,sha256=syMH6xc-ALRbsYQvlw9RcjX7c1MufFzwEdEzp_ucWig,5359
|
|
43
|
-
dbt_platform_helper/providers/config.py,sha256=
|
|
45
|
+
dbt_platform_helper/providers/config.py,sha256=8eK6txDTTF3s3iy7WxKszlaE33pHVhbJ55UDUJ9_nYw,9861
|
|
44
46
|
dbt_platform_helper/providers/config_validator.py,sha256=uF1GB-fl0ZuXVCtLNANgnY22UbiWZniBg1PiXgzGzuU,9923
|
|
45
|
-
dbt_platform_helper/providers/copilot.py,sha256=
|
|
46
|
-
dbt_platform_helper/providers/ecr.py,sha256=
|
|
47
|
-
dbt_platform_helper/providers/ecs.py,sha256=
|
|
47
|
+
dbt_platform_helper/providers/copilot.py,sha256=voFVGhvtOElulx6Cgd1KQGkybrg8v4oGkJTr_xRpF18,5582
|
|
48
|
+
dbt_platform_helper/providers/ecr.py,sha256=eYXSY1-pFN6F3Es1WSZgv3dmvX2oD-baqhHDO-QzgVg,4382
|
|
49
|
+
dbt_platform_helper/providers/ecs.py,sha256=4XRpOgcl7KFiTp9lhNrp4Lvmje0ZFYuUh9Z_eEqhyhA,6538
|
|
48
50
|
dbt_platform_helper/providers/files.py,sha256=cJdOV6Eupi-COmGUMxZMF10BZnMi3MCCipTVUnE_NPA,857
|
|
49
|
-
dbt_platform_helper/providers/io.py,sha256=
|
|
51
|
+
dbt_platform_helper/providers/io.py,sha256=5C7XUxy3XNqSWgxryr4Uy0l4J9np_lryGskqE0TRpmQ,1459
|
|
50
52
|
dbt_platform_helper/providers/kms.py,sha256=JR2EU3icXePoJCtr7QnqDPj1wWbyn5Uf9CRFq3_4lRs,647
|
|
51
53
|
dbt_platform_helper/providers/load_balancers.py,sha256=G-gqhthaO6ZmpKq6zAqnY1AUtc5YjnI99sQzpeaM0ec,10644
|
|
52
54
|
dbt_platform_helper/providers/parameter_store.py,sha256=klxDhcQ65Yc2KAc4Gf5P0vhpZOW7_vZalAVb-LLAA4s,1568
|
|
53
|
-
dbt_platform_helper/providers/platform_config_schema.py,sha256=ADkEP5PEjZswBKuPvpi1QHW_dXiC-CIAx730c11Uio0,27544
|
|
54
55
|
dbt_platform_helper/providers/schema_migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
55
56
|
dbt_platform_helper/providers/schema_migrations/schema_v0_to_v1_migration.py,sha256=dplbvEAc8l8F4dEAy3JwLP1Phjkt4QVuQYNX_EKe_Ls,2036
|
|
56
57
|
dbt_platform_helper/providers/schema_migrator.py,sha256=qk14k3hMz1av9VrxHyJw2OKJLQnCBv_ugOoxZr3tFXQ,2854
|
|
57
58
|
dbt_platform_helper/providers/secrets.py,sha256=mOTIrcRRxxV2tS40U8onAjWekfPS3NzCvvyCMjr_yrU,5327
|
|
58
|
-
dbt_platform_helper/providers/semantic_version.py,sha256=VgQ6V6OgSaleuVmMB8Kl_yLoakXl2auapJTDbK00mfc,2679
|
|
59
59
|
dbt_platform_helper/providers/slack_channel_notifier.py,sha256=G8etEcaBQSNHg8BnyC5UPv6l3vUB14cYWjcaAQksaEk,2135
|
|
60
|
-
dbt_platform_helper/providers/terraform_manifest.py,sha256=
|
|
60
|
+
dbt_platform_helper/providers/terraform_manifest.py,sha256=gfluve8mcHSkZqq4nNhazte48I7LFbJoQAVBMJBnNk4,9660
|
|
61
61
|
dbt_platform_helper/providers/validation.py,sha256=i2g-Mrd4hy_fGIfGa6ZQy4vTJ40OM44Fe_XpEifGWxs,126
|
|
62
|
-
dbt_platform_helper/providers/version.py,sha256=
|
|
63
|
-
dbt_platform_helper/providers/version_status.py,sha256
|
|
64
|
-
dbt_platform_helper/providers/vpc.py,sha256=
|
|
62
|
+
dbt_platform_helper/providers/version.py,sha256=QNGrV5nyJi0JysXowYUU4OrXGDn27WmFezlV8benpdY,4251
|
|
63
|
+
dbt_platform_helper/providers/version_status.py,sha256=qafnhZrEc9k1cvXJpvJhkGj6WtkzcsoQhqS_Y6JXy48,929
|
|
64
|
+
dbt_platform_helper/providers/vpc.py,sha256=V8kXXzy-JuRpuzZhI9xyfcNky-eD42K0v_uM2WejLoo,3048
|
|
65
65
|
dbt_platform_helper/providers/yaml_file.py,sha256=LZ8eCPDQRr1wlck13My5hQa0eE2OVhSomm-pOIuZ9h0,2881
|
|
66
66
|
dbt_platform_helper/templates/.copilot/config.yml,sha256=J_bA9sCtBdCPBRImpCBRnYvhQd4vpLYIXIU-lq9vbkA,158
|
|
67
67
|
dbt_platform_helper/templates/.copilot/image_build_run.sh,sha256=adYucYXEB-kAgZNjTQo0T6EIAY8sh_xCEvVhWKKQ8mw,164
|
|
@@ -87,6 +87,7 @@ dbt_platform_helper/templates/svc/maintenance_pages/default.html,sha256=OTZ-qwwS
|
|
|
87
87
|
dbt_platform_helper/templates/svc/maintenance_pages/dmas-migration.html,sha256=qvI6tHuI0UQbMBCuvPgK1a_zLANB6w7KVo9N5d8r-i0,829
|
|
88
88
|
dbt_platform_helper/templates/svc/maintenance_pages/migration.html,sha256=GiQsOiuaMFb7jG5_wU3V7BMcByHBl9fOBgrNf8quYlw,783
|
|
89
89
|
dbt_platform_helper/templates/svc/overrides/cfn.patches.yml,sha256=W7-d017akuUq9kda64DQxazavcRcCPDjaAik6t1EZqM,742
|
|
90
|
+
dbt_platform_helper/utilities/decorators.py,sha256=rS6ohsuo0bc6fkZP98Qwaeh0c_v2MDqn9hCvqfoz2w8,3548
|
|
90
91
|
dbt_platform_helper/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
91
92
|
dbt_platform_helper/utils/application.py,sha256=d7Tg5odZMy9e3o4R0mmU19hrxJuIfS_ATDH4hY8zJvk,5480
|
|
92
93
|
dbt_platform_helper/utils/arn_parser.py,sha256=BaXzIxSOLdFmP_IfAxRq-0j-0Re1iCN7L4j2Zi5-CRQ,1304
|
|
@@ -95,10 +96,10 @@ dbt_platform_helper/utils/click.py,sha256=Fx4y4bbve1zypvog_sgK7tJtCocmzheoEFLBRv
|
|
|
95
96
|
dbt_platform_helper/utils/git.py,sha256=9jyLhv37KKE6r-_hb3zvjhTbluA81kdrOdNeG6MB-_M,384
|
|
96
97
|
dbt_platform_helper/utils/messages.py,sha256=nWA7BWLb7ND0WH5TejDN4OQUJSKYBxU4tyCzteCrfT0,142
|
|
97
98
|
dbt_platform_helper/utils/template.py,sha256=g-Db-0I6a6diOHkgK1nYA0IxJSO4TRrjqOvlyeOR32o,950
|
|
98
|
-
dbt_platform_helper/utils/validation.py,sha256=
|
|
99
|
+
dbt_platform_helper/utils/validation.py,sha256=W5jKC2zp5Q7cJ0PT57GB-s9FkJXrNt1jmWojXRFymcY,1187
|
|
99
100
|
platform_helper.py,sha256=_YNNGtMkH5BcpC_mQQYJrmlf2mt7lkxTYeH7ZgflPoA,1925
|
|
100
|
-
dbt_platform_helper-15.
|
|
101
|
-
dbt_platform_helper-15.
|
|
102
|
-
dbt_platform_helper-15.
|
|
103
|
-
dbt_platform_helper-15.
|
|
104
|
-
dbt_platform_helper-15.
|
|
101
|
+
dbt_platform_helper-15.2.0.dist-info/LICENSE,sha256=dP79lN73--7LMApnankTGLqDbImXg8iYFqWgnExGkGk,1090
|
|
102
|
+
dbt_platform_helper-15.2.0.dist-info/METADATA,sha256=ypOBRkc3nhUh287vmFdYew3x44PGZ1VC57TKjd5e3Ds,3293
|
|
103
|
+
dbt_platform_helper-15.2.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
104
|
+
dbt_platform_helper-15.2.0.dist-info/entry_points.txt,sha256=QhbY8F434A-onsg0-FsdMd2U6HKh6Q7yCFFZrGUh5-M,67
|
|
105
|
+
dbt_platform_helper-15.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dbt_platform_helper-15.0.0.dist-info → dbt_platform_helper-15.2.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|