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.

@@ -167,13 +167,16 @@ class Codebase:
167
167
 
168
168
  image_ref = None
169
169
  if commit:
170
- image_ref = f"commit-{commit[0:7]}"
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
- image_details = self.ecr_provider.get_image_details(application, codebase, image_ref)
176
- image_ref = self.ecr_provider.find_commit_tag(image_details, image_ref)
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 subprocess
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 Conduit:
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
- ecs_provider: ECS,
19
- io: ClickIOProvider = ClickIOProvider(),
20
- subprocess: subprocess = subprocess,
21
- connect_to_addon_client_task=connect_to_addon_client_task,
22
- create_addon_client_task=create_addon_client_task,
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.subprocess = subprocess
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 start(self, env: str, addon_name: str, access: str = "read"):
35
- clients = self._initialise_clients(env)
36
- addon_type, cluster_arn, parameter_name, task_name = self._get_addon_details(
37
- addon_name, access
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
- self.io.info(f"Checking if a conduit task is already running for {addon_type}")
41
- task_arns = self.ecs_provider.get_ecs_task_arns(cluster_arn, task_name)
42
- if not task_arns:
43
- self.io.info("Creating conduit task")
44
- self.create_addon_client_task(
45
- clients["iam"],
46
- clients["ssm"],
47
- self.subprocess,
48
- self.application,
49
- env,
50
- addon_type,
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
- task_name,
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
- addon_type,
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
- else:
70
- self.io.info("Conduit task already running")
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
- self.io.info(f"Checking if exec is available for conduit task...")
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
- self.ecs_provider.ecs_exec_is_available(cluster_arn, task_arns)
299
+ if (addon_type == "opensearch" or addon_type == "redis") and (access != "read"):
300
+ access = "read"
75
301
 
76
- self.io.info("Connecting to conduit task")
77
- self.connect_to_addon_client_task(
78
- clients["ecs"], self.subprocess, self.application.name, env, cluster_arn, task_name
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.platform_exception import PlatformException
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.providers.semantic_version import (
6
+ from dbt_platform_helper.entities.semantic_version import (
10
7
  IncompatibleMinorVersionException,
11
8
  )
12
- from dbt_platform_helper.providers.semantic_version import SemanticVersion
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
@@ -3,3 +3,7 @@
3
3
  # error and abort.
4
4
  class PlatformException(Exception):
5
5
  pass
6
+
7
+
8
+ class ValidationException(PlatformException):
9
+ pass
@@ -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
- f"""An image labelled "{image_ref}" could not be found in your image repository. Try the `platform-helper codebase build` command first."""
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__(f"""The ECR repository "{repository}" could not be found.""")
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 create_postgres_admin_task(
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 get_image_details(
23
- self, application: Application, codebase: str, image_ref: str
24
- ) -> list[dict]:
25
- """Check if image exists in AWS ECR, and return a list of dictionaries
26
- containing image metadata."""
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
- repository = f"{application.name}/{codebase}"
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
- try:
31
- image_info = self._get_client().describe_images(
32
- repositoryName=repository,
33
- imageIds=[{"imageTag": image_ref}],
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
- self._check_image_details_exists(image_info, image_ref)
43
+ if not next_page_token:
44
+ break
37
45
 
38
- return image_info.get("imageDetails")
39
- except botocore.exceptions.ClientError as e:
40
- if e.response["Error"]["Code"] == "ImageNotFoundException":
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
- def find_commit_tag(self, image_details: list[dict], image_ref: str) -> str:
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
- if image_ref.startswith("commit-"):
50
- return image_ref
51
-
52
- if image_details:
53
- for image in image_details:
54
- image_tags = image.get("imageTags", {})
55
- for tag in image_tags:
56
- if tag.startswith("commit-"):
57
- self.click_io.info(
58
- f'INFO: The tag "{image_ref}" is not a unique, commit-specific tag. Deploying the corresponding commit tag "{tag}" instead.'
59
- )
60
- return tag
61
- self.click_io.warn(
62
- f'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 if new ECS Tasks for your service.'
63
- )
64
- return image_ref
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 time
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 get_cluster_arn(self) -> str:
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, task_name: 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=f"copilot-{task_name}",
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
- def ecs_exec_is_available(self, cluster_arn: str, task_arns: List[str]):
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
- current_attempts = 0
69
- execute_command_agent_status = ""
70
-
71
- while execute_command_agent_status != "RUNNING" and current_attempts < 25:
72
- current_attempts += 1
73
-
74
- task_details = self.ecs_client.describe_tasks(cluster=cluster_arn, tasks=task_arns)
75
-
76
- managed_agents = task_details["tasks"][0]["containers"][0]["managedAgents"]
77
- execute_command_agent_status = [
78
- agent["lastStatus"]
79
- for agent in managed_agents
80
- if agent["name"] == "ExecuteCommandAgent"
81
- ][0]
82
- if execute_command_agent_status != "RUNNING":
83
- time.sleep(1)
84
-
85
- if execute_command_agent_status != "RUNNING":
86
- raise ECSAgentNotRunningException
87
-
88
-
89
- class ECSException(PlatformException):
90
- pass
91
-
92
-
93
- class ECSAgentNotRunningException(ECSException):
94
- def __init__(self):
95
- super().__init__("""ECS exec agent never reached "RUNNING" status""")
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
- click.secho(message, fg="green")
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": {"source": source, "args": "${local.args}", "environment": env}
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.0.0
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=9BTs7PNYaBRqthv5bAR6_dtiwmAQ1CIZjt3jN21aQv8,12228
22
- dbt_platform_helper/domain/conduit.py,sha256=5C5GnF5jssJ1rFFCY6qaKgvLjYUkyytRJE4tHlanYa0,4370
23
- dbt_platform_helper/domain/config.py,sha256=NtWAPkopwclKajYzkqbTOQtaMAy2KMU3hc77biBC2eM,13804
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=kqx7P4iSjQxXJ58cTCdxTcTcJ-kYX7G28jMMlp_Zvc8,5497
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=bheZV9lqGvrCVTNT92349dVntNDEDWTEwciZgC83WzE,187
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=TOaUdTySNGZ9fU3kofgIj6upMmd4IT2f0rpbn8YR6fs,1742
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=7Mq0i-cg_YeAbWOlnDI0GGpszDNPRvFZiX49ETR3YDE,9863
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=eruF_ZWWtrJFNQVuzXRMRfqOWGCXpvR7yu50olU4Nk8,5362
46
- dbt_platform_helper/providers/ecr.py,sha256=siCGTEXR8Jd_pemPfKI3_U5P3Ix6dPrhWsg94EQiZzA,3266
47
- dbt_platform_helper/providers/ecs.py,sha256=XlQHYhZiLGrqR-1ZWMagGH2R2Hy7mCP6676eZL3YbNQ,3842
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=tU0jK8krKvBmdGM-sQXpFEqcUxORjFKFIdMNIe3TKB0,1376
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=otqVh_0KCqP35bZstTzd-TEEe0BYvEWmVn_quYumiNs,9345
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=pU1dCXo05Jm0Np6u6HiFFfpHEHNtWyU9fSXDZMqmRpU,4252
63
- dbt_platform_helper/providers/version_status.py,sha256=-y9-kNvXGHVT6IbMOVu30vNyPOEShH-rAVMFHlJek1U,930
64
- dbt_platform_helper/providers/vpc.py,sha256=EIjjD71K1Ry3V1jyaAkAjZwlwu_FSTn-AS7kiJFiipA,2953
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=coN7WsKW_nPGW9EU23AInBkAuvUl1NfQvc2bjVtgs14,1188
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.0.0.dist-info/LICENSE,sha256=dP79lN73--7LMApnankTGLqDbImXg8iYFqWgnExGkGk,1090
101
- dbt_platform_helper-15.0.0.dist-info/METADATA,sha256=t2pSFArbvOvniLssLuR9bq9ATpt_9TzVs8et5x_6xX8,3293
102
- dbt_platform_helper-15.0.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
103
- dbt_platform_helper-15.0.0.dist-info/entry_points.txt,sha256=QhbY8F434A-onsg0-FsdMd2U6HKh6Q7yCFFZrGUh5-M,67
104
- dbt_platform_helper-15.0.0.dist-info/RECORD,,
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,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.2
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any