dbt-platform-helper 13.1.0__py3-none-any.whl → 15.16.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dbt_platform_helper/COMMANDS.md +107 -27
- dbt_platform_helper/commands/application.py +5 -6
- dbt_platform_helper/commands/codebase.py +31 -10
- dbt_platform_helper/commands/conduit.py +3 -5
- dbt_platform_helper/commands/config.py +20 -311
- dbt_platform_helper/commands/copilot.py +18 -391
- dbt_platform_helper/commands/database.py +17 -9
- dbt_platform_helper/commands/environment.py +20 -14
- dbt_platform_helper/commands/generate.py +0 -3
- dbt_platform_helper/commands/internal.py +140 -0
- dbt_platform_helper/commands/notify.py +58 -78
- dbt_platform_helper/commands/pipeline.py +23 -19
- dbt_platform_helper/commands/secrets.py +39 -93
- dbt_platform_helper/commands/version.py +7 -12
- dbt_platform_helper/constants.py +52 -7
- dbt_platform_helper/domain/codebase.py +89 -39
- dbt_platform_helper/domain/conduit.py +335 -76
- dbt_platform_helper/domain/config.py +381 -0
- dbt_platform_helper/domain/copilot.py +398 -0
- dbt_platform_helper/domain/copilot_environment.py +8 -8
- dbt_platform_helper/domain/database_copy.py +2 -2
- dbt_platform_helper/domain/maintenance_page.py +254 -430
- dbt_platform_helper/domain/notify.py +64 -0
- dbt_platform_helper/domain/pipelines.py +43 -35
- dbt_platform_helper/domain/plans.py +41 -0
- dbt_platform_helper/domain/secrets.py +279 -0
- dbt_platform_helper/domain/service.py +570 -0
- dbt_platform_helper/domain/terraform_environment.py +14 -13
- dbt_platform_helper/domain/update_alb_rules.py +412 -0
- dbt_platform_helper/domain/versioning.py +249 -0
- dbt_platform_helper/{providers → entities}/platform_config_schema.py +75 -82
- dbt_platform_helper/entities/semantic_version.py +83 -0
- dbt_platform_helper/entities/service.py +339 -0
- dbt_platform_helper/platform_exception.py +4 -0
- dbt_platform_helper/providers/autoscaling.py +24 -0
- dbt_platform_helper/providers/aws/__init__.py +0 -0
- dbt_platform_helper/providers/aws/exceptions.py +70 -0
- dbt_platform_helper/providers/aws/interfaces.py +13 -0
- dbt_platform_helper/providers/aws/opensearch.py +23 -0
- dbt_platform_helper/providers/aws/redis.py +21 -0
- dbt_platform_helper/providers/aws/sso_auth.py +75 -0
- dbt_platform_helper/providers/cache.py +40 -4
- dbt_platform_helper/providers/cloudformation.py +1 -1
- dbt_platform_helper/providers/config.py +137 -19
- dbt_platform_helper/providers/config_validator.py +112 -51
- dbt_platform_helper/providers/copilot.py +24 -16
- dbt_platform_helper/providers/ecr.py +89 -7
- dbt_platform_helper/providers/ecs.py +228 -36
- dbt_platform_helper/providers/environment_variable.py +24 -0
- dbt_platform_helper/providers/files.py +1 -1
- dbt_platform_helper/providers/io.py +36 -4
- dbt_platform_helper/providers/kms.py +22 -0
- dbt_platform_helper/providers/load_balancers.py +402 -42
- dbt_platform_helper/providers/logs.py +72 -0
- dbt_platform_helper/providers/parameter_store.py +134 -0
- dbt_platform_helper/providers/s3.py +21 -0
- dbt_platform_helper/providers/schema_migrations/__init__.py +0 -0
- dbt_platform_helper/providers/schema_migrations/schema_v0_to_v1_migration.py +43 -0
- dbt_platform_helper/providers/schema_migrator.py +77 -0
- dbt_platform_helper/providers/secrets.py +5 -5
- dbt_platform_helper/providers/slack_channel_notifier.py +62 -0
- dbt_platform_helper/providers/terraform_manifest.py +121 -19
- dbt_platform_helper/providers/version.py +106 -23
- dbt_platform_helper/providers/version_status.py +27 -0
- dbt_platform_helper/providers/vpc.py +36 -5
- dbt_platform_helper/providers/yaml_file.py +58 -2
- dbt_platform_helper/templates/environment-pipelines/main.tf +4 -3
- dbt_platform_helper/templates/svc/overrides/cfn.patches.yml +5 -0
- dbt_platform_helper/utilities/decorators.py +103 -0
- dbt_platform_helper/utils/application.py +119 -22
- dbt_platform_helper/utils/aws.py +39 -150
- dbt_platform_helper/utils/deep_merge.py +10 -0
- dbt_platform_helper/utils/git.py +1 -14
- dbt_platform_helper/utils/validation.py +1 -1
- {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/METADATA +11 -20
- dbt_platform_helper-15.16.0.dist-info/RECORD +118 -0
- {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/WHEEL +1 -1
- platform_helper.py +3 -1
- terraform/elasticache-redis/plans.yml +85 -0
- terraform/opensearch/plans.yml +71 -0
- terraform/postgres/plans.yml +128 -0
- dbt_platform_helper/addon-plans.yml +0 -224
- dbt_platform_helper/providers/aws.py +0 -37
- dbt_platform_helper/providers/opensearch.py +0 -36
- dbt_platform_helper/providers/redis.py +0 -34
- dbt_platform_helper/providers/semantic_version.py +0 -126
- dbt_platform_helper/templates/svc/manifest-backend.yml +0 -69
- dbt_platform_helper/templates/svc/manifest-public.yml +0 -109
- dbt_platform_helper/utils/cloudfoundry.py +0 -14
- dbt_platform_helper/utils/files.py +0 -53
- dbt_platform_helper/utils/manifests.py +0 -18
- dbt_platform_helper/utils/versioning.py +0 -238
- dbt_platform_helper-13.1.0.dist-info/RECORD +0 -96
- {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/entry_points.txt +0 -0
- {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
from typing import Dict
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
from pydantic import Field
|
|
10
|
+
from pydantic import field_validator
|
|
11
|
+
from pydantic import model_validator
|
|
12
|
+
|
|
13
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HealthCheck(BaseModel):
|
|
17
|
+
path: Optional[str] = Field(
|
|
18
|
+
description="The destination that the health check requests are sent to.", default="/"
|
|
19
|
+
)
|
|
20
|
+
port: Optional[int] = Field(
|
|
21
|
+
description="The port that the health check requests are sent to.", default=8080
|
|
22
|
+
)
|
|
23
|
+
success_codes: Optional[str] = Field(
|
|
24
|
+
description="A comma-separated list of HTTP status codes that healthy targets must use when responding to a HTTP health check.",
|
|
25
|
+
default="200",
|
|
26
|
+
)
|
|
27
|
+
healthy_threshold: Optional[int] = Field(
|
|
28
|
+
description="The number of consecutive health check successes required before considering an unhealthy target healthy.",
|
|
29
|
+
default=3,
|
|
30
|
+
)
|
|
31
|
+
unhealthy_threshold: Optional[int] = Field(
|
|
32
|
+
description="The number of consecutive health check failures required before considering a target unhealthy.",
|
|
33
|
+
default=3,
|
|
34
|
+
)
|
|
35
|
+
interval: Optional[int] = Field(
|
|
36
|
+
description="The approximate amount of time, in seconds, between health checks of an individual target.",
|
|
37
|
+
default=35,
|
|
38
|
+
)
|
|
39
|
+
timeout: Optional[int] = Field(
|
|
40
|
+
description="The amount of time, in seconds, during which no response from a target means a failed health check.",
|
|
41
|
+
default=30,
|
|
42
|
+
)
|
|
43
|
+
grace_period: Optional[int] = Field(
|
|
44
|
+
description="The amount of time to ignore failing target group healthchecks on container start.",
|
|
45
|
+
default=30,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AdditionalRules(BaseModel):
|
|
50
|
+
path: str = Field(description="""Requests to this path will be forwarded to your service.""")
|
|
51
|
+
alias: list[str] = Field(description="""The HTTP domain alias of the service.""")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Http(BaseModel):
|
|
55
|
+
alias: list[str] = Field(
|
|
56
|
+
description="List of HTTPS domain alias(es) of your service.", default=None
|
|
57
|
+
)
|
|
58
|
+
stickiness: Optional[bool] = Field(description="Enable sticky sessions.", default=False)
|
|
59
|
+
path: str = Field(description="Requests to this path will be forwarded to your service.")
|
|
60
|
+
target_container: str = Field(description="Target container for the requests.")
|
|
61
|
+
healthcheck: HealthCheck = Field(default_factory=HealthCheck)
|
|
62
|
+
additional_rules: Optional[list[AdditionalRules]] = Field(default=None)
|
|
63
|
+
deregistration_delay: Optional[int] = Field(
|
|
64
|
+
default=60,
|
|
65
|
+
description="The amount of time to wait for targets to drain connections during deregistration.",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class HttpOverride(BaseModel):
|
|
70
|
+
alias: Optional[list[str]] = Field(
|
|
71
|
+
description="List of HTTPS domain alias(es) of your service.", default=None
|
|
72
|
+
)
|
|
73
|
+
stickiness: Optional[bool] = Field(description="Enable sticky sessions.", default=None)
|
|
74
|
+
path: Optional[str] = Field(
|
|
75
|
+
description="Requests to this path will be forwarded to your service.", default=None
|
|
76
|
+
)
|
|
77
|
+
target_container: Optional[str] = Field(
|
|
78
|
+
description="Target container for the requests", default=None
|
|
79
|
+
)
|
|
80
|
+
healthcheck: Optional[HealthCheck] = Field(default=None)
|
|
81
|
+
additional_rules: Optional[list[AdditionalRules]] = Field(default=None)
|
|
82
|
+
deregistration_delay: Optional[int] = Field(
|
|
83
|
+
default=None,
|
|
84
|
+
description="The amount of time to wait for targets to drain connections during deregistration.",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ContainerHealthCheck(BaseModel):
|
|
89
|
+
command: list[str] = Field(
|
|
90
|
+
description="The command to run to determine if the container is healthy."
|
|
91
|
+
)
|
|
92
|
+
interval: Optional[int] = Field(
|
|
93
|
+
default=10, description="Time period between health checks, in seconds."
|
|
94
|
+
)
|
|
95
|
+
retries: Optional[int] = Field(
|
|
96
|
+
default=2, description="Number of times to retry before container is deemed unhealthy."
|
|
97
|
+
)
|
|
98
|
+
timeout: Optional[int] = Field(
|
|
99
|
+
default=5,
|
|
100
|
+
description="How long to wait before considering the health check failed, in seconds.",
|
|
101
|
+
)
|
|
102
|
+
start_period: Optional[int] = Field(
|
|
103
|
+
default=0,
|
|
104
|
+
description="Length of grace period for containers to bootstrap before failed health checks count towards the maximum number of retries.",
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class Sidecar(BaseModel):
|
|
109
|
+
port: int = Field(description="Container port exposed by the sidecar to receive traffic.")
|
|
110
|
+
image: str = Field(description="Container image URI for the sidecar (e.g. 'repo/image:tag').")
|
|
111
|
+
essential: Optional[bool] = Field(
|
|
112
|
+
description="Whether the ECS task should stop if this sidecar container exits.",
|
|
113
|
+
default=True,
|
|
114
|
+
)
|
|
115
|
+
variables: Optional[Dict[str, Union[str, int, bool]]] = Field(
|
|
116
|
+
description="Environment variables to inject into the sidecar container.", default=None
|
|
117
|
+
)
|
|
118
|
+
secrets: Optional[Dict[str, str]] = Field(
|
|
119
|
+
description="Parameter Store secrets to inject into the sidecar.", default=None
|
|
120
|
+
)
|
|
121
|
+
healthcheck: Optional[ContainerHealthCheck] = Field(default=None)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class SidecarOverride(BaseModel):
|
|
125
|
+
port: Optional[int] = Field(default=None)
|
|
126
|
+
image: Optional[str] = Field(default=None)
|
|
127
|
+
essential: Optional[bool] = Field(default=None)
|
|
128
|
+
variables: Optional[Dict[str, Union[str, int, bool]]] = Field(default=None)
|
|
129
|
+
secrets: Optional[Dict[str, str]] = Field(default=None)
|
|
130
|
+
healthcheck: Optional[ContainerHealthCheck] = Field(default=None)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Image(BaseModel):
|
|
134
|
+
location: str = Field(description="Main container image location.")
|
|
135
|
+
port: Optional[int] = Field(
|
|
136
|
+
description="Port exposed by the main ECS task container (used by the load balancer/Service Connect).",
|
|
137
|
+
default=None,
|
|
138
|
+
)
|
|
139
|
+
depends_on: Optional[dict[str, str]] = Field(
|
|
140
|
+
description="Container dependency conditions.", default=None
|
|
141
|
+
)
|
|
142
|
+
healthcheck: Optional[ContainerHealthCheck] = Field(default=None)
|
|
143
|
+
|
|
144
|
+
@field_validator("location", mode="after")
|
|
145
|
+
@classmethod
|
|
146
|
+
def is_image_untagged(cls, value: str) -> str:
|
|
147
|
+
image_name = value.split("/")[-1]
|
|
148
|
+
if ":" in image_name:
|
|
149
|
+
raise PlatformException(
|
|
150
|
+
f"Image location cannot contain a tag '{value}'\nPlease remove the tag from your image location. The image tag is automatically added during deployment."
|
|
151
|
+
)
|
|
152
|
+
return value
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class Storage(BaseModel):
|
|
156
|
+
readonly_fs: Optional[bool] = Field(
|
|
157
|
+
description="Specify true to give your container read-only access to its root file system.",
|
|
158
|
+
default=False,
|
|
159
|
+
)
|
|
160
|
+
writable_directories: Optional[list[str]] = Field(
|
|
161
|
+
description="List of directories with read/write access.", default=None
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
@field_validator("writable_directories", mode="after")
|
|
165
|
+
@classmethod
|
|
166
|
+
def has_leading_forward_slash(cls, value: Union[list, None]) -> Union[list, None]:
|
|
167
|
+
if value is not None:
|
|
168
|
+
for path in value:
|
|
169
|
+
if not path.startswith("/"):
|
|
170
|
+
raise PlatformException(
|
|
171
|
+
"All writable directory paths must be absolute (starts with a /)"
|
|
172
|
+
)
|
|
173
|
+
return value
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class Cooldown(BaseModel):
|
|
177
|
+
in_: Optional[int] = Field(
|
|
178
|
+
alias="in",
|
|
179
|
+
description="Number of seconds to wait before scaling in (down) after a drop in load.",
|
|
180
|
+
default=60,
|
|
181
|
+
) # Can't use 'in' because it's a reserved keyword
|
|
182
|
+
out: Optional[int] = Field(
|
|
183
|
+
description="Number of seconds to wait before scaling out (up) after a spike in load.",
|
|
184
|
+
default=60,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
@field_validator("in_", "out", mode="before")
|
|
188
|
+
@classmethod
|
|
189
|
+
def parse_seconds(cls, value):
|
|
190
|
+
if isinstance(value, str) and value.endswith("s"):
|
|
191
|
+
value = value.removesuffix("s") # remove the trailing 's'
|
|
192
|
+
try:
|
|
193
|
+
return int(value)
|
|
194
|
+
except (ValueError, TypeError):
|
|
195
|
+
raise PlatformException("Cooldown values must be integers or strings like '30s'")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class CpuPercentage(BaseModel):
|
|
199
|
+
value: int = Field(description="Target CPU utilisation percentage that triggers autoscaling.")
|
|
200
|
+
cooldown: Optional[Cooldown] = Field(
|
|
201
|
+
default=None, description="Optional CPU cooldown that overrides the global cooldown policy."
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class MemoryPercentage(BaseModel):
|
|
206
|
+
value: int = Field(description="Target CPU utilisation percentage that triggers autoscaling.")
|
|
207
|
+
cooldown: Optional[Cooldown] = Field(
|
|
208
|
+
default=None,
|
|
209
|
+
description="Optional memory cooldown that overrides the global cooldown policy.",
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class RequestsPerMinute(BaseModel):
|
|
214
|
+
value: int = Field(
|
|
215
|
+
description="Number of incoming requests per minute that triggers autoscaling."
|
|
216
|
+
)
|
|
217
|
+
cooldown: Optional[Cooldown] = Field(
|
|
218
|
+
default=None,
|
|
219
|
+
description="Optional requests cooldown that overrides the global cooldown policy.",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class Count(BaseModel):
|
|
224
|
+
range: str = Field(
|
|
225
|
+
description="Minimum and maximum number of ECS tasks to maintain e.g. '1-2'."
|
|
226
|
+
)
|
|
227
|
+
cooldown: Optional[Cooldown] = Field(
|
|
228
|
+
default=None,
|
|
229
|
+
description="Global cooldown applied to all autoscaling metrics unless overridden per metric.",
|
|
230
|
+
)
|
|
231
|
+
cpu_percentage: Optional[Union[int, CpuPercentage]] = Field(
|
|
232
|
+
default=None,
|
|
233
|
+
description="CPU utilisation threshold (0–100). Either a plain integer or a map with 'value' and 'cooldown'.",
|
|
234
|
+
)
|
|
235
|
+
memory_percentage: Optional[Union[int, MemoryPercentage]] = Field(
|
|
236
|
+
default=None,
|
|
237
|
+
description="Memory utilisation threshold (0–100). Either a plain integer or a map with 'value' and 'cooldown'.",
|
|
238
|
+
)
|
|
239
|
+
requests_per_minute: Optional[Union[int, RequestsPerMinute]] = Field(
|
|
240
|
+
default=None,
|
|
241
|
+
description="Request-rate threshold. Either a plain integer or a map with 'value' and 'cooldown'.",
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
@model_validator(mode="after")
|
|
245
|
+
def at_least_one_autoscaling_metric(self):
|
|
246
|
+
|
|
247
|
+
if not any([self.cpu_percentage, self.memory_percentage, self.requests_per_minute]):
|
|
248
|
+
raise PlatformException(
|
|
249
|
+
"If autoscaling is enabled, you must define at least one metric: "
|
|
250
|
+
"cpu_percentage, memory_percentage, or requests_per_minute"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
if not re.match(r"^(\d+)-(\d+)$", self.range):
|
|
254
|
+
raise PlatformException("Range must be in the format 'int-int' e.g. '1-2'")
|
|
255
|
+
|
|
256
|
+
range_split = self.range.split("-")
|
|
257
|
+
if range_split[0] >= range_split[1]:
|
|
258
|
+
raise PlatformException("Range minimum value must be less than the maximum value.")
|
|
259
|
+
|
|
260
|
+
return self
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class ServiceConfigEnvironmentOverride(BaseModel):
|
|
264
|
+
http: Optional[HttpOverride] = Field(default=None)
|
|
265
|
+
sidecars: Optional[Dict[str, SidecarOverride]] = Field(default=None)
|
|
266
|
+
image: Optional[Image] = Field(default=None)
|
|
267
|
+
|
|
268
|
+
cpu: Optional[int] = Field(default=None)
|
|
269
|
+
memory: Optional[int] = Field(default=None)
|
|
270
|
+
count: Optional[Union[int, Count]] = Field(default=None)
|
|
271
|
+
exec: Optional[bool] = Field(default=None)
|
|
272
|
+
entrypoint: Optional[list[str]] = Field(default=None)
|
|
273
|
+
essential: Optional[bool] = Field(default=None)
|
|
274
|
+
|
|
275
|
+
storage: Optional[Storage] = Field(default=None)
|
|
276
|
+
|
|
277
|
+
variables: Optional[Dict[str, Union[str, int, bool]]] = Field(default=None)
|
|
278
|
+
secrets: Optional[Dict[str, str]] = Field(default=None)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class ServiceType(str, Enum):
|
|
282
|
+
BACKEND_SERVICE = "Backend Service"
|
|
283
|
+
LOAD_BALANCED_WEB_SERVICE = "Load Balanced Web Service"
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class ServiceConfig(BaseModel):
|
|
287
|
+
name: str = Field(description="Service name.")
|
|
288
|
+
type: ServiceType = Field(
|
|
289
|
+
description=f"Type of service. Must one one of: '{ServiceType.LOAD_BALANCED_WEB_SERVICE.value}', '{ServiceType.BACKEND_SERVICE.value}'"
|
|
290
|
+
)
|
|
291
|
+
http: Optional[Http] = Field(default=None)
|
|
292
|
+
|
|
293
|
+
@model_validator(mode="after")
|
|
294
|
+
def check_http_for_web_service(self):
|
|
295
|
+
if self.type == ServiceType.LOAD_BALANCED_WEB_SERVICE and self.http is None:
|
|
296
|
+
raise PlatformException(
|
|
297
|
+
f"A 'http' block must be provided when service type == {self.type.value}"
|
|
298
|
+
)
|
|
299
|
+
return self
|
|
300
|
+
|
|
301
|
+
sidecars: Optional[Dict[str, Sidecar]] = Field(default=None)
|
|
302
|
+
image: Image = Field()
|
|
303
|
+
cpu: int = Field(
|
|
304
|
+
description="vCPU units reserved for the ECS task (e.g. 256=0.25 vCPU, 512=0.5 vCPU, 1024=1 vCPU)."
|
|
305
|
+
)
|
|
306
|
+
memory: int = Field(
|
|
307
|
+
description="Memory in MiB reserved for the ECS task (e.g. 256, 512, 1024)."
|
|
308
|
+
)
|
|
309
|
+
count: Union[int, Count] = Field(
|
|
310
|
+
description="Desired task count — either a fixed integer or an autoscaling policy map with 'range', 'cooldown', and at least one of 'cpu_percentage', 'memory_percentage', or 'requests_per_minute' metrics."
|
|
311
|
+
)
|
|
312
|
+
exec: Optional[bool] = Field(
|
|
313
|
+
description="Enable ECS Exec (remote command execution) for running ECS tasks.",
|
|
314
|
+
default=False,
|
|
315
|
+
)
|
|
316
|
+
entrypoint: Optional[list[str]] = Field(
|
|
317
|
+
description="Overrides the default entrypoint in the image.", default=None
|
|
318
|
+
)
|
|
319
|
+
essential: Optional[bool] = Field(
|
|
320
|
+
description="Whether the main container is marked essential; The entire ECS task stops if it exits.",
|
|
321
|
+
default=True,
|
|
322
|
+
)
|
|
323
|
+
storage: Storage = Field(default_factory=Storage)
|
|
324
|
+
variables: Optional[Dict[str, Union[str, int, bool]]] = Field(
|
|
325
|
+
description="Environment variables to inject into the main application container.",
|
|
326
|
+
default=None,
|
|
327
|
+
)
|
|
328
|
+
secrets: Optional[Dict[str, str]] = Field(
|
|
329
|
+
description="Parameter Store secrets to inject into the main application container.",
|
|
330
|
+
default=None,
|
|
331
|
+
)
|
|
332
|
+
# Environment overrides can override almost the full config
|
|
333
|
+
environments: Optional[Dict[str, ServiceConfigEnvironmentOverride]] = Field(
|
|
334
|
+
description="Allows you to override most service config properties for specific environments.",
|
|
335
|
+
default=None,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# Class based variable used when handling the object
|
|
339
|
+
local_terraform_source: ClassVar[str] = "../../../../../platform-tools/terraform/ecs-service"
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import boto3
|
|
4
|
+
from botocore.exceptions import ClientError
|
|
5
|
+
|
|
6
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AutoscalingProvider:
|
|
10
|
+
def __init__(self, client: boto3.client):
|
|
11
|
+
self.autoscaling_client = client
|
|
12
|
+
|
|
13
|
+
def describe_autoscaling_target(
|
|
14
|
+
self, cluster_name: str, ecs_service_name: str
|
|
15
|
+
) -> dict[str, Any]:
|
|
16
|
+
"""Return autoscaling target information for an ECS service."""
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
response = self.autoscaling_client.describe_scalable_targets(
|
|
20
|
+
ServiceNamespace="ecs", ResourceIds=[f"service/{cluster_name}/{ecs_service_name}"]
|
|
21
|
+
)
|
|
22
|
+
return response["ScalableTargets"][0]
|
|
23
|
+
except ClientError as err:
|
|
24
|
+
raise PlatformException(f"Error retrieving scalable targets: {err}")
|
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class AWSException(PlatformException):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CreateTaskTimeoutException(AWSException):
|
|
9
|
+
def __init__(self, addon_name: str, application_name: str, environment: str):
|
|
10
|
+
super().__init__(
|
|
11
|
+
f"""Client ({addon_name}) ECS task has failed to start for "{application_name}" in "{environment}" environment."""
|
|
12
|
+
)
|
|
13
|
+
|
|
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
|
+
|
|
18
|
+
class ImageNotFoundException(AWSException):
|
|
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]):
|
|
30
|
+
super().__init__(
|
|
31
|
+
MULTIPLE_IMAGES_FOUND_TEMPLATE.format(
|
|
32
|
+
image_ref=image_ref, matching_images=", ".join(sorted(matching_images))
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
REPOSITORY_NOT_FOUND_TEMPLATE = """The ECR repository "{repository}" could not be found."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class RepositoryNotFoundException(AWSException):
|
|
41
|
+
def __init__(self, repository: str):
|
|
42
|
+
super().__init__(REPOSITORY_NOT_FOUND_TEMPLATE.format(repository=repository))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class LogGroupNotFoundException(AWSException):
|
|
46
|
+
def __init__(self, log_group_name: str):
|
|
47
|
+
super().__init__(f"""No log group called "{log_group_name}".""")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# TODO: DBTP-1976: This should probably be in the AWS Copilot provider, but was causing circular import when we tried it pre refactoring the utils/aws.py
|
|
51
|
+
class CopilotCodebaseNotFoundException(PlatformException):
|
|
52
|
+
def __init__(self, codebase: str):
|
|
53
|
+
super().__init__(
|
|
54
|
+
f"""The codebase "{codebase}" either does not exist or has not been deployed."""
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class CreateAccessTokenException(AWSException):
|
|
59
|
+
def __init__(self, client_id: str):
|
|
60
|
+
super().__init__(f"""Failed to create access token for Client "{client_id}".""")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class UnableToRetrieveSSOAccountList(AWSException):
|
|
64
|
+
def __init__(self):
|
|
65
|
+
super().__init__("Unable to retrieve AWS SSO account list")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class UnableToRetrieveSSOAccountRolesList(AWSException):
|
|
69
|
+
def __init__(self, account_id: str):
|
|
70
|
+
super().__init__(f"Unable to retrieve AWS SSO roles list for AWS account {account_id}")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Protocol
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class GetVersionsProtocol(Protocol):
|
|
5
|
+
def get_supported_versions(self) -> list[str]: ...
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GetReferenceProtocol(Protocol):
|
|
9
|
+
def get_reference(self) -> str: ...
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AwsGetVersionProtocol(GetReferenceProtocol, GetVersionsProtocol):
|
|
13
|
+
pass
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Opensearch:
|
|
5
|
+
|
|
6
|
+
def __init__(self, client: boto3.client):
|
|
7
|
+
self.client = client
|
|
8
|
+
self.engine = "OpenSearch"
|
|
9
|
+
|
|
10
|
+
def get_reference(self) -> str:
|
|
11
|
+
return self.engine.lower()
|
|
12
|
+
|
|
13
|
+
def get_supported_versions(self) -> list[str]:
|
|
14
|
+
response = self.client.list_versions()
|
|
15
|
+
all_versions = response["Versions"]
|
|
16
|
+
|
|
17
|
+
supported_versions = [
|
|
18
|
+
version.removeprefix(f"{self.engine}_")
|
|
19
|
+
for version in all_versions
|
|
20
|
+
if version.startswith(f"{self.engine}_")
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
return supported_versions
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Redis:
|
|
5
|
+
|
|
6
|
+
def __init__(self, client: boto3.client):
|
|
7
|
+
self.client = client
|
|
8
|
+
self.engine = "redis"
|
|
9
|
+
|
|
10
|
+
def get_reference(self) -> str:
|
|
11
|
+
return self.engine.lower()
|
|
12
|
+
|
|
13
|
+
def get_supported_versions(self) -> list[str]:
|
|
14
|
+
supported_versions_response = self.client.describe_cache_engine_versions(Engine=self.engine)
|
|
15
|
+
|
|
16
|
+
supported_versions = [
|
|
17
|
+
version["EngineVersion"]
|
|
18
|
+
for version in supported_versions_response["CacheEngineVersions"]
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
return supported_versions
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import botocore
|
|
2
|
+
from boto3 import Session
|
|
3
|
+
|
|
4
|
+
from dbt_platform_helper.providers.aws.exceptions import CreateAccessTokenException
|
|
5
|
+
from dbt_platform_helper.providers.aws.exceptions import UnableToRetrieveSSOAccountList
|
|
6
|
+
from dbt_platform_helper.providers.aws.exceptions import (
|
|
7
|
+
UnableToRetrieveSSOAccountRolesList,
|
|
8
|
+
)
|
|
9
|
+
from dbt_platform_helper.utils.aws import get_aws_session_or_abort
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SSOAuthProvider:
|
|
13
|
+
def __init__(self, session: Session = None):
|
|
14
|
+
self.session = session
|
|
15
|
+
self.sso_oidc = self._get_client("sso-oidc")
|
|
16
|
+
self.sso = self._get_client("sso")
|
|
17
|
+
|
|
18
|
+
def register(self, client_name, client_type):
|
|
19
|
+
client = self.sso_oidc.register_client(clientName=client_name, clientType=client_type)
|
|
20
|
+
client_id = client.get("clientId")
|
|
21
|
+
client_secret = client.get("clientSecret")
|
|
22
|
+
|
|
23
|
+
return client_id, client_secret
|
|
24
|
+
|
|
25
|
+
def start_device_authorization(self, client_id, client_secret, start_url):
|
|
26
|
+
authz = self.sso_oidc.start_device_authorization(
|
|
27
|
+
clientId=client_id,
|
|
28
|
+
clientSecret=client_secret,
|
|
29
|
+
startUrl=start_url,
|
|
30
|
+
)
|
|
31
|
+
url = authz.get("verificationUriComplete")
|
|
32
|
+
deviceCode = authz.get("deviceCode")
|
|
33
|
+
|
|
34
|
+
return url, deviceCode
|
|
35
|
+
|
|
36
|
+
def create_access_token(self, client_id, client_secret, device_code):
|
|
37
|
+
try:
|
|
38
|
+
response = self.sso_oidc.create_token(
|
|
39
|
+
clientId=client_id,
|
|
40
|
+
clientSecret=client_secret,
|
|
41
|
+
grantType="urn:ietf:params:oauth:grant-type:device_code",
|
|
42
|
+
deviceCode=device_code,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
return response.get("accessToken")
|
|
46
|
+
|
|
47
|
+
except botocore.exceptions.ClientError as e:
|
|
48
|
+
if e.response["Error"]["Code"] != "AuthorizationPendingException":
|
|
49
|
+
raise CreateAccessTokenException(client_id)
|
|
50
|
+
|
|
51
|
+
def list_accounts(self, access_token, max_results=100):
|
|
52
|
+
aws_accounts_response = self.sso.list_accounts(
|
|
53
|
+
accessToken=access_token,
|
|
54
|
+
maxResults=max_results,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if len(aws_accounts_response.get("accountList", [])) == 0:
|
|
58
|
+
raise UnableToRetrieveSSOAccountList()
|
|
59
|
+
return aws_accounts_response.get("accountList")
|
|
60
|
+
|
|
61
|
+
def list_account_roles(self, access_token, account_id, max_results=100):
|
|
62
|
+
aws_account_roles_response = self.sso.list_account_roles(
|
|
63
|
+
accessToken=access_token,
|
|
64
|
+
accountId=account_id,
|
|
65
|
+
maxResults=max_results,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if len(aws_account_roles_response.get("roleList", [])) == 0:
|
|
69
|
+
raise UnableToRetrieveSSOAccountRolesList(account_id=account_id)
|
|
70
|
+
return aws_account_roles_response.get("roleList")
|
|
71
|
+
|
|
72
|
+
def _get_client(self, client: str):
|
|
73
|
+
if not self.session:
|
|
74
|
+
self.session = get_aws_session_or_abort()
|
|
75
|
+
return self.session.client(client)
|
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from abc import abstractmethod
|
|
2
4
|
from datetime import datetime
|
|
3
5
|
|
|
6
|
+
from dbt_platform_helper.providers.aws.interfaces import AwsGetVersionProtocol
|
|
4
7
|
from dbt_platform_helper.providers.yaml_file import YamlFileProvider
|
|
5
8
|
|
|
6
9
|
|
|
7
|
-
class
|
|
10
|
+
class GetDataStrategy(ABC):
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def retrieve_fresh_data(self):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def get_data_identifier(self):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class GetAWSVersionStrategy(GetDataStrategy):
|
|
21
|
+
def __init__(self, client_provider: AwsGetVersionProtocol):
|
|
22
|
+
self.client_provider = client_provider
|
|
23
|
+
|
|
24
|
+
def retrieve_fresh_data(self):
|
|
25
|
+
return self.client_provider.get_supported_versions()
|
|
26
|
+
|
|
27
|
+
def get_data_identifier(self):
|
|
28
|
+
return self.client_provider.get_reference()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Cache:
|
|
8
32
|
def __init__(
|
|
9
33
|
self,
|
|
10
34
|
file_provider: YamlFileProvider = None,
|
|
@@ -12,13 +36,25 @@ class CacheProvider:
|
|
|
12
36
|
self._cache_file = ".platform-helper-config-cache.yml"
|
|
13
37
|
self.file_provider = file_provider or YamlFileProvider
|
|
14
38
|
|
|
15
|
-
def
|
|
39
|
+
def get_data(self, strategy: GetDataStrategy):
|
|
40
|
+
"""Main method to retrieve caching data using the client-specific
|
|
41
|
+
strategy."""
|
|
42
|
+
cache_key = strategy.get_data_identifier()
|
|
43
|
+
if self._cache_refresh_required(cache_key):
|
|
44
|
+
data = strategy.retrieve_fresh_data()
|
|
45
|
+
self._update_cache(cache_key, data)
|
|
46
|
+
else:
|
|
47
|
+
data = self._read_from_cache(cache_key)
|
|
48
|
+
|
|
49
|
+
return data
|
|
50
|
+
|
|
51
|
+
def _read_from_cache(self, resource_name):
|
|
16
52
|
|
|
17
53
|
platform_helper_config = self.file_provider.load(self._cache_file)
|
|
18
54
|
|
|
19
55
|
return platform_helper_config.get(resource_name).get("versions")
|
|
20
56
|
|
|
21
|
-
def
|
|
57
|
+
def _update_cache(self, resource_name, supported_versions):
|
|
22
58
|
|
|
23
59
|
platform_helper_config = {}
|
|
24
60
|
|
|
@@ -40,7 +76,7 @@ class CacheProvider:
|
|
|
40
76
|
"# [!] This file is autogenerated via the platform-helper. Do not edit.\n",
|
|
41
77
|
)
|
|
42
78
|
|
|
43
|
-
def
|
|
79
|
+
def _cache_refresh_required(self, resource_name) -> bool:
|
|
44
80
|
"""
|
|
45
81
|
Checks if the platform-helper should reach out to AWS to 'refresh' its
|
|
46
82
|
cached values.
|
|
@@ -8,7 +8,7 @@ from dbt_platform_helper.platform_exception import PlatformException
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class CloudFormation:
|
|
11
|
-
# TODO add handling for optional client parameters to handle case of calling boto API with None
|
|
11
|
+
# TODO: DBTP-1966: add handling for optional client parameters to handle case of calling boto API with None
|
|
12
12
|
def __init__(self, cloudformation_client, iam_client=None, ssm_client=None):
|
|
13
13
|
self.cloudformation_client = cloudformation_client
|
|
14
14
|
self.iam_client = iam_client
|