dbt-platform-helper 15.9.0__py3-none-any.whl → 15.11.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/COMMANDS.md +1 -51
- dbt_platform_helper/commands/internal.py +134 -0
- dbt_platform_helper/constants.py +14 -0
- dbt_platform_helper/domain/conduit.py +1 -1
- dbt_platform_helper/domain/config.py +30 -1
- dbt_platform_helper/domain/maintenance_page.py +10 -8
- dbt_platform_helper/domain/service.py +317 -53
- dbt_platform_helper/domain/update_alb_rules.py +346 -0
- dbt_platform_helper/entities/platform_config_schema.py +4 -5
- dbt_platform_helper/entities/service.py +139 -13
- dbt_platform_helper/providers/aws/exceptions.py +5 -0
- dbt_platform_helper/providers/aws/sso_auth.py +14 -0
- dbt_platform_helper/providers/config.py +0 -11
- dbt_platform_helper/providers/ecs.py +104 -11
- dbt_platform_helper/providers/load_balancers.py +86 -8
- dbt_platform_helper/providers/logs.py +57 -0
- dbt_platform_helper/providers/s3.py +21 -0
- dbt_platform_helper/providers/terraform_manifest.py +3 -5
- dbt_platform_helper/providers/yaml_file.py +13 -5
- {dbt_platform_helper-15.9.0.dist-info → dbt_platform_helper-15.11.0.dist-info}/METADATA +5 -3
- {dbt_platform_helper-15.9.0.dist-info → dbt_platform_helper-15.11.0.dist-info}/RECORD +25 -22
- {dbt_platform_helper-15.9.0.dist-info → dbt_platform_helper-15.11.0.dist-info}/WHEEL +1 -1
- platform_helper.py +2 -2
- dbt_platform_helper/commands/service.py +0 -53
- {dbt_platform_helper-15.9.0.dist-info → dbt_platform_helper-15.11.0.dist-info}/entry_points.txt +0 -0
- {dbt_platform_helper-15.9.0.dist-info → dbt_platform_helper-15.11.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from copy import deepcopy
|
|
1
6
|
from datetime import datetime
|
|
2
7
|
from importlib.metadata import version
|
|
3
8
|
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
4
10
|
|
|
5
11
|
from dbt_platform_helper.constants import IMAGE_TAG_ENV_VAR
|
|
12
|
+
from dbt_platform_helper.constants import PLATFORM_HELPER_PACKAGE_NAME
|
|
6
13
|
from dbt_platform_helper.constants import PLATFORM_HELPER_VERSION_OVERRIDE_KEY
|
|
7
14
|
from dbt_platform_helper.constants import SERVICE_CONFIG_FILE
|
|
8
15
|
from dbt_platform_helper.constants import SERVICE_DIRECTORY
|
|
@@ -15,63 +22,68 @@ from dbt_platform_helper.domain.terraform_environment import (
|
|
|
15
22
|
)
|
|
16
23
|
from dbt_platform_helper.entities.service import ServiceConfig
|
|
17
24
|
from dbt_platform_helper.platform_exception import PlatformException
|
|
18
|
-
from dbt_platform_helper.providers.config import ConfigLoader
|
|
19
25
|
from dbt_platform_helper.providers.config import ConfigProvider
|
|
20
26
|
from dbt_platform_helper.providers.config_validator import ConfigValidator
|
|
27
|
+
from dbt_platform_helper.providers.ecs import ECS
|
|
21
28
|
from dbt_platform_helper.providers.environment_variable import (
|
|
22
29
|
EnvironmentVariableProvider,
|
|
23
30
|
)
|
|
31
|
+
from dbt_platform_helper.providers.files import FileProvider
|
|
24
32
|
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
33
|
+
from dbt_platform_helper.providers.logs import LogsProvider
|
|
34
|
+
from dbt_platform_helper.providers.s3 import S3Provider
|
|
25
35
|
from dbt_platform_helper.providers.terraform_manifest import TerraformManifestProvider
|
|
36
|
+
from dbt_platform_helper.providers.version import InstalledVersionProvider
|
|
26
37
|
from dbt_platform_helper.providers.yaml_file import YamlFileProvider
|
|
27
38
|
from dbt_platform_helper.utils.application import load_application
|
|
28
39
|
from dbt_platform_helper.utils.deep_merge import deep_merge
|
|
29
40
|
|
|
30
|
-
|
|
41
|
+
SERVICE_TYPES = ["Load Balanced Web Service", "Backend Service"]
|
|
42
|
+
DEPLOYMENT_TIMEOUT_SECONDS = 600
|
|
43
|
+
POLL_INTERVAL_SECONDS = 5
|
|
44
|
+
|
|
45
|
+
# TODO add schema version to service config
|
|
31
46
|
|
|
32
47
|
|
|
33
48
|
class ServiceManager:
|
|
34
49
|
def __init__(
|
|
35
50
|
self,
|
|
36
51
|
config_provider=ConfigProvider(ConfigValidator()),
|
|
37
|
-
loader: ConfigLoader = ConfigLoader(),
|
|
38
52
|
io: ClickIOProvider = ClickIOProvider(),
|
|
39
53
|
file_provider=YamlFileProvider,
|
|
40
|
-
environment_variable_provider: EnvironmentVariableProvider = None,
|
|
41
54
|
manifest_provider: TerraformManifestProvider = None,
|
|
42
55
|
platform_helper_version_override: str = None,
|
|
43
56
|
load_application=load_application,
|
|
57
|
+
installed_version_provider: InstalledVersionProvider = InstalledVersionProvider(),
|
|
58
|
+
ecs_provider: ECS = None,
|
|
59
|
+
s3_provider: S3Provider = None,
|
|
60
|
+
logs_provider: LogsProvider = None,
|
|
44
61
|
):
|
|
45
62
|
|
|
46
63
|
self.file_provider = file_provider
|
|
47
64
|
self.config_provider = config_provider
|
|
48
|
-
self.loader = loader
|
|
49
65
|
self.io = io
|
|
50
|
-
self.environment_variable_provider = (
|
|
51
|
-
environment_variable_provider or EnvironmentVariableProvider()
|
|
52
|
-
)
|
|
53
66
|
self.manifest_provider = manifest_provider or TerraformManifestProvider()
|
|
54
67
|
self.platform_helper_version_override = (
|
|
55
68
|
platform_helper_version_override
|
|
56
|
-
or
|
|
69
|
+
or EnvironmentVariableProvider.get(PLATFORM_HELPER_VERSION_OVERRIDE_KEY)
|
|
57
70
|
)
|
|
58
71
|
self.load_application = load_application
|
|
72
|
+
self.installed_version_provider = installed_version_provider
|
|
73
|
+
self.ecs_provider = ecs_provider
|
|
74
|
+
self.s3_provider = s3_provider
|
|
75
|
+
self.logs_provider = logs_provider
|
|
59
76
|
|
|
60
|
-
def generate(self,
|
|
77
|
+
def generate(self, environment: str, services: list[str]):
|
|
61
78
|
|
|
62
79
|
config = self.config_provider.get_enriched_config()
|
|
63
80
|
application_name = config.get("application", "")
|
|
64
81
|
application = self.load_application(app=application_name)
|
|
65
82
|
|
|
66
|
-
if not environments:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
for environment in environments:
|
|
71
|
-
if environment not in application.environments:
|
|
72
|
-
raise EnvironmentNotFoundException(
|
|
73
|
-
f"cannot generate terraform for environment {environment}. It does not exist in your configuration"
|
|
74
|
-
)
|
|
83
|
+
if environment not in application.environments:
|
|
84
|
+
raise EnvironmentNotFoundException(
|
|
85
|
+
f"Cannot generate Terraform for environment '{environment}'. It does not exist in your configuration."
|
|
86
|
+
)
|
|
75
87
|
|
|
76
88
|
if not services:
|
|
77
89
|
try:
|
|
@@ -87,67 +99,319 @@ class ServiceManager:
|
|
|
87
99
|
)
|
|
88
100
|
except Exception as e:
|
|
89
101
|
self.io.abort_with_error(f"Failed extracting services with exception, {e}")
|
|
102
|
+
|
|
90
103
|
service_models = []
|
|
91
104
|
for service in services:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
105
|
+
file_content = self.file_provider.load(
|
|
106
|
+
f"{SERVICE_DIRECTORY}/{service}/{SERVICE_CONFIG_FILE}"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
file_content = YamlFileProvider.find_and_replace(
|
|
110
|
+
config=file_content,
|
|
111
|
+
strings=[
|
|
112
|
+
"${PLATFORM_APPLICATION_NAME}",
|
|
113
|
+
"${PLATFORM_ENVIRONMENT_NAME}",
|
|
114
|
+
],
|
|
115
|
+
replacements=[application.name, environment],
|
|
97
116
|
)
|
|
117
|
+
service_models.append(ServiceConfig(**file_content))
|
|
98
118
|
|
|
99
119
|
platform_helper_version_for_template: str = (
|
|
100
120
|
self.platform_helper_version_override
|
|
101
121
|
or config.get("default_versions", {}).get("platform-helper")
|
|
102
122
|
)
|
|
103
123
|
|
|
104
|
-
source_type =
|
|
124
|
+
source_type = EnvironmentVariableProvider.get(TERRAFORM_MODULE_SOURCE_TYPE_ENV_VAR)
|
|
105
125
|
|
|
106
126
|
if source_type == "LOCAL":
|
|
107
127
|
module_source_override = ServiceConfig.local_terraform_source
|
|
108
128
|
elif source_type == "OVERRIDE":
|
|
109
|
-
module_source_override =
|
|
129
|
+
module_source_override = EnvironmentVariableProvider.get(
|
|
110
130
|
TERRAFORM_ECS_SERVICE_MODULE_SOURCE_OVERRIDE_ENV_VAR
|
|
111
131
|
)
|
|
112
132
|
else:
|
|
113
133
|
module_source_override = None
|
|
114
134
|
|
|
115
|
-
image_tag = image_tag_flag or self.environment_variable_provider.get(IMAGE_TAG_ENV_VAR)
|
|
116
|
-
if not image_tag:
|
|
117
|
-
raise PlatformException(
|
|
118
|
-
f"An image tag must be provided to deploy a service. This can be set by the $IMAGE_TAG environment variable, or the --image-tag flag."
|
|
119
|
-
)
|
|
120
|
-
|
|
121
135
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
122
136
|
|
|
123
137
|
for service in service_models:
|
|
124
138
|
|
|
125
|
-
|
|
139
|
+
model_dump = service.model_dump(
|
|
140
|
+
exclude_none=True, by_alias=True
|
|
141
|
+
) # Use by_alias=True so that the Cooldown field 'in_' is written as 'in' in the output
|
|
142
|
+
env_overrides = model_dump.get("environments", {}).get(environment)
|
|
143
|
+
if env_overrides:
|
|
144
|
+
merged_config = deep_merge(model_dump, env_overrides)
|
|
145
|
+
else:
|
|
146
|
+
merged_config = model_dump.copy()
|
|
147
|
+
merged_config.pop("environments", None)
|
|
148
|
+
|
|
149
|
+
output_path = Path(
|
|
150
|
+
f"terraform/{SERVICE_DIRECTORY}/{environment}/{service.name}/{SERVICE_CONFIG_FILE}"
|
|
151
|
+
)
|
|
152
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
|
|
154
|
+
self.file_provider.write(
|
|
155
|
+
str(output_path),
|
|
156
|
+
merged_config,
|
|
157
|
+
f"# WARNING: This is an autogenerated file, not for manual editing.\n# Generated by platform-helper {version('dbt-platform-helper')} / {timestamp}.\n",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
self.manifest_provider.generate_service_config(
|
|
161
|
+
service,
|
|
162
|
+
environment,
|
|
163
|
+
platform_helper_version_for_template,
|
|
164
|
+
config,
|
|
165
|
+
module_source_override,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def migrate_copilot_manifests(self) -> None:
|
|
169
|
+
service_directory = Path("services/")
|
|
170
|
+
service_directory.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
|
|
172
|
+
for dirname, _, filenames in os.walk("copilot"):
|
|
173
|
+
if "manifest.yml" in filenames and "environments" not in dirname:
|
|
174
|
+
copilot_manifest = self.file_provider.load(f"{dirname}/manifest.yml")
|
|
175
|
+
service_manifest = OrderedDict(deepcopy(copilot_manifest))
|
|
176
|
+
|
|
177
|
+
if service_manifest["type"] not in SERVICE_TYPES:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
if "environments" in service_manifest:
|
|
181
|
+
for env in service_manifest["environments"]:
|
|
182
|
+
env_config = service_manifest["environments"][env]
|
|
183
|
+
if "http" in env_config:
|
|
184
|
+
if "alb" in env_config["http"]:
|
|
185
|
+
del env_config["http"]["alb"]
|
|
126
186
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
merged_config = deep_merge(model_dump, env_overrides)
|
|
131
|
-
else:
|
|
132
|
-
merged_config = model_dump.copy()
|
|
133
|
-
merged_config.pop("environments", None)
|
|
187
|
+
if "entrypoint" in service_manifest:
|
|
188
|
+
if isinstance(service_manifest["entrypoint"], str):
|
|
189
|
+
service_manifest["entrypoint"] = [service_manifest["entrypoint"]]
|
|
134
190
|
|
|
135
|
-
|
|
136
|
-
|
|
191
|
+
service_manifest = self.file_provider.find_and_replace(
|
|
192
|
+
config=service_manifest,
|
|
193
|
+
strings=["${COPILOT_APPLICATION_NAME}", "${COPILOT_ENVIRONMENT_NAME}"],
|
|
194
|
+
replacements=["${PLATFORM_APPLICATION_NAME}", "${PLATFORM_ENVIRONMENT_NAME}"],
|
|
137
195
|
)
|
|
138
|
-
|
|
196
|
+
|
|
197
|
+
service_manifest = self.file_provider.remove_empty_keys(service_manifest)
|
|
198
|
+
|
|
199
|
+
if "sidecars" in service_manifest:
|
|
200
|
+
new_sidecars = {}
|
|
201
|
+
writable_directories = []
|
|
202
|
+
|
|
203
|
+
for sidecar_name, sidecar in service_manifest["sidecars"].items():
|
|
204
|
+
if "chown" not in sidecar.get("command", "") and "chmod" not in sidecar.get(
|
|
205
|
+
"command", ""
|
|
206
|
+
):
|
|
207
|
+
new_sidecars[sidecar_name] = sidecar
|
|
208
|
+
if "chown" in sidecar.get("command", "") and "mount_points" in sidecar:
|
|
209
|
+
for mountpoint in sidecar["mount_points"]:
|
|
210
|
+
writable_directories.append(mountpoint["path"])
|
|
211
|
+
|
|
212
|
+
service_manifest["sidecars"] = new_sidecars
|
|
213
|
+
if "storage" in service_manifest:
|
|
214
|
+
service_manifest["storage"]["writable_directories"] = writable_directories
|
|
215
|
+
|
|
216
|
+
service_path = service_directory / service_manifest["name"]
|
|
217
|
+
|
|
218
|
+
self.io.info(
|
|
219
|
+
FileProvider.mkfile(
|
|
220
|
+
service_path,
|
|
221
|
+
"service-config.yml",
|
|
222
|
+
"",
|
|
223
|
+
overwrite=True,
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
current_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
228
|
+
message = f"# Generated by platform-helper {self.installed_version_provider.get_semantic_version(PLATFORM_HELPER_PACKAGE_NAME)} / {current_date}.\n\n"
|
|
139
229
|
|
|
140
230
|
self.file_provider.write(
|
|
141
|
-
|
|
142
|
-
merged_config,
|
|
143
|
-
f"# WARNING: This is an autogenerated file, not for manual editing.\n# Generated by platform-helper {version('dbt-platform-helper')} / {timestamp}.\n",
|
|
231
|
+
f"{service_path}/service-config.yml", dict(service_manifest), message
|
|
144
232
|
)
|
|
145
233
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
234
|
+
def deploy(self, service: str, environment: str, application: str, image_tag: str = None):
|
|
235
|
+
"""Register a new ECS task definition revision, update the ECS service
|
|
236
|
+
with it, and output Cloudwatch logs until deployment is complete."""
|
|
237
|
+
|
|
238
|
+
application_obj = self.load_application(app=application)
|
|
239
|
+
application_envs = application_obj.environments
|
|
240
|
+
account_id = application_envs.get(environment).account_id
|
|
241
|
+
|
|
242
|
+
s3_response = self.s3_provider.get_object(
|
|
243
|
+
bucket_name=f"ecs-task-definitions-{application}-{environment}",
|
|
244
|
+
object_key=f"{application}/{environment}/{service}.json",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
task_definition = json.loads(s3_response)
|
|
248
|
+
|
|
249
|
+
image_tag = image_tag or EnvironmentVariableProvider.get(IMAGE_TAG_ENV_VAR)
|
|
250
|
+
|
|
251
|
+
task_def_arn = self.ecs_provider.register_task_definition(
|
|
252
|
+
service=service,
|
|
253
|
+
image_tag=image_tag,
|
|
254
|
+
task_definition=task_definition,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
self.io.info(f"Task definition successfully registered with ARN '{task_def_arn}'.\n")
|
|
258
|
+
|
|
259
|
+
service_response = self.ecs_provider.update_service(
|
|
260
|
+
service=service,
|
|
261
|
+
task_def_arn=task_def_arn,
|
|
262
|
+
environment=environment,
|
|
263
|
+
application=application,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
self.io.info(f"Successfully updated ECS service '{service_response['serviceName']}'.\n")
|
|
267
|
+
|
|
268
|
+
primary_deployment_id = self._get_primary_deployment_id(service_response=service_response)
|
|
269
|
+
self.io.info(f"New ECS Deployment with ID '{primary_deployment_id}' has been triggered.\n")
|
|
270
|
+
|
|
271
|
+
expected_count = service_response.get("desiredCount", 1)
|
|
272
|
+
task_ids = self._fetch_ecs_task_ids(
|
|
273
|
+
application=application,
|
|
274
|
+
environment=environment,
|
|
275
|
+
deployment_id=primary_deployment_id,
|
|
276
|
+
expected_count=expected_count,
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
self.io.info(
|
|
280
|
+
f"Detected {len(task_ids)} new ECS task(s) with the following ID(s) {task_ids}.\n"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
container_names = self.ecs_provider.get_container_names_from_ecs_tasks(
|
|
284
|
+
cluster_name=f"{application}-{environment}-cluster",
|
|
285
|
+
task_ids=task_ids,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
log_streams = self._build_log_stream_names(
|
|
289
|
+
task_ids=task_ids, container_names=container_names, stream_prefix="platform"
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
log_group = f"/platform/ecs/service/{application}/{environment}/{service}"
|
|
293
|
+
self.logs_provider.check_log_streams_present(
|
|
294
|
+
log_group=log_group, expected_log_streams=log_streams
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
cloudwatch_url = self._build_cloudwatch_live_tail_url(
|
|
298
|
+
account_id=account_id, log_group=log_group, log_streams=log_streams
|
|
299
|
+
)
|
|
300
|
+
self.io.info(f"View real-time deployment logs in the AWS Console: \n{cloudwatch_url}\n")
|
|
301
|
+
|
|
302
|
+
self._monitor_ecs_deployment(
|
|
303
|
+
application=application,
|
|
304
|
+
environment=environment,
|
|
305
|
+
service=service,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
@staticmethod
|
|
309
|
+
def _build_cloudwatch_live_tail_url(
|
|
310
|
+
account_id: str, log_group: str, log_streams: list[str]
|
|
311
|
+
) -> str:
|
|
312
|
+
"""Build CloudWatch live tail URL with log group and log streams pre-
|
|
313
|
+
populated in Rison format."""
|
|
314
|
+
|
|
315
|
+
log_group_rison = log_group.replace("/", "*2f")
|
|
316
|
+
|
|
317
|
+
delimiter = "~'"
|
|
318
|
+
log_streams_rison = ""
|
|
319
|
+
for stream in log_streams:
|
|
320
|
+
stream_rison = stream.replace("/", "*2f")
|
|
321
|
+
log_streams_rison = log_streams_rison + f"{delimiter}{stream_rison}"
|
|
322
|
+
|
|
323
|
+
base = "https://eu-west-2.console.aws.amazon.com/cloudwatch/home?region=eu-west-2#logsV2:live-tail"
|
|
324
|
+
log_group_fragment = f"$3FlogGroupArns$3D~(~'arn*3aaws*3alogs*3aeu-west-2*3a{account_id}*3alog-group*3a{log_group_rison}*3a*2a)"
|
|
325
|
+
log_streams_fragment = f"$26logStreamNames$3D~({log_streams_rison})"
|
|
326
|
+
|
|
327
|
+
return base + log_group_fragment + log_streams_fragment
|
|
328
|
+
|
|
329
|
+
@staticmethod
|
|
330
|
+
def _build_log_stream_names(
|
|
331
|
+
task_ids: list[str], container_names: list[str], stream_prefix: str
|
|
332
|
+
) -> list[str]:
|
|
333
|
+
"""Manually build names of the log stream that will get created."""
|
|
334
|
+
|
|
335
|
+
log_streams = []
|
|
336
|
+
for id in task_ids:
|
|
337
|
+
for name in container_names:
|
|
338
|
+
if not name.startswith(
|
|
339
|
+
"ecs-service-connect"
|
|
340
|
+
): # ECS Service Connect container logs are noisy and not relevant in most cases
|
|
341
|
+
log_streams.append(f"{stream_prefix}/{name}/{id}")
|
|
342
|
+
|
|
343
|
+
return log_streams
|
|
344
|
+
|
|
345
|
+
@staticmethod
|
|
346
|
+
def _get_primary_deployment_id(service_response: dict[str, Any]):
|
|
347
|
+
for dep in service_response["deployments"]:
|
|
348
|
+
if dep["status"] == "PRIMARY":
|
|
349
|
+
return dep["id"]
|
|
350
|
+
raise PlatformException(
|
|
351
|
+
f"\nUnable to find primary ECS deployment for service '{service_response['serviceName']}'\n"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
def _fetch_ecs_task_ids(
|
|
355
|
+
self, application: str, environment: str, deployment_id: str, expected_count: int
|
|
356
|
+
) -> list[str]:
|
|
357
|
+
"""Return ECS task ID(s) of tasks started by the PRIMARY ECS
|
|
358
|
+
deployment."""
|
|
359
|
+
|
|
360
|
+
timeout_seconds = DEPLOYMENT_TIMEOUT_SECONDS
|
|
361
|
+
deadline = time.monotonic() + timeout_seconds # 10 minute deadline before timing out
|
|
362
|
+
|
|
363
|
+
self.io.info(f"Waiting for the new ECS task(s) to spin up.\n")
|
|
364
|
+
|
|
365
|
+
while time.monotonic() < deadline:
|
|
366
|
+
task_arns = self.ecs_provider.get_ecs_task_arns(
|
|
367
|
+
cluster=f"{application}-{environment}-cluster",
|
|
368
|
+
started_by=deployment_id,
|
|
369
|
+
desired_status="RUNNING",
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
if len(task_arns) >= expected_count:
|
|
373
|
+
break
|
|
374
|
+
|
|
375
|
+
time.sleep(POLL_INTERVAL_SECONDS)
|
|
376
|
+
|
|
377
|
+
if len(task_arns) < expected_count:
|
|
378
|
+
raise PlatformException(
|
|
379
|
+
f"Timed out waiting for {expected_count} RUNNING ECS task(s) to spin up after {timeout_seconds}s. Got {len(task_arns)} instead."
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
task_ids = []
|
|
383
|
+
for arn in task_arns:
|
|
384
|
+
task_ids.append(arn.rsplit("/", 1)[-1])
|
|
385
|
+
return task_ids
|
|
386
|
+
|
|
387
|
+
def _monitor_ecs_deployment(self, application: str, environment: str, service: str) -> bool:
|
|
388
|
+
"""Loop until ECS rollout state is SUCCESSFUL or a fail status or else
|
|
389
|
+
times out."""
|
|
390
|
+
|
|
391
|
+
cluster_name = f"{application}-{environment}-cluster"
|
|
392
|
+
ecs_service_name = f"{application}-{environment}-{service}"
|
|
393
|
+
start_time = time.time()
|
|
394
|
+
timeout_seconds = DEPLOYMENT_TIMEOUT_SECONDS
|
|
395
|
+
deadline = time.monotonic() + timeout_seconds # 10 minute deadline before timing out
|
|
396
|
+
|
|
397
|
+
while time.monotonic() < deadline:
|
|
398
|
+
try:
|
|
399
|
+
state, reason = self.ecs_provider.get_service_rollout_state(
|
|
400
|
+
cluster_name=cluster_name, service_name=ecs_service_name, start_time=start_time
|
|
153
401
|
)
|
|
402
|
+
except Exception as e:
|
|
403
|
+
raise PlatformException(f"Failed to fetch ECS rollout state: {e}")
|
|
404
|
+
|
|
405
|
+
if state == "SUCCESSFUL":
|
|
406
|
+
self.io.info("\nECS deployment complete!")
|
|
407
|
+
return True
|
|
408
|
+
if state in ["STOPPED", "ROLLBACK_SUCCESSFUL", "ROLLBACK_FAILED"]:
|
|
409
|
+
raise PlatformException(f"\nECS deployment failed: {reason or 'unknown reason'}")
|
|
410
|
+
|
|
411
|
+
elapsed_time = int(time.time() - start_time)
|
|
412
|
+
self.io.info(f"Deployment in progress {elapsed_time}s")
|
|
413
|
+
time.sleep(POLL_INTERVAL_SECONDS)
|
|
414
|
+
|
|
415
|
+
raise PlatformException(
|
|
416
|
+
f"Timed out after {timeout_seconds}s waiting for '{ecs_service_name}' to stabilise."
|
|
417
|
+
)
|