dbt-platform-helper 12.0.2__py3-none-any.whl → 12.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dbt-platform-helper might be problematic. Click here for more details.
- dbt_platform_helper/COMMANDS.md +7 -8
- dbt_platform_helper/commands/application.py +1 -0
- dbt_platform_helper/commands/codebase.py +63 -228
- dbt_platform_helper/commands/conduit.py +34 -409
- dbt_platform_helper/commands/secrets.py +1 -1
- dbt_platform_helper/constants.py +12 -1
- dbt_platform_helper/domain/codebase.py +222 -0
- dbt_platform_helper/domain/conduit.py +172 -0
- dbt_platform_helper/domain/database_copy.py +1 -1
- dbt_platform_helper/exceptions.py +61 -0
- dbt_platform_helper/providers/__init__.py +0 -0
- dbt_platform_helper/providers/cloudformation.py +105 -0
- dbt_platform_helper/providers/copilot.py +144 -0
- dbt_platform_helper/providers/ecs.py +78 -0
- dbt_platform_helper/providers/secrets.py +85 -0
- dbt_platform_helper/templates/addons/svc/prometheus-policy.yml +2 -0
- dbt_platform_helper/templates/pipelines/environments/manifest.yml +0 -1
- dbt_platform_helper/utils/application.py +1 -4
- dbt_platform_helper/utils/aws.py +132 -0
- dbt_platform_helper/utils/files.py +70 -0
- dbt_platform_helper/utils/git.py +13 -0
- dbt_platform_helper/utils/validation.py +121 -3
- {dbt_platform_helper-12.0.2.dist-info → dbt_platform_helper-12.2.0.dist-info}/METADATA +2 -1
- {dbt_platform_helper-12.0.2.dist-info → dbt_platform_helper-12.2.0.dist-info}/RECORD +27 -29
- {dbt_platform_helper-12.0.2.dist-info → dbt_platform_helper-12.2.0.dist-info}/WHEEL +1 -1
- dbt_platform_helper/templates/env/overrides/.gitignore +0 -12
- dbt_platform_helper/templates/env/overrides/README.md +0 -11
- dbt_platform_helper/templates/env/overrides/bin/override.ts +0 -9
- dbt_platform_helper/templates/env/overrides/cdk.json +0 -20
- dbt_platform_helper/templates/env/overrides/log_resource_policy.json +0 -68
- dbt_platform_helper/templates/env/overrides/package-lock.json +0 -4307
- dbt_platform_helper/templates/env/overrides/package.json +0 -27
- dbt_platform_helper/templates/env/overrides/stack.ts +0 -51
- dbt_platform_helper/templates/env/overrides/tsconfig.json +0 -32
- {dbt_platform_helper-12.0.2.dist-info → dbt_platform_helper-12.2.0.dist-info}/LICENSE +0 -0
- {dbt_platform_helper-12.0.2.dist-info → dbt_platform_helper-12.2.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import stat
|
|
3
|
+
import subprocess
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import requests
|
|
9
|
+
import yaml
|
|
10
|
+
from boto3 import Session
|
|
11
|
+
|
|
12
|
+
from dbt_platform_helper.exceptions import ApplicationDeploymentNotTriggered
|
|
13
|
+
from dbt_platform_helper.exceptions import ApplicationEnvironmentNotFoundError
|
|
14
|
+
from dbt_platform_helper.exceptions import NoCopilotCodebasesFoundError
|
|
15
|
+
from dbt_platform_helper.exceptions import NotInCodeBaseRepositoryError
|
|
16
|
+
from dbt_platform_helper.utils.application import Application
|
|
17
|
+
from dbt_platform_helper.utils.application import load_application
|
|
18
|
+
from dbt_platform_helper.utils.aws import check_codebase_exists
|
|
19
|
+
from dbt_platform_helper.utils.aws import check_image_exists
|
|
20
|
+
from dbt_platform_helper.utils.aws import get_aws_session_or_abort
|
|
21
|
+
from dbt_platform_helper.utils.aws import get_build_url_from_arn
|
|
22
|
+
from dbt_platform_helper.utils.aws import list_latest_images
|
|
23
|
+
from dbt_platform_helper.utils.aws import start_build_extraction
|
|
24
|
+
from dbt_platform_helper.utils.files import mkfile
|
|
25
|
+
from dbt_platform_helper.utils.git import check_if_commit_exists
|
|
26
|
+
from dbt_platform_helper.utils.template import setup_templates
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Codebase:
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
input_fn: Callable[[str], str] = click.prompt,
|
|
33
|
+
echo_fn: Callable[[str], str] = click.secho,
|
|
34
|
+
confirm_fn: Callable[[str], bool] = click.confirm,
|
|
35
|
+
load_application_fn: Callable[[str], Application] = load_application,
|
|
36
|
+
get_aws_session_or_abort_fn: Callable[[str], Session] = get_aws_session_or_abort,
|
|
37
|
+
check_codebase_exists_fn: Callable[[str], str] = check_codebase_exists,
|
|
38
|
+
check_image_exists_fn: Callable[[str], str] = check_image_exists,
|
|
39
|
+
get_build_url_from_arn_fn: Callable[[str], str] = get_build_url_from_arn,
|
|
40
|
+
list_latest_images_fn: Callable[[str], str] = list_latest_images,
|
|
41
|
+
start_build_extraction_fn: Callable[[str], str] = start_build_extraction,
|
|
42
|
+
check_if_commit_exists_fn: Callable[[str], str] = check_if_commit_exists,
|
|
43
|
+
subprocess: Callable[[str], str] = subprocess.run,
|
|
44
|
+
):
|
|
45
|
+
self.input_fn = input_fn
|
|
46
|
+
self.echo_fn = echo_fn
|
|
47
|
+
self.confirm_fn = confirm_fn
|
|
48
|
+
self.load_application_fn = load_application_fn
|
|
49
|
+
self.get_aws_session_or_abort_fn = get_aws_session_or_abort_fn
|
|
50
|
+
self.check_codebase_exists_fn = check_codebase_exists_fn
|
|
51
|
+
self.check_image_exists_fn = check_image_exists_fn
|
|
52
|
+
self.get_build_url_from_arn_fn = get_build_url_from_arn_fn
|
|
53
|
+
self.list_latest_images_fn = list_latest_images_fn
|
|
54
|
+
self.start_build_extraction_fn = start_build_extraction_fn
|
|
55
|
+
self.check_if_commit_exists_fn = check_if_commit_exists_fn
|
|
56
|
+
self.subprocess = subprocess
|
|
57
|
+
|
|
58
|
+
def prepare(self):
|
|
59
|
+
"""Sets up an application codebase for use within a DBT platform
|
|
60
|
+
project."""
|
|
61
|
+
templates = setup_templates()
|
|
62
|
+
|
|
63
|
+
repository = (
|
|
64
|
+
self.subprocess(["git", "remote", "get-url", "origin"], capture_output=True, text=True)
|
|
65
|
+
.stdout.split("/")[-1]
|
|
66
|
+
.strip()
|
|
67
|
+
.removesuffix(".git")
|
|
68
|
+
)
|
|
69
|
+
if repository.endswith("-deploy") or Path("./copilot").exists():
|
|
70
|
+
raise NotInCodeBaseRepositoryError
|
|
71
|
+
|
|
72
|
+
builder_configuration_url = "https://raw.githubusercontent.com/uktrade/ci-image-builder/main/image_builder/configuration/builder_configuration.yml"
|
|
73
|
+
builder_configuration_response = requests.get(builder_configuration_url)
|
|
74
|
+
builder_configuration_content = yaml.safe_load(
|
|
75
|
+
builder_configuration_response.content.decode("utf-8")
|
|
76
|
+
)
|
|
77
|
+
builder_versions = next(
|
|
78
|
+
(
|
|
79
|
+
item
|
|
80
|
+
for item in builder_configuration_content["builders"]
|
|
81
|
+
if item["name"] == "paketobuildpacks/builder-jammy-base"
|
|
82
|
+
),
|
|
83
|
+
None,
|
|
84
|
+
)
|
|
85
|
+
builder_version = max(x["version"] for x in builder_versions["versions"])
|
|
86
|
+
builder_version = min(builder_version, "0.4.240")
|
|
87
|
+
|
|
88
|
+
Path("./.copilot/phases").mkdir(parents=True, exist_ok=True)
|
|
89
|
+
image_build_run_contents = templates.get_template(f".copilot/image_build_run.sh").render()
|
|
90
|
+
|
|
91
|
+
config_contents = templates.get_template(f".copilot/config.yml").render(
|
|
92
|
+
repository=repository, builder_version=builder_version
|
|
93
|
+
)
|
|
94
|
+
self.echo_fn(
|
|
95
|
+
mkfile(
|
|
96
|
+
Path("."), ".copilot/image_build_run.sh", image_build_run_contents, overwrite=True
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
image_build_run_file = Path(".copilot/image_build_run.sh")
|
|
101
|
+
image_build_run_file.chmod(image_build_run_file.stat().st_mode | stat.S_IEXEC)
|
|
102
|
+
|
|
103
|
+
self.echo_fn(mkfile(Path("."), ".copilot/config.yml", config_contents, overwrite=True))
|
|
104
|
+
|
|
105
|
+
for phase in ["build", "install", "post_build", "pre_build"]:
|
|
106
|
+
phase_contents = templates.get_template(f".copilot/phases/{phase}.sh").render()
|
|
107
|
+
|
|
108
|
+
self.echo_fn(
|
|
109
|
+
mkfile(Path("./.copilot"), f"phases/{phase}.sh", phase_contents, overwrite=True)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def build(self, app: str, codebase: str, commit: str):
|
|
113
|
+
"""Trigger a CodePipeline pipeline based build."""
|
|
114
|
+
session = self.get_aws_session_or_abort_fn()
|
|
115
|
+
self.load_application_fn(app, default_session=session)
|
|
116
|
+
|
|
117
|
+
self.check_if_commit_exists_fn(commit)
|
|
118
|
+
|
|
119
|
+
codebuild_client = session.client("codebuild")
|
|
120
|
+
build_url = self.__start_build_with_confirmation(
|
|
121
|
+
self.confirm_fn,
|
|
122
|
+
codebuild_client,
|
|
123
|
+
self.get_build_url_from_arn_fn,
|
|
124
|
+
f'You are about to build "{app}" for "{codebase}" with commit "{commit}". Do you want to continue?',
|
|
125
|
+
{
|
|
126
|
+
"projectName": f"codebuild-{app}-{codebase}",
|
|
127
|
+
"artifactsOverride": {"type": "NO_ARTIFACTS"},
|
|
128
|
+
"sourceVersion": commit,
|
|
129
|
+
},
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if build_url:
|
|
133
|
+
return self.echo_fn(
|
|
134
|
+
f"Your build has been triggered. Check your build progress in the AWS Console: {build_url}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
raise ApplicationDeploymentNotTriggered()
|
|
138
|
+
|
|
139
|
+
def deploy(self, app, env, codebase, commit):
|
|
140
|
+
"""Trigger a CodePipeline pipeline based deployment."""
|
|
141
|
+
session = self.get_aws_session_or_abort_fn()
|
|
142
|
+
|
|
143
|
+
application = self.load_application_fn(app, default_session=session)
|
|
144
|
+
if not application.environments.get(env):
|
|
145
|
+
raise ApplicationEnvironmentNotFoundError()
|
|
146
|
+
|
|
147
|
+
json.loads(self.check_codebase_exists_fn(session, application, codebase))
|
|
148
|
+
|
|
149
|
+
self.check_image_exists_fn(session, application, codebase, commit)
|
|
150
|
+
|
|
151
|
+
codebuild_client = session.client("codebuild")
|
|
152
|
+
build_url = self.__start_build_with_confirmation(
|
|
153
|
+
self.confirm_fn,
|
|
154
|
+
codebuild_client,
|
|
155
|
+
self.get_build_url_from_arn_fn,
|
|
156
|
+
f'You are about to deploy "{app}" for "{codebase}" with commit "{commit}" to the "{env}" environment. Do you want to continue?',
|
|
157
|
+
{
|
|
158
|
+
"projectName": f"pipeline-{application.name}-{codebase}-BuildProject",
|
|
159
|
+
"artifactsOverride": {"type": "NO_ARTIFACTS"},
|
|
160
|
+
"sourceTypeOverride": "NO_SOURCE",
|
|
161
|
+
"environmentVariablesOverride": [
|
|
162
|
+
{"name": "COPILOT_ENVIRONMENT", "value": env},
|
|
163
|
+
{"name": "IMAGE_TAG", "value": f"commit-{commit}"},
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if build_url:
|
|
169
|
+
return self.echo_fn(
|
|
170
|
+
"Your deployment has been triggered. Check your build progress in the AWS Console: "
|
|
171
|
+
f"{build_url}",
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
raise ApplicationDeploymentNotTriggered()
|
|
175
|
+
|
|
176
|
+
def list(self, app: str, with_images: bool):
|
|
177
|
+
"""List available codebases for the application."""
|
|
178
|
+
session = self.get_aws_session_or_abort_fn()
|
|
179
|
+
application = self.load_application_fn(app, session)
|
|
180
|
+
ssm_client = session.client("ssm")
|
|
181
|
+
ecr_client = session.client("ecr")
|
|
182
|
+
codebases = self.__get_codebases(application, ssm_client)
|
|
183
|
+
|
|
184
|
+
self.echo_fn("The following codebases are available:")
|
|
185
|
+
|
|
186
|
+
for codebase in codebases:
|
|
187
|
+
self.echo_fn(f"- {codebase['name']} (https://github.com/{codebase['repository']})")
|
|
188
|
+
if with_images:
|
|
189
|
+
self.list_latest_images_fn(
|
|
190
|
+
ecr_client,
|
|
191
|
+
f"{application.name}/{codebase['name']}",
|
|
192
|
+
codebase["repository"],
|
|
193
|
+
self.echo_fn,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
self.echo_fn("")
|
|
197
|
+
|
|
198
|
+
def __get_codebases(self, application, ssm_client):
|
|
199
|
+
parameters = ssm_client.get_parameters_by_path(
|
|
200
|
+
Path=f"/copilot/applications/{application.name}/codebases",
|
|
201
|
+
Recursive=True,
|
|
202
|
+
)["Parameters"]
|
|
203
|
+
|
|
204
|
+
codebases = [json.loads(p["Value"]) for p in parameters]
|
|
205
|
+
|
|
206
|
+
if not codebases:
|
|
207
|
+
# TODO Is this really an error? Or just no codebases so we could return an empty list?
|
|
208
|
+
raise NoCopilotCodebasesFoundError
|
|
209
|
+
return codebases
|
|
210
|
+
|
|
211
|
+
def __start_build_with_confirmation(
|
|
212
|
+
self,
|
|
213
|
+
confirm_fn,
|
|
214
|
+
codebuild_client,
|
|
215
|
+
get_build_url_from_arn_fn,
|
|
216
|
+
confirmation_message,
|
|
217
|
+
build_options,
|
|
218
|
+
):
|
|
219
|
+
if confirm_fn(confirmation_message):
|
|
220
|
+
build_arn = self.start_build_extraction_fn(codebuild_client, build_options)
|
|
221
|
+
return get_build_url_from_arn_fn(build_arn)
|
|
222
|
+
return None
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from dbt_platform_helper.exceptions import ECSAgentNotRunning
|
|
7
|
+
from dbt_platform_helper.providers.cloudformation import (
|
|
8
|
+
add_stack_delete_policy_to_task_role,
|
|
9
|
+
)
|
|
10
|
+
from dbt_platform_helper.providers.cloudformation import update_conduit_stack_resources
|
|
11
|
+
from dbt_platform_helper.providers.cloudformation import (
|
|
12
|
+
wait_for_cloudformation_to_reach_status,
|
|
13
|
+
)
|
|
14
|
+
from dbt_platform_helper.providers.copilot import connect_to_addon_client_task
|
|
15
|
+
from dbt_platform_helper.providers.copilot import create_addon_client_task
|
|
16
|
+
from dbt_platform_helper.providers.copilot import create_postgres_admin_task
|
|
17
|
+
from dbt_platform_helper.providers.ecs import ecs_exec_is_available
|
|
18
|
+
from dbt_platform_helper.providers.ecs import get_cluster_arn
|
|
19
|
+
from dbt_platform_helper.providers.ecs import get_ecs_task_arns
|
|
20
|
+
from dbt_platform_helper.providers.ecs import get_or_create_task_name
|
|
21
|
+
from dbt_platform_helper.providers.secrets import get_addon_type
|
|
22
|
+
from dbt_platform_helper.providers.secrets import get_parameter_name
|
|
23
|
+
from dbt_platform_helper.utils.application import Application
|
|
24
|
+
from dbt_platform_helper.utils.messages import abort_with_error
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Conduit:
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
application: Application,
|
|
31
|
+
echo_fn: Callable[[str], str] = click.secho,
|
|
32
|
+
subprocess_fn: subprocess = subprocess,
|
|
33
|
+
get_ecs_task_arns_fn=get_ecs_task_arns,
|
|
34
|
+
connect_to_addon_client_task_fn=connect_to_addon_client_task,
|
|
35
|
+
create_addon_client_task_fn=create_addon_client_task,
|
|
36
|
+
create_postgres_admin_task_fn=create_postgres_admin_task,
|
|
37
|
+
get_addon_type_fn=get_addon_type,
|
|
38
|
+
ecs_exec_is_available_fn=ecs_exec_is_available,
|
|
39
|
+
get_cluster_arn_fn=get_cluster_arn,
|
|
40
|
+
get_parameter_name_fn=get_parameter_name,
|
|
41
|
+
get_or_create_task_name_fn=get_or_create_task_name,
|
|
42
|
+
add_stack_delete_policy_to_task_role_fn=add_stack_delete_policy_to_task_role,
|
|
43
|
+
update_conduit_stack_resources_fn=update_conduit_stack_resources,
|
|
44
|
+
wait_for_cloudformation_to_reach_status_fn=wait_for_cloudformation_to_reach_status,
|
|
45
|
+
abort_fn=abort_with_error,
|
|
46
|
+
):
|
|
47
|
+
|
|
48
|
+
self.application = application
|
|
49
|
+
self.subprocess_fn = subprocess_fn
|
|
50
|
+
self.echo_fn = echo_fn
|
|
51
|
+
self.get_ecs_task_arns_fn = get_ecs_task_arns_fn
|
|
52
|
+
self.connect_to_addon_client_task_fn = connect_to_addon_client_task_fn
|
|
53
|
+
self.create_addon_client_task_fn = create_addon_client_task_fn
|
|
54
|
+
self.create_postgres_admin_task = create_postgres_admin_task_fn
|
|
55
|
+
self.get_addon_type_fn = get_addon_type_fn
|
|
56
|
+
self.ecs_exec_is_available_fn = ecs_exec_is_available_fn
|
|
57
|
+
self.get_cluster_arn_fn = get_cluster_arn_fn
|
|
58
|
+
self.get_parameter_name_fn = get_parameter_name_fn
|
|
59
|
+
self.get_or_create_task_name_fn = get_or_create_task_name_fn
|
|
60
|
+
self.add_stack_delete_policy_to_task_role_fn = add_stack_delete_policy_to_task_role_fn
|
|
61
|
+
self.update_conduit_stack_resources_fn = update_conduit_stack_resources_fn
|
|
62
|
+
self.wait_for_cloudformation_to_reach_status_fn = wait_for_cloudformation_to_reach_status_fn
|
|
63
|
+
self.abort_fn = abort_fn
|
|
64
|
+
|
|
65
|
+
def start(self, env: str, addon_name: str, access: str = "read"):
|
|
66
|
+
clients = self._initialise_clients(env)
|
|
67
|
+
addon_type, cluster_arn, parameter_name, task_name = self._get_addon_details(
|
|
68
|
+
env, addon_name, access
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
self.echo_fn(f"Checking if a conduit task is already running for {addon_type}")
|
|
72
|
+
task_arn = self.get_ecs_task_arns_fn(clients["ecs"], cluster_arn, task_name)
|
|
73
|
+
if not task_arn:
|
|
74
|
+
self.echo_fn("Creating conduit task")
|
|
75
|
+
self.create_addon_client_task_fn(
|
|
76
|
+
clients["iam"],
|
|
77
|
+
clients["ssm"],
|
|
78
|
+
clients["secrets_manager"],
|
|
79
|
+
self.subprocess_fn,
|
|
80
|
+
self.application,
|
|
81
|
+
env,
|
|
82
|
+
addon_type,
|
|
83
|
+
addon_name,
|
|
84
|
+
task_name,
|
|
85
|
+
access,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self.echo_fn("Updating conduit task")
|
|
89
|
+
self._update_stack_resources(
|
|
90
|
+
clients["cloudformation"],
|
|
91
|
+
clients["iam"],
|
|
92
|
+
clients["ssm"],
|
|
93
|
+
self.application.name,
|
|
94
|
+
env,
|
|
95
|
+
addon_type,
|
|
96
|
+
addon_name,
|
|
97
|
+
task_name,
|
|
98
|
+
parameter_name,
|
|
99
|
+
access,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
task_arn = self.get_ecs_task_arns_fn(clients["ecs"], cluster_arn, task_name)
|
|
103
|
+
|
|
104
|
+
else:
|
|
105
|
+
self.echo_fn("Conduit task already running")
|
|
106
|
+
|
|
107
|
+
self.echo_fn(f"Checking if exec is available for conduit task...")
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
self.ecs_exec_is_available_fn(clients["ecs"], cluster_arn, task_arn)
|
|
111
|
+
except ECSAgentNotRunning:
|
|
112
|
+
self.abort_fn('ECS exec agent never reached "RUNNING" status')
|
|
113
|
+
|
|
114
|
+
self.echo_fn("Connecting to conduit task")
|
|
115
|
+
self.connect_to_addon_client_task_fn(
|
|
116
|
+
clients["ecs"], self.subprocess_fn, self.application.name, env, cluster_arn, task_name
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def _initialise_clients(self, env):
|
|
120
|
+
return {
|
|
121
|
+
"ecs": self.application.environments[env].session.client("ecs"),
|
|
122
|
+
"iam": self.application.environments[env].session.client("iam"),
|
|
123
|
+
"ssm": self.application.environments[env].session.client("ssm"),
|
|
124
|
+
"cloudformation": self.application.environments[env].session.client("cloudformation"),
|
|
125
|
+
"secrets_manager": self.application.environments[env].session.client("secretsmanager"),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
def _get_addon_details(self, env, addon_name, access):
|
|
129
|
+
ssm_client = self.application.environments[env].session.client("ssm")
|
|
130
|
+
ecs_client = self.application.environments[env].session.client("ecs")
|
|
131
|
+
|
|
132
|
+
addon_type = self.get_addon_type_fn(ssm_client, self.application.name, env, addon_name)
|
|
133
|
+
cluster_arn = self.get_cluster_arn_fn(ecs_client, self.application.name, env)
|
|
134
|
+
parameter_name = self.get_parameter_name_fn(
|
|
135
|
+
self.application.name, env, addon_type, addon_name, access
|
|
136
|
+
)
|
|
137
|
+
task_name = self.get_or_create_task_name_fn(
|
|
138
|
+
ssm_client, self.application.name, env, addon_name, parameter_name
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return addon_type, cluster_arn, parameter_name, task_name
|
|
142
|
+
|
|
143
|
+
def _update_stack_resources(
|
|
144
|
+
self,
|
|
145
|
+
cloudformation_client,
|
|
146
|
+
iam_client,
|
|
147
|
+
ssm_client,
|
|
148
|
+
app_name,
|
|
149
|
+
env,
|
|
150
|
+
addon_type,
|
|
151
|
+
addon_name,
|
|
152
|
+
task_name,
|
|
153
|
+
parameter_name,
|
|
154
|
+
access,
|
|
155
|
+
):
|
|
156
|
+
self.add_stack_delete_policy_to_task_role_fn(cloudformation_client, iam_client, task_name)
|
|
157
|
+
stack_name = self.update_conduit_stack_resources_fn(
|
|
158
|
+
cloudformation_client,
|
|
159
|
+
iam_client,
|
|
160
|
+
ssm_client,
|
|
161
|
+
app_name,
|
|
162
|
+
env,
|
|
163
|
+
addon_type,
|
|
164
|
+
addon_name,
|
|
165
|
+
task_name,
|
|
166
|
+
parameter_name,
|
|
167
|
+
access,
|
|
168
|
+
)
|
|
169
|
+
self.echo_fn("Waiting for conduit task update to complete...")
|
|
170
|
+
self.wait_for_cloudformation_to_reach_status_fn(
|
|
171
|
+
cloudformation_client, "stack_update_complete", stack_name
|
|
172
|
+
)
|
|
@@ -8,9 +8,9 @@ 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.exceptions import ApplicationNotFoundError
|
|
11
12
|
from dbt_platform_helper.exceptions import AWSException
|
|
12
13
|
from dbt_platform_helper.utils.application import Application
|
|
13
|
-
from dbt_platform_helper.utils.application import ApplicationNotFoundError
|
|
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
|
|
@@ -18,3 +18,64 @@ class IncompatibleMinorVersion(ValidationException):
|
|
|
18
18
|
super().__init__()
|
|
19
19
|
self.app_version = app_version
|
|
20
20
|
self.check_version = check_version
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class NoClusterError(AWSException):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CreateTaskTimeoutError(AWSException):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ParameterNotFoundError(AWSException):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AddonNotFoundError(AWSException):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class InvalidAddonTypeError(AWSException):
|
|
40
|
+
def __init__(self, addon_type):
|
|
41
|
+
self.addon_type = addon_type
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AddonTypeMissingFromConfigError(AWSException):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CopilotCodebaseNotFoundError(Exception):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class NotInCodeBaseRepositoryError(Exception):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class NoCopilotCodebasesFoundError(Exception):
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ImageNotFoundError(Exception):
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class ApplicationDeploymentNotTriggered(Exception):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ApplicationNotFoundError(Exception):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ApplicationEnvironmentNotFoundError(Exception):
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SecretNotFoundError(AWSException):
|
|
77
|
+
pass
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ECSAgentNotRunning(AWSException):
|
|
81
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from cfn_tools import dump_yaml
|
|
4
|
+
from cfn_tools import load_yaml
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def add_stack_delete_policy_to_task_role(cloudformation_client, iam_client, task_name: str):
|
|
8
|
+
|
|
9
|
+
stack_name = f"task-{task_name}"
|
|
10
|
+
stack_resources = cloudformation_client.list_stack_resources(StackName=stack_name)[
|
|
11
|
+
"StackResourceSummaries"
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
for resource in stack_resources:
|
|
15
|
+
if resource["LogicalResourceId"] == "DefaultTaskRole":
|
|
16
|
+
task_role_name = resource["PhysicalResourceId"]
|
|
17
|
+
iam_client.put_role_policy(
|
|
18
|
+
RoleName=task_role_name,
|
|
19
|
+
PolicyName="DeleteCloudFormationStack",
|
|
20
|
+
PolicyDocument=json.dumps(
|
|
21
|
+
{
|
|
22
|
+
"Version": "2012-10-17",
|
|
23
|
+
"Statement": [
|
|
24
|
+
{
|
|
25
|
+
"Action": ["cloudformation:DeleteStack"],
|
|
26
|
+
"Effect": "Allow",
|
|
27
|
+
"Resource": f"arn:aws:cloudformation:*:*:stack/{stack_name}/*",
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def update_conduit_stack_resources(
|
|
36
|
+
cloudformation_client,
|
|
37
|
+
iam_client,
|
|
38
|
+
ssm_client,
|
|
39
|
+
application_name: str,
|
|
40
|
+
env: str,
|
|
41
|
+
addon_type: str,
|
|
42
|
+
addon_name: str,
|
|
43
|
+
task_name: str,
|
|
44
|
+
parameter_name: str,
|
|
45
|
+
access: str,
|
|
46
|
+
):
|
|
47
|
+
|
|
48
|
+
conduit_stack_name = f"task-{task_name}"
|
|
49
|
+
template = cloudformation_client.get_template(StackName=conduit_stack_name)
|
|
50
|
+
template_yml = load_yaml(template["TemplateBody"])
|
|
51
|
+
template_yml["Resources"]["LogGroup"]["DeletionPolicy"] = "Retain"
|
|
52
|
+
template_yml["Resources"]["TaskNameParameter"] = load_yaml(
|
|
53
|
+
f"""
|
|
54
|
+
Type: AWS::SSM::Parameter
|
|
55
|
+
Properties:
|
|
56
|
+
Name: {parameter_name}
|
|
57
|
+
Type: String
|
|
58
|
+
Value: {task_name}
|
|
59
|
+
"""
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
log_filter_role_arn = iam_client.get_role(RoleName="CWLtoSubscriptionFilterRole")["Role"]["Arn"]
|
|
63
|
+
|
|
64
|
+
destination_log_group_arns = json.loads(
|
|
65
|
+
ssm_client.get_parameter(Name="/copilot/tools/central_log_groups")["Parameter"]["Value"]
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
destination_arn = destination_log_group_arns["dev"]
|
|
69
|
+
if env.lower() in ("prod", "production"):
|
|
70
|
+
destination_arn = destination_log_group_arns["prod"]
|
|
71
|
+
|
|
72
|
+
template_yml["Resources"]["SubscriptionFilter"] = load_yaml(
|
|
73
|
+
f"""
|
|
74
|
+
Type: AWS::Logs::SubscriptionFilter
|
|
75
|
+
DeletionPolicy: Retain
|
|
76
|
+
Properties:
|
|
77
|
+
RoleArn: {log_filter_role_arn}
|
|
78
|
+
LogGroupName: /copilot/{task_name}
|
|
79
|
+
FilterName: /copilot/conduit/{application_name}/{env}/{addon_type}/{addon_name}/{task_name.rsplit("-", 1)[1]}/{access}
|
|
80
|
+
FilterPattern: ''
|
|
81
|
+
DestinationArn: {destination_arn}
|
|
82
|
+
"""
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
params = []
|
|
86
|
+
if "Parameters" in template_yml:
|
|
87
|
+
for param in template_yml["Parameters"]:
|
|
88
|
+
# TODO testing missed in codecov, update test to assert on method call below with params including ExistingParameter from cloudformation template.
|
|
89
|
+
params.append({"ParameterKey": param, "UsePreviousValue": True})
|
|
90
|
+
|
|
91
|
+
cloudformation_client.update_stack(
|
|
92
|
+
StackName=conduit_stack_name,
|
|
93
|
+
TemplateBody=dump_yaml(template_yml),
|
|
94
|
+
Parameters=params,
|
|
95
|
+
Capabilities=["CAPABILITY_IAM"],
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return conduit_stack_name
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# TODO Catch errors and raise a more human friendly Exception is the CloudFormation stack goes into a "unhappy" state, e.g. ROLLBACK_IN_PROGRESS. Currently we get things like botocore.exceptions.WaiterError: Waiter StackUpdateComplete failed: Waiter encountered a terminal failure state: For expression "Stacks[].StackStatus" we matched expected path: "UPDATE_ROLLBACK_COMPLETE" at least once
|
|
102
|
+
def wait_for_cloudformation_to_reach_status(cloudformation_client, stack_status, stack_name):
|
|
103
|
+
|
|
104
|
+
waiter = cloudformation_client.get_waiter(stack_status)
|
|
105
|
+
waiter.wait(StackName=stack_name, WaiterConfig={"Delay": 5, "MaxAttempts": 20})
|