dbt-platform-helper 12.2.4__py3-none-any.whl → 12.4.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 +6 -1
- dbt_platform_helper/commands/codebase.py +9 -80
- dbt_platform_helper/commands/conduit.py +25 -45
- dbt_platform_helper/commands/config.py +4 -4
- dbt_platform_helper/commands/copilot.py +13 -15
- dbt_platform_helper/commands/database.py +17 -4
- dbt_platform_helper/commands/environment.py +3 -2
- dbt_platform_helper/commands/secrets.py +1 -1
- dbt_platform_helper/domain/codebase.py +81 -63
- dbt_platform_helper/domain/conduit.py +42 -93
- dbt_platform_helper/domain/database_copy.py +48 -42
- dbt_platform_helper/domain/maintenance_page.py +8 -8
- dbt_platform_helper/platform_exception.py +5 -0
- dbt_platform_helper/providers/aws.py +32 -0
- dbt_platform_helper/providers/cloudformation.py +129 -100
- dbt_platform_helper/providers/copilot.py +33 -16
- dbt_platform_helper/providers/ecs.py +97 -74
- dbt_platform_helper/providers/load_balancers.py +11 -5
- dbt_platform_helper/providers/secrets.py +100 -59
- dbt_platform_helper/providers/validation.py +19 -0
- dbt_platform_helper/utils/application.py +14 -2
- dbt_platform_helper/utils/arn_parser.py +1 -1
- dbt_platform_helper/utils/aws.py +38 -12
- dbt_platform_helper/utils/git.py +2 -2
- dbt_platform_helper/utils/validation.py +57 -18
- dbt_platform_helper/utils/versioning.py +8 -8
- {dbt_platform_helper-12.2.4.dist-info → dbt_platform_helper-12.4.0.dist-info}/METADATA +1 -1
- {dbt_platform_helper-12.2.4.dist-info → dbt_platform_helper-12.4.0.dist-info}/RECORD +31 -30
- dbt_platform_helper/addons-template-map.yml +0 -29
- dbt_platform_helper/exceptions.py +0 -81
- {dbt_platform_helper-12.2.4.dist-info → dbt_platform_helper-12.4.0.dist-info}/LICENSE +0 -0
- {dbt_platform_helper-12.2.4.dist-info → dbt_platform_helper-12.4.0.dist-info}/WHEEL +0 -0
- {dbt_platform_helper-12.2.4.dist-info → dbt_platform_helper-12.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -8,13 +8,15 @@ from boto3 import Session
|
|
|
8
8
|
|
|
9
9
|
from dbt_platform_helper.constants import PLATFORM_CONFIG_FILE
|
|
10
10
|
from dbt_platform_helper.domain.maintenance_page import MaintenancePageProvider
|
|
11
|
-
from dbt_platform_helper.
|
|
12
|
-
from dbt_platform_helper.exceptions import AWSException
|
|
11
|
+
from dbt_platform_helper.providers.aws import AWSException
|
|
13
12
|
from dbt_platform_helper.utils.application import Application
|
|
13
|
+
from dbt_platform_helper.utils.application import ApplicationNotFoundException
|
|
14
14
|
from dbt_platform_helper.utils.application import load_application
|
|
15
15
|
from dbt_platform_helper.utils.aws import Vpc
|
|
16
16
|
from dbt_platform_helper.utils.aws import get_connection_string
|
|
17
17
|
from dbt_platform_helper.utils.aws import get_vpc_info_by_name
|
|
18
|
+
from dbt_platform_helper.utils.aws import wait_for_log_group_to_exist
|
|
19
|
+
from dbt_platform_helper.utils.files import apply_environment_defaults
|
|
18
20
|
from dbt_platform_helper.utils.messages import abort_with_error
|
|
19
21
|
from dbt_platform_helper.utils.validation import load_and_validate_platform_config
|
|
20
22
|
|
|
@@ -25,80 +27,80 @@ class DatabaseCopy:
|
|
|
25
27
|
app: str,
|
|
26
28
|
database: str,
|
|
27
29
|
auto_approve: bool = False,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
load_application: Callable[[str], Application] = load_application,
|
|
31
|
+
vpc_config: Callable[[Session, str, str, str], Vpc] = get_vpc_info_by_name,
|
|
32
|
+
db_connection_string: Callable[
|
|
31
33
|
[Session, str, str, str, Callable], str
|
|
32
34
|
] = get_connection_string,
|
|
33
35
|
maintenance_page_provider: Callable[
|
|
34
36
|
[str, str, list[str], str, str], None
|
|
35
37
|
] = MaintenancePageProvider(),
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
input: Callable[[str], str] = click.prompt,
|
|
39
|
+
echo: Callable[[str], str] = click.secho,
|
|
40
|
+
abort: Callable[[str], None] = abort_with_error,
|
|
39
41
|
):
|
|
40
42
|
self.app = app
|
|
41
43
|
self.database = database
|
|
42
44
|
self.auto_approve = auto_approve
|
|
43
|
-
self.
|
|
44
|
-
self.
|
|
45
|
+
self.vpc_config = vpc_config
|
|
46
|
+
self.db_connection_string = db_connection_string
|
|
45
47
|
self.maintenance_page_provider = maintenance_page_provider
|
|
46
|
-
self.
|
|
47
|
-
self.
|
|
48
|
-
self.
|
|
48
|
+
self.input = input
|
|
49
|
+
self.echo = echo
|
|
50
|
+
self.abort = abort
|
|
49
51
|
|
|
50
52
|
if not self.app:
|
|
51
53
|
if not Path(PLATFORM_CONFIG_FILE).exists():
|
|
52
|
-
self.
|
|
54
|
+
self.abort("You must either be in a deploy repo, or provide the --app option.")
|
|
53
55
|
|
|
54
56
|
config = load_and_validate_platform_config()
|
|
55
57
|
self.app = config["application"]
|
|
56
58
|
|
|
57
59
|
try:
|
|
58
|
-
self.application =
|
|
59
|
-
except
|
|
60
|
-
|
|
60
|
+
self.application = load_application(self.app)
|
|
61
|
+
except ApplicationNotFoundException:
|
|
62
|
+
abort(f"No such application '{app}'.")
|
|
61
63
|
|
|
62
|
-
def _execute_operation(self, is_dump: bool, env: str, vpc_name: str):
|
|
64
|
+
def _execute_operation(self, is_dump: bool, env: str, vpc_name: str, filename: str):
|
|
63
65
|
vpc_name = self.enrich_vpc_name(env, vpc_name)
|
|
64
66
|
|
|
65
67
|
environments = self.application.environments
|
|
66
68
|
environment = environments.get(env)
|
|
67
69
|
if not environment:
|
|
68
|
-
self.
|
|
70
|
+
self.abort(
|
|
69
71
|
f"No such environment '{env}'. Available environments are: {', '.join(environments.keys())}"
|
|
70
72
|
)
|
|
71
73
|
|
|
72
74
|
env_session = environment.session
|
|
73
75
|
|
|
74
76
|
try:
|
|
75
|
-
vpc_config = self.
|
|
77
|
+
vpc_config = self.vpc_config(env_session, self.app, env, vpc_name)
|
|
76
78
|
except AWSException as ex:
|
|
77
|
-
self.
|
|
79
|
+
self.abort(str(ex))
|
|
78
80
|
|
|
79
81
|
database_identifier = f"{self.app}-{env}-{self.database}"
|
|
80
82
|
|
|
81
83
|
try:
|
|
82
|
-
db_connection_string = self.
|
|
84
|
+
db_connection_string = self.db_connection_string(
|
|
83
85
|
env_session, self.app, env, database_identifier
|
|
84
86
|
)
|
|
85
87
|
except Exception as exc:
|
|
86
|
-
self.
|
|
88
|
+
self.abort(f"{exc} (Database: {database_identifier})")
|
|
87
89
|
|
|
88
90
|
try:
|
|
89
91
|
task_arn = self.run_database_copy_task(
|
|
90
|
-
env_session, env, vpc_config, is_dump, db_connection_string
|
|
92
|
+
env_session, env, vpc_config, is_dump, db_connection_string, filename
|
|
91
93
|
)
|
|
92
94
|
except Exception as exc:
|
|
93
|
-
self.
|
|
95
|
+
self.abort(f"{exc} (Account id: {self.account_id(env)})")
|
|
94
96
|
|
|
95
97
|
if is_dump:
|
|
96
98
|
message = f"Dumping {self.database} from the {env} environment into S3"
|
|
97
99
|
else:
|
|
98
100
|
message = f"Loading data into {self.database} in the {env} environment from S3"
|
|
99
101
|
|
|
100
|
-
self.
|
|
101
|
-
self.
|
|
102
|
+
self.echo(message, fg="white", bold=True)
|
|
103
|
+
self.echo(
|
|
102
104
|
f"Task {task_arn} started. Waiting for it to complete (this may take some time)...",
|
|
103
105
|
fg="white",
|
|
104
106
|
)
|
|
@@ -107,11 +109,10 @@ class DatabaseCopy:
|
|
|
107
109
|
def enrich_vpc_name(self, env, vpc_name):
|
|
108
110
|
if not vpc_name:
|
|
109
111
|
if not Path(PLATFORM_CONFIG_FILE).exists():
|
|
110
|
-
self.
|
|
111
|
-
"You must either be in a deploy repo, or provide the vpc name option."
|
|
112
|
-
)
|
|
112
|
+
self.abort("You must either be in a deploy repo, or provide the vpc name option.")
|
|
113
113
|
config = load_and_validate_platform_config()
|
|
114
|
-
|
|
114
|
+
env_config = apply_environment_defaults(config)["environments"]
|
|
115
|
+
vpc_name = env_config.get(env, {}).get("vpc")
|
|
115
116
|
return vpc_name
|
|
116
117
|
|
|
117
118
|
def run_database_copy_task(
|
|
@@ -121,12 +122,15 @@ class DatabaseCopy:
|
|
|
121
122
|
vpc_config: Vpc,
|
|
122
123
|
is_dump: bool,
|
|
123
124
|
db_connection_string: str,
|
|
125
|
+
filename: str,
|
|
124
126
|
) -> str:
|
|
125
127
|
client = session.client("ecs")
|
|
126
128
|
action = "dump" if is_dump else "load"
|
|
129
|
+
dump_file_name = filename if filename else "data_dump"
|
|
127
130
|
env_vars = [
|
|
128
131
|
{"name": "DATA_COPY_OPERATION", "value": action.upper()},
|
|
129
132
|
{"name": "DB_CONNECTION_STRING", "value": db_connection_string},
|
|
133
|
+
{"name": "DUMP_FILE_NAME", "value": dump_file_name},
|
|
130
134
|
]
|
|
131
135
|
if not is_dump:
|
|
132
136
|
env_vars.append({"name": "ECS_CLUSTER", "value": f"{self.app}-{env}"})
|
|
@@ -156,12 +160,12 @@ class DatabaseCopy:
|
|
|
156
160
|
|
|
157
161
|
return response.get("tasks", [{}])[0].get("taskArn")
|
|
158
162
|
|
|
159
|
-
def dump(self, env: str, vpc_name: str):
|
|
160
|
-
self._execute_operation(True, env, vpc_name)
|
|
163
|
+
def dump(self, env: str, vpc_name: str, filename: str = None):
|
|
164
|
+
self._execute_operation(True, env, vpc_name, filename)
|
|
161
165
|
|
|
162
|
-
def load(self, env: str, vpc_name: str):
|
|
166
|
+
def load(self, env: str, vpc_name: str, filename: str = None):
|
|
163
167
|
if self.is_confirmed_ready_to_load(env):
|
|
164
|
-
self._execute_operation(False, env, vpc_name)
|
|
168
|
+
self._execute_operation(False, env, vpc_name, filename)
|
|
165
169
|
|
|
166
170
|
def copy(
|
|
167
171
|
self,
|
|
@@ -176,8 +180,8 @@ class DatabaseCopy:
|
|
|
176
180
|
to_vpc = self.enrich_vpc_name(to_env, to_vpc)
|
|
177
181
|
if not no_maintenance_page:
|
|
178
182
|
self.maintenance_page_provider.activate(self.app, to_env, services, template, to_vpc)
|
|
179
|
-
self.dump(from_env, from_vpc)
|
|
180
|
-
self.load(to_env, to_vpc)
|
|
183
|
+
self.dump(from_env, from_vpc, f"data_dump_{to_env}")
|
|
184
|
+
self.load(to_env, to_vpc, f"data_dump_{to_env}")
|
|
181
185
|
if not no_maintenance_page:
|
|
182
186
|
self.maintenance_page_provider.deactivate(self.app, to_env)
|
|
183
187
|
|
|
@@ -185,7 +189,7 @@ class DatabaseCopy:
|
|
|
185
189
|
if self.auto_approve:
|
|
186
190
|
return True
|
|
187
191
|
|
|
188
|
-
user_input = self.
|
|
192
|
+
user_input = self.input(
|
|
189
193
|
f"\nWARNING: the load operation is destructive and will delete the {self.database} database in the {env} environment. Continue? (y/n)"
|
|
190
194
|
)
|
|
191
195
|
return user_input.lower().strip() in ["y", "yes"]
|
|
@@ -194,9 +198,11 @@ class DatabaseCopy:
|
|
|
194
198
|
action = "dump" if is_dump else "load"
|
|
195
199
|
log_group_name = f"/ecs/{self.app}-{env}-{self.database}-{action}"
|
|
196
200
|
log_group_arn = f"arn:aws:logs:eu-west-2:{self.account_id(env)}:log-group:{log_group_name}"
|
|
197
|
-
self.
|
|
201
|
+
self.echo(f"Tailing {log_group_name} logs", fg="yellow")
|
|
198
202
|
session = self.application.environments[env].session
|
|
199
|
-
|
|
203
|
+
log_client = session.client("logs")
|
|
204
|
+
wait_for_log_group_to_exist(log_client, log_group_name)
|
|
205
|
+
response = log_client.start_live_tail(logGroupIdentifiers=[log_group_arn])
|
|
200
206
|
|
|
201
207
|
stopped = False
|
|
202
208
|
for data in response["responseStream"]:
|
|
@@ -210,9 +216,9 @@ class DatabaseCopy:
|
|
|
210
216
|
match = re.match(r"(Stopping|Aborting) data (load|dump).*", message)
|
|
211
217
|
if match:
|
|
212
218
|
if match.group(1) == "Aborting":
|
|
213
|
-
self.
|
|
219
|
+
self.abort("Task aborted abnormally. See logs above for details.")
|
|
214
220
|
stopped = True
|
|
215
|
-
self.
|
|
221
|
+
self.echo(message)
|
|
216
222
|
|
|
217
223
|
def account_id(self, env):
|
|
218
224
|
envs = self.application.environments
|
|
@@ -9,9 +9,9 @@ from typing import Union
|
|
|
9
9
|
import boto3
|
|
10
10
|
import click
|
|
11
11
|
|
|
12
|
-
from dbt_platform_helper.providers.load_balancers import
|
|
13
|
-
from dbt_platform_helper.providers.load_balancers import
|
|
14
|
-
from dbt_platform_helper.providers.load_balancers import
|
|
12
|
+
from dbt_platform_helper.providers.load_balancers import ListenerNotFoundException
|
|
13
|
+
from dbt_platform_helper.providers.load_balancers import ListenerRuleNotFoundException
|
|
14
|
+
from dbt_platform_helper.providers.load_balancers import LoadBalancerNotFoundException
|
|
15
15
|
from dbt_platform_helper.providers.load_balancers import find_https_listener
|
|
16
16
|
from dbt_platform_helper.utils.application import Environment
|
|
17
17
|
from dbt_platform_helper.utils.application import Service
|
|
@@ -75,13 +75,13 @@ class MaintenancePageProvider:
|
|
|
75
75
|
else:
|
|
76
76
|
raise click.Abort
|
|
77
77
|
|
|
78
|
-
except
|
|
78
|
+
except LoadBalancerNotFoundException:
|
|
79
79
|
click.secho(
|
|
80
80
|
f"No load balancer found for environment {env} in the application {app}.", fg="red"
|
|
81
81
|
)
|
|
82
82
|
raise click.Abort
|
|
83
83
|
|
|
84
|
-
except
|
|
84
|
+
except ListenerNotFoundException:
|
|
85
85
|
click.secho(
|
|
86
86
|
f"No HTTPS listener found for environment {env} in the application {app}.", fg="red"
|
|
87
87
|
)
|
|
@@ -110,13 +110,13 @@ class MaintenancePageProvider:
|
|
|
110
110
|
f"Maintenance page removed from environment {env} in application {app}", fg="green"
|
|
111
111
|
)
|
|
112
112
|
|
|
113
|
-
except
|
|
113
|
+
except LoadBalancerNotFoundException:
|
|
114
114
|
click.secho(
|
|
115
115
|
f"No load balancer found for environment {env} in the application {app}.", fg="red"
|
|
116
116
|
)
|
|
117
117
|
raise click.Abort
|
|
118
118
|
|
|
119
|
-
except
|
|
119
|
+
except ListenerNotFoundException:
|
|
120
120
|
click.secho(
|
|
121
121
|
f"No HTTPS listener found for environment {env} in the application {app}.", fg="red"
|
|
122
122
|
)
|
|
@@ -180,7 +180,7 @@ def remove_maintenance_page(session: boto3.Session, listener_arn: str):
|
|
|
180
180
|
deleted = delete_listener_rule(tag_descriptions, name, lb_client)
|
|
181
181
|
|
|
182
182
|
if name == "MaintenancePage" and not deleted:
|
|
183
|
-
raise
|
|
183
|
+
raise ListenerRuleNotFoundException()
|
|
184
184
|
|
|
185
185
|
|
|
186
186
|
def get_rules_tag_descriptions(rules: list, lb_client):
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
class ImageNotFoundException(AWSException):
|
|
16
|
+
def __init__(self, commit: str):
|
|
17
|
+
super().__init__(
|
|
18
|
+
f"""The commit hash "{commit}" has not been built into an image, try the `platform-helper codebase build` command first."""
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LogGroupNotFoundException(AWSException):
|
|
23
|
+
def __init__(self, log_group_name: str):
|
|
24
|
+
super().__init__(f"""No log group called "{log_group_name}".""")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Todo: This should probably be in the AWS Copilot provider, but was causing circular import when we tried it pre refactoring the utils/aws.py
|
|
28
|
+
class CopilotCodebaseNotFoundException(PlatformException):
|
|
29
|
+
def __init__(self, codebase: str):
|
|
30
|
+
super().__init__(
|
|
31
|
+
f"""The codebase "{codebase}" either does not exist or has not been deployed."""
|
|
32
|
+
)
|
|
@@ -1,105 +1,134 @@
|
|
|
1
1
|
import json
|
|
2
2
|
|
|
3
|
+
import botocore
|
|
3
4
|
from cfn_tools import dump_yaml
|
|
4
5
|
from cfn_tools import load_yaml
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
7
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CloudFormation:
|
|
11
|
+
def __init__(self, cloudformation_client, iam_client, ssm_client):
|
|
12
|
+
self.cloudformation_client = cloudformation_client
|
|
13
|
+
self.iam_client = iam_client
|
|
14
|
+
self.ssm_client = ssm_client
|
|
15
|
+
|
|
16
|
+
def add_stack_delete_policy_to_task_role(self, task_name: str):
|
|
17
|
+
stack_name = f"task-{task_name}"
|
|
18
|
+
stack_resources = self.cloudformation_client.list_stack_resources(StackName=stack_name)[
|
|
19
|
+
"StackResourceSummaries"
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
for resource in stack_resources:
|
|
23
|
+
if resource["LogicalResourceId"] == "DefaultTaskRole":
|
|
24
|
+
task_role_name = resource["PhysicalResourceId"]
|
|
25
|
+
self.iam_client.put_role_policy(
|
|
26
|
+
RoleName=task_role_name,
|
|
27
|
+
PolicyName="DeleteCloudFormationStack",
|
|
28
|
+
PolicyDocument=json.dumps(
|
|
29
|
+
{
|
|
30
|
+
"Version": "2012-10-17",
|
|
31
|
+
"Statement": [
|
|
32
|
+
{
|
|
33
|
+
"Action": ["cloudformation:DeleteStack"],
|
|
34
|
+
"Effect": "Allow",
|
|
35
|
+
"Resource": f"arn:aws:cloudformation:*:*:stack/{stack_name}/*",
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def update_conduit_stack_resources(
|
|
43
|
+
self,
|
|
44
|
+
application_name: str,
|
|
45
|
+
env: str,
|
|
46
|
+
addon_type: str,
|
|
47
|
+
addon_name: str,
|
|
48
|
+
task_name: str,
|
|
49
|
+
parameter_name: str,
|
|
50
|
+
access: str,
|
|
51
|
+
):
|
|
52
|
+
conduit_stack_name = f"task-{task_name}"
|
|
53
|
+
template = self.cloudformation_client.get_template(StackName=conduit_stack_name)
|
|
54
|
+
template_yml = load_yaml(template["TemplateBody"])
|
|
55
|
+
|
|
56
|
+
template_yml["Resources"]["LogGroup"]["DeletionPolicy"] = "Retain"
|
|
57
|
+
|
|
58
|
+
template_yml["Resources"]["TaskNameParameter"] = load_yaml(
|
|
59
|
+
f"""
|
|
60
|
+
Type: AWS::SSM::Parameter
|
|
61
|
+
Properties:
|
|
62
|
+
Name: {parameter_name}
|
|
63
|
+
Type: String
|
|
64
|
+
Value: {task_name}
|
|
65
|
+
"""
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
log_filter_role_arn = self.iam_client.get_role(RoleName="CWLtoSubscriptionFilterRole")[
|
|
69
|
+
"Role"
|
|
70
|
+
]["Arn"]
|
|
71
|
+
|
|
72
|
+
destination_log_group_arns = json.loads(
|
|
73
|
+
self.ssm_client.get_parameter(Name="/copilot/tools/central_log_groups")["Parameter"][
|
|
74
|
+
"Value"
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
destination_arn = destination_log_group_arns["dev"]
|
|
79
|
+
if env.lower() in ("prod", "production"):
|
|
80
|
+
destination_arn = destination_log_group_arns["prod"]
|
|
81
|
+
|
|
82
|
+
template_yml["Resources"]["SubscriptionFilter"] = load_yaml(
|
|
83
|
+
f"""
|
|
84
|
+
Type: AWS::Logs::SubscriptionFilter
|
|
85
|
+
DeletionPolicy: Retain
|
|
86
|
+
Properties:
|
|
87
|
+
RoleArn: {log_filter_role_arn}
|
|
88
|
+
LogGroupName: /copilot/{task_name}
|
|
89
|
+
FilterName: /copilot/conduit/{application_name}/{env}/{addon_type}/{addon_name}/{task_name.rsplit("-", 1)[1]}/{access}
|
|
90
|
+
FilterPattern: ''
|
|
91
|
+
DestinationArn: {destination_arn}
|
|
92
|
+
"""
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
params = []
|
|
96
|
+
# TODO Currently not covered by tests - see https://uktrade.atlassian.net/browse/DBTP-1582
|
|
97
|
+
if "Parameters" in template_yml:
|
|
98
|
+
for param in template_yml["Parameters"]:
|
|
99
|
+
params.append({"ParameterKey": param, "UsePreviousValue": True})
|
|
100
|
+
|
|
101
|
+
self.cloudformation_client.update_stack(
|
|
102
|
+
StackName=conduit_stack_name,
|
|
103
|
+
TemplateBody=dump_yaml(template_yml),
|
|
104
|
+
Parameters=params,
|
|
105
|
+
Capabilities=["CAPABILITY_IAM"],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return conduit_stack_name
|
|
109
|
+
|
|
110
|
+
def wait_for_cloudformation_to_reach_status(self, stack_status, stack_name):
|
|
111
|
+
waiter = self.cloudformation_client.get_waiter(stack_status)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
waiter.wait(StackName=stack_name, WaiterConfig={"Delay": 5, "MaxAttempts": 20})
|
|
115
|
+
except botocore.exceptions.WaiterError as err:
|
|
116
|
+
current_status = err.last_response.get("Stacks", [{}])[0].get("StackStatus", "")
|
|
117
|
+
|
|
118
|
+
if current_status in [
|
|
119
|
+
"ROLLBACK_IN_PROGRESS",
|
|
120
|
+
"UPDATE_ROLLBACK_IN_PROGRESS",
|
|
121
|
+
"ROLLBACK_FAILED",
|
|
122
|
+
]:
|
|
123
|
+
raise CloudFormationException(stack_name, current_status)
|
|
124
|
+
else:
|
|
125
|
+
raise CloudFormationException(
|
|
126
|
+
stack_name, f"Error while waiting for stack status: {str(err)}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class CloudFormationException(PlatformException):
|
|
131
|
+
def __init__(self, stack_name: str, current_status: str):
|
|
132
|
+
super().__init__(
|
|
133
|
+
f"The CloudFormation stack '{stack_name}' is not in a good state: {current_status}"
|
|
134
|
+
)
|