dbt-platform-helper 15.3.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 +36 -11
- dbt_platform_helper/commands/application.py +2 -1
- dbt_platform_helper/commands/conduit.py +1 -1
- dbt_platform_helper/commands/environment.py +12 -1
- dbt_platform_helper/commands/generate.py +0 -2
- dbt_platform_helper/commands/internal.py +140 -0
- dbt_platform_helper/commands/pipeline.py +15 -3
- dbt_platform_helper/commands/secrets.py +37 -89
- dbt_platform_helper/commands/version.py +3 -2
- dbt_platform_helper/constants.py +38 -2
- dbt_platform_helper/domain/conduit.py +22 -9
- dbt_platform_helper/domain/config.py +30 -1
- dbt_platform_helper/domain/database_copy.py +1 -1
- dbt_platform_helper/domain/maintenance_page.py +27 -3
- dbt_platform_helper/domain/pipelines.py +36 -60
- dbt_platform_helper/domain/secrets.py +279 -0
- dbt_platform_helper/domain/service.py +570 -0
- dbt_platform_helper/domain/terraform_environment.py +7 -29
- dbt_platform_helper/domain/update_alb_rules.py +412 -0
- dbt_platform_helper/domain/versioning.py +124 -13
- dbt_platform_helper/entities/platform_config_schema.py +31 -11
- dbt_platform_helper/entities/semantic_version.py +2 -0
- dbt_platform_helper/entities/service.py +339 -0
- dbt_platform_helper/providers/autoscaling.py +24 -0
- 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 +17 -2
- dbt_platform_helper/providers/config_validator.py +87 -2
- dbt_platform_helper/providers/ecs.py +131 -11
- dbt_platform_helper/providers/environment_variable.py +2 -2
- dbt_platform_helper/providers/io.py +9 -2
- dbt_platform_helper/providers/load_balancers.py +122 -16
- dbt_platform_helper/providers/logs.py +72 -0
- dbt_platform_helper/providers/parameter_store.py +97 -10
- dbt_platform_helper/providers/s3.py +21 -0
- dbt_platform_helper/providers/terraform_manifest.py +97 -13
- dbt_platform_helper/providers/vpc.py +36 -5
- dbt_platform_helper/providers/yaml_file.py +35 -0
- dbt_platform_helper/templates/environment-pipelines/main.tf +3 -2
- dbt_platform_helper/templates/svc/overrides/cfn.patches.yml +5 -0
- dbt_platform_helper/utils/application.py +104 -21
- dbt_platform_helper/utils/aws.py +11 -10
- dbt_platform_helper/utils/deep_merge.py +10 -0
- dbt_platform_helper/utils/git.py +1 -1
- {dbt_platform_helper-15.3.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/METADATA +8 -17
- {dbt_platform_helper-15.3.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/RECORD +50 -41
- {dbt_platform_helper-15.3.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/WHEEL +1 -1
- platform_helper.py +2 -0
- {dbt_platform_helper-15.3.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/entry_points.txt +0 -0
- {dbt_platform_helper-15.3.0.dist-info → dbt_platform_helper-15.16.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -7,6 +7,7 @@ from typing import Dict
|
|
|
7
7
|
from prettytable import PrettyTable
|
|
8
8
|
|
|
9
9
|
from dbt_platform_helper.constants import PLATFORM_CONFIG_FILE
|
|
10
|
+
from dbt_platform_helper.constants import STANDARD_PLATFORM_SSO_ROLES
|
|
10
11
|
from dbt_platform_helper.domain.versioning import AWSVersioning
|
|
11
12
|
from dbt_platform_helper.domain.versioning import CopilotVersioning
|
|
12
13
|
from dbt_platform_helper.domain.versioning import PlatformHelperVersioning
|
|
@@ -140,6 +141,10 @@ class Config:
|
|
|
140
141
|
if self.io.confirm(
|
|
141
142
|
f"This command is destructive and will overwrite file contents at {file_path}. Are you sure you want to continue?"
|
|
142
143
|
):
|
|
144
|
+
self.io.info(
|
|
145
|
+
"Fetching credentials... this may take longer if you have access to many accounts."
|
|
146
|
+
)
|
|
147
|
+
|
|
143
148
|
with open(aws_config_path, "w") as config_file:
|
|
144
149
|
config_file.write(AWS_CONFIG)
|
|
145
150
|
|
|
@@ -147,7 +152,9 @@ class Config:
|
|
|
147
152
|
config_file.write(f"[profile {account['accountName']}]\n")
|
|
148
153
|
config_file.write("sso_session = uktrade\n")
|
|
149
154
|
config_file.write(f"sso_account_id = {account['accountId']}\n")
|
|
150
|
-
config_file.write(
|
|
155
|
+
config_file.write(
|
|
156
|
+
f"sso_role_name = {self._retrieve_role_for_aws_account(aws_sso_token=access_token, account_id=account['accountId'])}\n"
|
|
157
|
+
)
|
|
151
158
|
config_file.write("region = eu-west-2\n")
|
|
152
159
|
config_file.write("output = json\n")
|
|
153
160
|
config_file.write("\n")
|
|
@@ -177,6 +184,28 @@ class Config:
|
|
|
177
184
|
)
|
|
178
185
|
return accounts_list
|
|
179
186
|
|
|
187
|
+
def _retrieve_role_for_aws_account(self, aws_sso_token, account_id):
|
|
188
|
+
"""
|
|
189
|
+
Selects the most appropriate IAM role for a given AWS account.
|
|
190
|
+
|
|
191
|
+
Roles listed in STANDARD_PLATFORM_SSO_ROLES are preferred if available.
|
|
192
|
+
If none are found, the first available role returned by
|
|
193
|
+
sso.list_account_roles is used.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
role_list = self.sso.list_account_roles(
|
|
197
|
+
access_token=aws_sso_token,
|
|
198
|
+
account_id=account_id,
|
|
199
|
+
max_results=100,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
roles_retrieved = [r["roleName"] for r in role_list]
|
|
203
|
+
|
|
204
|
+
for role in STANDARD_PLATFORM_SSO_ROLES:
|
|
205
|
+
if role in roles_retrieved:
|
|
206
|
+
return role
|
|
207
|
+
return role_list[0]["roleName"]
|
|
208
|
+
|
|
180
209
|
def _add_version_status_row(
|
|
181
210
|
self, table: PrettyTable, header: str, version_status: VersionStatus
|
|
182
211
|
):
|
|
@@ -68,7 +68,7 @@ class DatabaseCopy:
|
|
|
68
68
|
environment = environments.get(env)
|
|
69
69
|
if not environment:
|
|
70
70
|
self.io.abort_with_error(
|
|
71
|
-
f"
|
|
71
|
+
f"Environment '{env}' cannot be found in your account configuration in parameter store. Available environments are: {', '.join(environments.keys())}. Please check that the Terraform infrastructure for your environment is up to date."
|
|
72
72
|
)
|
|
73
73
|
|
|
74
74
|
env_session = environment.session
|
|
@@ -10,6 +10,8 @@ from typing import Union
|
|
|
10
10
|
|
|
11
11
|
import click
|
|
12
12
|
|
|
13
|
+
from dbt_platform_helper.constants import MAINTENANCE_PAGE_REASON
|
|
14
|
+
from dbt_platform_helper.constants import MANAGED_BY_PLATFORM
|
|
13
15
|
from dbt_platform_helper.platform_exception import PlatformException
|
|
14
16
|
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
15
17
|
from dbt_platform_helper.providers.load_balancers import ListenerRuleNotFoundException
|
|
@@ -199,7 +201,13 @@ class MaintenancePage:
|
|
|
199
201
|
"AllowedIps",
|
|
200
202
|
next(rule_priority),
|
|
201
203
|
service_conditions,
|
|
202
|
-
[
|
|
204
|
+
[
|
|
205
|
+
{"Key": "application", "Value": app},
|
|
206
|
+
{"Key": "environment", "Value": env},
|
|
207
|
+
{"Key": "reason", "Value": MAINTENANCE_PAGE_REASON},
|
|
208
|
+
{"Key": "managed-by", "Value": MANAGED_BY_PLATFORM},
|
|
209
|
+
{"Key": "service", "Value": svc.name},
|
|
210
|
+
],
|
|
203
211
|
)
|
|
204
212
|
self.load_balancer.create_source_ip_rule(
|
|
205
213
|
listener_arn,
|
|
@@ -208,7 +216,13 @@ class MaintenancePage:
|
|
|
208
216
|
"AllowedSourceIps",
|
|
209
217
|
next(rule_priority),
|
|
210
218
|
service_conditions,
|
|
211
|
-
[
|
|
219
|
+
[
|
|
220
|
+
{"Key": "application", "Value": app},
|
|
221
|
+
{"Key": "environment", "Value": env},
|
|
222
|
+
{"Key": "reason", "Value": MAINTENANCE_PAGE_REASON},
|
|
223
|
+
{"Key": "managed-by", "Value": MANAGED_BY_PLATFORM},
|
|
224
|
+
{"Key": "service", "Value": svc.name},
|
|
225
|
+
],
|
|
212
226
|
)
|
|
213
227
|
|
|
214
228
|
self.load_balancer.create_header_rule(
|
|
@@ -219,7 +233,13 @@ class MaintenancePage:
|
|
|
219
233
|
"BypassIpFilter",
|
|
220
234
|
next(rule_priority),
|
|
221
235
|
service_conditions,
|
|
222
|
-
[
|
|
236
|
+
[
|
|
237
|
+
{"Key": "application", "Value": app},
|
|
238
|
+
{"Key": "environment", "Value": env},
|
|
239
|
+
{"Key": "reason", "Value": MAINTENANCE_PAGE_REASON},
|
|
240
|
+
{"Key": "managed-by", "Value": MANAGED_BY_PLATFORM},
|
|
241
|
+
{"Key": "service", "Value": svc.name},
|
|
242
|
+
],
|
|
223
243
|
)
|
|
224
244
|
|
|
225
245
|
# add to accumilating list of conditions for maintenace page rule
|
|
@@ -267,8 +287,12 @@ class MaintenancePage:
|
|
|
267
287
|
}
|
|
268
288
|
],
|
|
269
289
|
tags=[
|
|
290
|
+
{"Key": "application", "Value": app},
|
|
291
|
+
{"Key": "environment", "Value": env},
|
|
292
|
+
{"Key": "reason", "Value": MAINTENANCE_PAGE_REASON},
|
|
270
293
|
{"Key": "name", "Value": "MaintenancePage"},
|
|
271
294
|
{"Key": "type", "Value": template},
|
|
295
|
+
{"Key": "managed-by", "Value": MANAGED_BY_PLATFORM},
|
|
272
296
|
],
|
|
273
297
|
)
|
|
274
298
|
except Exception as e:
|
|
@@ -5,24 +5,14 @@ from shutil import rmtree
|
|
|
5
5
|
|
|
6
6
|
from dbt_platform_helper.constants import CODEBASE_PIPELINES_KEY
|
|
7
7
|
from dbt_platform_helper.constants import ENVIRONMENT_PIPELINES_KEY
|
|
8
|
-
from dbt_platform_helper.constants import PLATFORM_HELPER_VERSION_OVERRIDE_KEY
|
|
9
8
|
from dbt_platform_helper.constants import SUPPORTED_AWS_PROVIDER_VERSION
|
|
10
9
|
from dbt_platform_helper.constants import SUPPORTED_TERRAFORM_VERSION
|
|
11
|
-
from dbt_platform_helper.
|
|
12
|
-
TERRAFORM_CODEBASE_PIPELINES_MODULE_SOURCE_OVERRIDE_ENV_VAR,
|
|
13
|
-
)
|
|
14
|
-
from dbt_platform_helper.constants import (
|
|
15
|
-
TERRAFORM_ENVIRONMENT_PIPELINES_MODULE_SOURCE_OVERRIDE_ENV_VAR,
|
|
16
|
-
)
|
|
10
|
+
from dbt_platform_helper.domain.versioning import PlatformHelperVersioning
|
|
17
11
|
from dbt_platform_helper.providers.config import ConfigProvider
|
|
18
12
|
from dbt_platform_helper.providers.ecr import ECRProvider
|
|
19
|
-
from dbt_platform_helper.providers.environment_variable import (
|
|
20
|
-
EnvironmentVariableProvider,
|
|
21
|
-
)
|
|
22
13
|
from dbt_platform_helper.providers.files import FileProvider
|
|
23
14
|
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
24
15
|
from dbt_platform_helper.providers.terraform_manifest import TerraformManifestProvider
|
|
25
|
-
from dbt_platform_helper.utils.application import get_application_name
|
|
26
16
|
from dbt_platform_helper.utils.template import setup_templates
|
|
27
17
|
|
|
28
18
|
|
|
@@ -33,31 +23,43 @@ class Pipelines:
|
|
|
33
23
|
terraform_manifest_provider: TerraformManifestProvider,
|
|
34
24
|
ecr_provider: ECRProvider,
|
|
35
25
|
get_git_remote: Callable[[], str],
|
|
36
|
-
get_codestar_arn: Callable[[str], str],
|
|
37
26
|
io: ClickIOProvider = ClickIOProvider(),
|
|
38
27
|
file_provider: FileProvider = FileProvider(),
|
|
39
|
-
|
|
40
|
-
platform_helper_version_override: str = None,
|
|
28
|
+
platform_helper_versioning: PlatformHelperVersioning = None,
|
|
41
29
|
):
|
|
42
30
|
self.config_provider = config_provider
|
|
43
31
|
self.get_git_remote = get_git_remote
|
|
44
|
-
self.get_codestar_arn = get_codestar_arn
|
|
45
32
|
self.terraform_manifest_provider = terraform_manifest_provider
|
|
46
33
|
self.ecr_provider = ecr_provider
|
|
47
34
|
self.io = io
|
|
48
35
|
self.file_provider = file_provider
|
|
49
|
-
self.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
36
|
+
self.platform_helper_versioning = platform_helper_versioning
|
|
37
|
+
|
|
38
|
+
def _map_environment_pipeline_accounts(self, platform_config) -> list[tuple[str, str]]:
|
|
39
|
+
environment_pipelines_config = platform_config[ENVIRONMENT_PIPELINES_KEY]
|
|
40
|
+
environment_config = platform_config["environments"]
|
|
41
|
+
|
|
42
|
+
account_id_lookup = {
|
|
43
|
+
env["accounts"]["deploy"]["name"]: env["accounts"]["deploy"]["id"]
|
|
44
|
+
for env in environment_config.values()
|
|
45
|
+
if env is not None and "accounts" in env and "deploy" in env["accounts"]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
accounts = set()
|
|
49
|
+
|
|
50
|
+
for config in environment_pipelines_config.values():
|
|
51
|
+
account = config.get("account")
|
|
52
|
+
deploy_account_id = account_id_lookup.get(account)
|
|
53
|
+
accounts.add((account, deploy_account_id))
|
|
54
|
+
|
|
55
|
+
return list(accounts)
|
|
56
56
|
|
|
57
57
|
def generate(
|
|
58
58
|
self,
|
|
59
59
|
deploy_branch: str,
|
|
60
60
|
):
|
|
61
|
+
self.platform_helper_versioning.check_platform_helper_version_mismatch()
|
|
62
|
+
|
|
61
63
|
platform_config = self.config_provider.load_and_validate_platform_config()
|
|
62
64
|
|
|
63
65
|
has_codebase_pipelines = CODEBASE_PIPELINES_KEY in platform_config
|
|
@@ -67,28 +69,15 @@ class Pipelines:
|
|
|
67
69
|
self.io.warn("No pipelines defined: nothing to do.")
|
|
68
70
|
return
|
|
69
71
|
|
|
70
|
-
app_name = get_application_name()
|
|
71
|
-
|
|
72
72
|
git_repo = self.get_git_remote()
|
|
73
73
|
if not git_repo:
|
|
74
74
|
self.io.abort_with_error("The current directory is not a git repository")
|
|
75
75
|
|
|
76
|
-
codestar_connection_arn = self.get_codestar_arn(app_name)
|
|
77
|
-
if codestar_connection_arn is None:
|
|
78
|
-
self.io.abort_with_error(f'There is no CodeStar Connection named "{app_name}" to use')
|
|
79
|
-
|
|
80
76
|
base_path = Path(".")
|
|
81
77
|
copilot_pipelines_dir = base_path / f"copilot/pipelines"
|
|
82
78
|
|
|
83
79
|
self._clean_pipeline_config(copilot_pipelines_dir)
|
|
84
80
|
|
|
85
|
-
platform_helper_version_for_template: str = platform_config.get("default_versions", {}).get(
|
|
86
|
-
"platform-helper"
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
if self.platform_helper_version_override:
|
|
90
|
-
platform_helper_version_for_template = self.platform_helper_version_override
|
|
91
|
-
|
|
92
81
|
# TODO: DBTP-1965: - this whole code block/if-statement can fall away once the deploy_repository is a required key.
|
|
93
82
|
deploy_repository = ""
|
|
94
83
|
if "deploy_repository" in platform_config.keys():
|
|
@@ -99,28 +88,18 @@ class Pipelines:
|
|
|
99
88
|
)
|
|
100
89
|
deploy_repository = f"uktrade/{platform_config['application']}-deploy"
|
|
101
90
|
|
|
102
|
-
env_pipeline_module_source = (
|
|
103
|
-
self.environment_variable_provider.get(
|
|
104
|
-
TERRAFORM_ENVIRONMENT_PIPELINES_MODULE_SOURCE_OVERRIDE_ENV_VAR
|
|
105
|
-
)
|
|
106
|
-
or f"git::git@github.com:uktrade/platform-tools.git//terraform/environment-pipelines?depth=1&ref={platform_helper_version_for_template}"
|
|
107
|
-
)
|
|
108
|
-
|
|
109
91
|
if has_environment_pipelines:
|
|
110
|
-
|
|
111
|
-
accounts = {
|
|
112
|
-
config.get("account")
|
|
113
|
-
for config in environment_pipelines.values()
|
|
114
|
-
if "account" in config
|
|
115
|
-
}
|
|
92
|
+
accounts = self._map_environment_pipeline_accounts(platform_config)
|
|
116
93
|
|
|
117
|
-
for
|
|
94
|
+
for account_name, account_id in accounts:
|
|
118
95
|
self._generate_terraform_environment_pipeline_manifest(
|
|
119
96
|
platform_config["application"],
|
|
120
97
|
deploy_repository,
|
|
121
|
-
|
|
122
|
-
|
|
98
|
+
account_name,
|
|
99
|
+
self.platform_helper_versioning.get_environment_pipeline_modules_source(),
|
|
123
100
|
deploy_branch,
|
|
101
|
+
account_id,
|
|
102
|
+
self.platform_helper_versioning.get_pinned_version(),
|
|
124
103
|
)
|
|
125
104
|
|
|
126
105
|
if has_codebase_pipelines:
|
|
@@ -136,19 +115,12 @@ class Pipelines:
|
|
|
136
115
|
if repo in ecrs_already_provisioned
|
|
137
116
|
}
|
|
138
117
|
|
|
139
|
-
codebase_pipeline_module_source = (
|
|
140
|
-
self.environment_variable_provider.get(
|
|
141
|
-
TERRAFORM_CODEBASE_PIPELINES_MODULE_SOURCE_OVERRIDE_ENV_VAR
|
|
142
|
-
)
|
|
143
|
-
or f"git::git@github.com:uktrade/platform-tools.git//terraform/codebase-pipelines?depth=1&ref={platform_helper_version_for_template}"
|
|
144
|
-
)
|
|
145
|
-
|
|
146
118
|
self.terraform_manifest_provider.generate_codebase_pipeline_config(
|
|
147
119
|
platform_config,
|
|
148
|
-
|
|
120
|
+
self.platform_helper_versioning.get_template_version(),
|
|
149
121
|
ecrs_that_need_importing,
|
|
150
122
|
deploy_repository,
|
|
151
|
-
|
|
123
|
+
self.platform_helper_versioning.get_codebase_pipeline_modules_source(),
|
|
152
124
|
)
|
|
153
125
|
|
|
154
126
|
def _clean_pipeline_config(self, pipelines_dir: Path):
|
|
@@ -163,6 +135,8 @@ class Pipelines:
|
|
|
163
135
|
aws_account: str,
|
|
164
136
|
module_source: str,
|
|
165
137
|
deploy_branch: str,
|
|
138
|
+
aws_account_id: str,
|
|
139
|
+
pinned_version: str,
|
|
166
140
|
):
|
|
167
141
|
env_pipeline_template = setup_templates().get_template("environment-pipelines/main.tf")
|
|
168
142
|
|
|
@@ -175,6 +149,8 @@ class Pipelines:
|
|
|
175
149
|
"deploy_branch": deploy_branch,
|
|
176
150
|
"terraform_version": SUPPORTED_TERRAFORM_VERSION,
|
|
177
151
|
"aws_provider_version": SUPPORTED_AWS_PROVIDER_VERSION,
|
|
152
|
+
"deploy_account_id": aws_account_id,
|
|
153
|
+
"pinned_version": pinned_version,
|
|
178
154
|
}
|
|
179
155
|
)
|
|
180
156
|
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import botocore
|
|
2
|
+
|
|
3
|
+
from dbt_platform_helper.constants import MANAGED_BY_PLATFORM
|
|
4
|
+
from dbt_platform_helper.constants import MANAGED_BY_PLATFORM_TERRAFORM
|
|
5
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
6
|
+
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
7
|
+
from dbt_platform_helper.providers.parameter_store import Parameter
|
|
8
|
+
from dbt_platform_helper.providers.parameter_store import ParameterStore
|
|
9
|
+
from dbt_platform_helper.utils.application import load_application
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Secrets:
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
load_application=load_application,
|
|
17
|
+
io: ClickIOProvider = ClickIOProvider(),
|
|
18
|
+
parameter_store_provider: ParameterStore = ParameterStore,
|
|
19
|
+
):
|
|
20
|
+
self.load_application_fn = load_application
|
|
21
|
+
self.application = None
|
|
22
|
+
self.io = io
|
|
23
|
+
self.parameter_store_provider: ParameterStore = parameter_store_provider
|
|
24
|
+
|
|
25
|
+
def _check_ssm_write_access(self, accounts):
|
|
26
|
+
"""Check access."""
|
|
27
|
+
no_access = []
|
|
28
|
+
for account, session in accounts.items():
|
|
29
|
+
sts = session.client("sts")
|
|
30
|
+
iam = session.client("iam")
|
|
31
|
+
|
|
32
|
+
sts_arn = sts.get_caller_identity()["Arn"]
|
|
33
|
+
role_name = sts_arn.split("/")[1]
|
|
34
|
+
|
|
35
|
+
role_arn = (
|
|
36
|
+
f"arn:aws:iam::{account}:role/aws-reserved/sso.amazonaws.com/eu-west-2/{role_name}"
|
|
37
|
+
)
|
|
38
|
+
response = iam.simulate_principal_policy(
|
|
39
|
+
PolicySourceArn=role_arn,
|
|
40
|
+
ActionNames=[
|
|
41
|
+
"ssm:PutParameter",
|
|
42
|
+
],
|
|
43
|
+
ContextEntries=[
|
|
44
|
+
{
|
|
45
|
+
"ContextKeyName": "aws:RequestedRegion",
|
|
46
|
+
"ContextKeyValues": [
|
|
47
|
+
"eu-west-2",
|
|
48
|
+
],
|
|
49
|
+
"ContextKeyType": "string",
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
)["EvaluationResults"]
|
|
53
|
+
|
|
54
|
+
has_access = [
|
|
55
|
+
account for eval_result in response if eval_result["EvalDecision"] == "allowed"
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
if not has_access:
|
|
59
|
+
no_access.append(account)
|
|
60
|
+
|
|
61
|
+
if no_access:
|
|
62
|
+
account_ids = "', '".join(no_access)
|
|
63
|
+
raise PlatformException(
|
|
64
|
+
f"You do not have AWS Parameter Store write access to the following AWS accounts: '{account_ids}'"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def _check_for_existing_params(self, get_secret_name):
|
|
68
|
+
found_params = []
|
|
69
|
+
for _, environment in self.application.environments.items():
|
|
70
|
+
parameter_store: ParameterStore = self.parameter_store_provider(
|
|
71
|
+
environment.session.client("ssm")
|
|
72
|
+
)
|
|
73
|
+
try:
|
|
74
|
+
param = parameter_store.get_ssm_parameter_by_name(get_secret_name(environment.name))
|
|
75
|
+
if param:
|
|
76
|
+
found_params.append(environment.name)
|
|
77
|
+
except botocore.exceptions.ClientError as error:
|
|
78
|
+
if error.response["Error"]["Code"] == "ParameterNotFound":
|
|
79
|
+
pass
|
|
80
|
+
else:
|
|
81
|
+
raise PlatformException(error)
|
|
82
|
+
|
|
83
|
+
return found_params
|
|
84
|
+
|
|
85
|
+
def create(self, app_name, name, overwrite):
|
|
86
|
+
self.application = (
|
|
87
|
+
self.load_application_fn(app_name) if not self.application else self.application
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
accounts = {}
|
|
91
|
+
for _, environment in self.application.environments.items():
|
|
92
|
+
if environment.account_id not in accounts:
|
|
93
|
+
accounts[environment.account_id] = environment.session
|
|
94
|
+
|
|
95
|
+
self._check_ssm_write_access(accounts)
|
|
96
|
+
|
|
97
|
+
get_secret_name = lambda env: f"/platform/{app_name}/{env}/secrets/{name.upper()}"
|
|
98
|
+
found_params = self._check_for_existing_params(get_secret_name)
|
|
99
|
+
|
|
100
|
+
if overwrite is False and found_params:
|
|
101
|
+
envs = "', '".join(found_params)
|
|
102
|
+
raise PlatformException(
|
|
103
|
+
f"AWS Parameter Store secret '{name.upper()}' already exists for the following environments: '{envs}'. \nUse the --overwrite flag to replacing existing secret values."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
values = {}
|
|
107
|
+
for _, environment in self.application.environments.items():
|
|
108
|
+
value = self.io.input(
|
|
109
|
+
f"Please enter value for secret '{name.upper()}' in environment '{environment.name}'",
|
|
110
|
+
hide_input=True,
|
|
111
|
+
)
|
|
112
|
+
values[environment.name] = value
|
|
113
|
+
|
|
114
|
+
for environment_name, secret_value in values.items():
|
|
115
|
+
|
|
116
|
+
environment = self.application.environments[environment_name]
|
|
117
|
+
parameter_store: ParameterStore = self.parameter_store_provider(
|
|
118
|
+
environment.session.client("ssm")
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
data_dict = dict(
|
|
122
|
+
Name=get_secret_name(environment.name),
|
|
123
|
+
Value=secret_value,
|
|
124
|
+
Overwrite=overwrite,
|
|
125
|
+
Type="SecureString",
|
|
126
|
+
Tags=[
|
|
127
|
+
{"Key": "application", "Value": app_name},
|
|
128
|
+
{"Key": "environment", "Value": environment.name},
|
|
129
|
+
{"Key": "managed-by", "Value": MANAGED_BY_PLATFORM},
|
|
130
|
+
],
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# If in found params we are overwriting
|
|
134
|
+
if overwrite and environment_name in found_params:
|
|
135
|
+
data_dict["Overwrite"] = True
|
|
136
|
+
del data_dict["Tags"]
|
|
137
|
+
self.io.debug(
|
|
138
|
+
f"Creating AWS Parameter Store secret {get_secret_name(environment.name)} ..."
|
|
139
|
+
)
|
|
140
|
+
parameter_store.put_parameter(data_dict)
|
|
141
|
+
self.io.debug(
|
|
142
|
+
f"Successfully created AWS Parameter Store secret {get_secret_name(environment.name)}"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
self.io.info(
|
|
146
|
+
"\nTo check or update your secrets, log into your AWS account via the Console and visit the Parameter Store https://eu-west-2.console.aws.amazon.com/systems-manager/parameters/\n"
|
|
147
|
+
"You can attach secrets into ECS container by adding them to the `secrets` section of your 'service-config.yml' file."
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
self.io.info(
|
|
151
|
+
message=f"```\nsecrets:\n\t{name.upper()}: /platform/${{PLATFORM_APPLICATION_NAME}}/${{PLATFORM_ENVIRONMENT_NAME}}/secrets/{name.upper()}\n```",
|
|
152
|
+
fg="cyan",
|
|
153
|
+
bold=True,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def __has_access(self, env, actions=["ssm:PutParameter"], access_type="write"):
|
|
157
|
+
sts_arn = env.session.client("sts").get_caller_identity()["Arn"]
|
|
158
|
+
role_name = sts_arn.split("/")[1]
|
|
159
|
+
|
|
160
|
+
role_arn = f"arn:aws:iam::{env.account_id}:role/aws-reserved/sso.amazonaws.com/eu-west-2/{role_name}"
|
|
161
|
+
response = env.session.client("iam").simulate_principal_policy(
|
|
162
|
+
PolicySourceArn=role_arn,
|
|
163
|
+
ActionNames=actions,
|
|
164
|
+
ContextEntries=[
|
|
165
|
+
{
|
|
166
|
+
"ContextKeyName": "aws:RequestedRegion",
|
|
167
|
+
"ContextKeyValues": [
|
|
168
|
+
"eu-west-2",
|
|
169
|
+
],
|
|
170
|
+
"ContextKeyType": "string",
|
|
171
|
+
}
|
|
172
|
+
],
|
|
173
|
+
)["EvaluationResults"]
|
|
174
|
+
has_access = [
|
|
175
|
+
env.account_id for eval_result in response if eval_result["EvalDecision"] == "allowed"
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
if not has_access:
|
|
179
|
+
raise PlatformException(
|
|
180
|
+
f"You do not have AWS Parameter Store {access_type} access to the following AWS accounts: '{env.account_id}'"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def copy(self, app_name: str, source: str, target: str):
|
|
184
|
+
"""Copy AWS Parameter Store secrets from one environment into
|
|
185
|
+
another."""
|
|
186
|
+
|
|
187
|
+
self.application = (
|
|
188
|
+
self.load_application_fn(app_name) if not self.application else self.application
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if not self.application.environments.get(target, ""):
|
|
192
|
+
raise PlatformException(
|
|
193
|
+
f"Environment '{target}' not found for application '{app_name}'."
|
|
194
|
+
)
|
|
195
|
+
elif not self.application.environments.get(source, ""):
|
|
196
|
+
raise PlatformException(
|
|
197
|
+
f"Environment '{source}' not found for application '{app_name}'."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
source_env = self.application.environments.get(source)
|
|
201
|
+
target_env = self.application.environments.get(target)
|
|
202
|
+
|
|
203
|
+
self.__has_access(source_env, actions=["ssm:GetParameter"], access_type="read")
|
|
204
|
+
self.__has_access(target_env)
|
|
205
|
+
|
|
206
|
+
prod_account_id = self.application.environments["prod"].account_id
|
|
207
|
+
if (
|
|
208
|
+
self.application.environments[source].account_id == prod_account_id
|
|
209
|
+
and self.application.environments[target].account_id != prod_account_id
|
|
210
|
+
):
|
|
211
|
+
raise PlatformException(
|
|
212
|
+
f"Cannot transfer secrets out from '{source}' in the prod account '{prod_account_id}'"
|
|
213
|
+
f" to '{target}' in '{self.application.environments[target].account_id}'"
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
parameter_store: ParameterStore = self.parameter_store_provider(
|
|
217
|
+
source_env.session.client("ssm"), with_model=True
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
target_parameter_store: ParameterStore = self.parameter_store_provider(
|
|
221
|
+
target_env.session.client("ssm"), with_model=True
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
copilot_secrets: list[Parameter] = parameter_store.get_ssm_parameters_by_path(
|
|
225
|
+
f"/copilot/{app_name}/{source}/secrets", add_tags=True
|
|
226
|
+
)
|
|
227
|
+
platform_secrets: list[Parameter] = parameter_store.get_ssm_parameters_by_path(
|
|
228
|
+
f"/platform/{app_name}/{source}/secrets", add_tags=True
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
secrets = copilot_secrets + platform_secrets
|
|
232
|
+
|
|
233
|
+
for secret in secrets:
|
|
234
|
+
secret.name = secret.name.replace(f"/{source}/", f"/{target}/")
|
|
235
|
+
|
|
236
|
+
if (
|
|
237
|
+
"AWS_" in secret.name
|
|
238
|
+
or secret.tags.get("managed-by", "") == MANAGED_BY_PLATFORM_TERRAFORM
|
|
239
|
+
or secret.tags.get("managed-by", "")
|
|
240
|
+
== "Terraform" # SSM params POSTGRES_APPLICATION_USER and POSTGRES_READ_ONLY_USER are tagged differently from the rest
|
|
241
|
+
):
|
|
242
|
+
message = f"Skipping AWS Parameter Store secret {secret.name}"
|
|
243
|
+
if secret.tags.get("managed-by", ""):
|
|
244
|
+
managed_by = secret.tags["managed-by"]
|
|
245
|
+
message += f" with managed-by: {managed_by}"
|
|
246
|
+
self.io.debug(message)
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
secret.tags["application"] = app_name
|
|
250
|
+
secret.tags["environment"] = target
|
|
251
|
+
secret.tags["managed-by"] = MANAGED_BY_PLATFORM
|
|
252
|
+
secret.tags["copied-from"] = source
|
|
253
|
+
|
|
254
|
+
if secret.name.startswith("/copilot/"):
|
|
255
|
+
secret.tags["copilot-environment"] = target
|
|
256
|
+
|
|
257
|
+
data_dict = dict(
|
|
258
|
+
Name=secret.name,
|
|
259
|
+
Value=secret.value,
|
|
260
|
+
Overwrite=False,
|
|
261
|
+
Type=secret.type,
|
|
262
|
+
Description=f"Copied from {source} environment.",
|
|
263
|
+
Tags=secret.tags_to_list(),
|
|
264
|
+
)
|
|
265
|
+
self.io.debug(f"Creating AWS Parameter Store secret {secret.name} ...")
|
|
266
|
+
|
|
267
|
+
try:
|
|
268
|
+
target_parameter_store.put_parameter(data_dict)
|
|
269
|
+
secret_name = secret.name.split("/")[-1]
|
|
270
|
+
self.io.info(
|
|
271
|
+
f"Secret {secret_name} was successfully copied from the '{source} environment to '{target}'"
|
|
272
|
+
)
|
|
273
|
+
except botocore.exceptions.ClientError as e:
|
|
274
|
+
if e.response["Error"]["Code"] == "ParameterAlreadyExists":
|
|
275
|
+
self.io.warn(
|
|
276
|
+
f"""The "{secret.name.split("/")[-1]}" parameter already exists for the "{target}" environment.""",
|
|
277
|
+
)
|
|
278
|
+
else:
|
|
279
|
+
raise PlatformException(e)
|