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.
Files changed (50) hide show
  1. dbt_platform_helper/COMMANDS.md +36 -11
  2. dbt_platform_helper/commands/application.py +2 -1
  3. dbt_platform_helper/commands/conduit.py +1 -1
  4. dbt_platform_helper/commands/environment.py +12 -1
  5. dbt_platform_helper/commands/generate.py +0 -2
  6. dbt_platform_helper/commands/internal.py +140 -0
  7. dbt_platform_helper/commands/pipeline.py +15 -3
  8. dbt_platform_helper/commands/secrets.py +37 -89
  9. dbt_platform_helper/commands/version.py +3 -2
  10. dbt_platform_helper/constants.py +38 -2
  11. dbt_platform_helper/domain/conduit.py +22 -9
  12. dbt_platform_helper/domain/config.py +30 -1
  13. dbt_platform_helper/domain/database_copy.py +1 -1
  14. dbt_platform_helper/domain/maintenance_page.py +27 -3
  15. dbt_platform_helper/domain/pipelines.py +36 -60
  16. dbt_platform_helper/domain/secrets.py +279 -0
  17. dbt_platform_helper/domain/service.py +570 -0
  18. dbt_platform_helper/domain/terraform_environment.py +7 -29
  19. dbt_platform_helper/domain/update_alb_rules.py +412 -0
  20. dbt_platform_helper/domain/versioning.py +124 -13
  21. dbt_platform_helper/entities/platform_config_schema.py +31 -11
  22. dbt_platform_helper/entities/semantic_version.py +2 -0
  23. dbt_platform_helper/entities/service.py +339 -0
  24. dbt_platform_helper/providers/autoscaling.py +24 -0
  25. dbt_platform_helper/providers/aws/exceptions.py +5 -0
  26. dbt_platform_helper/providers/aws/sso_auth.py +14 -0
  27. dbt_platform_helper/providers/config.py +17 -2
  28. dbt_platform_helper/providers/config_validator.py +87 -2
  29. dbt_platform_helper/providers/ecs.py +131 -11
  30. dbt_platform_helper/providers/environment_variable.py +2 -2
  31. dbt_platform_helper/providers/io.py +9 -2
  32. dbt_platform_helper/providers/load_balancers.py +122 -16
  33. dbt_platform_helper/providers/logs.py +72 -0
  34. dbt_platform_helper/providers/parameter_store.py +97 -10
  35. dbt_platform_helper/providers/s3.py +21 -0
  36. dbt_platform_helper/providers/terraform_manifest.py +97 -13
  37. dbt_platform_helper/providers/vpc.py +36 -5
  38. dbt_platform_helper/providers/yaml_file.py +35 -0
  39. dbt_platform_helper/templates/environment-pipelines/main.tf +3 -2
  40. dbt_platform_helper/templates/svc/overrides/cfn.patches.yml +5 -0
  41. dbt_platform_helper/utils/application.py +104 -21
  42. dbt_platform_helper/utils/aws.py +11 -10
  43. dbt_platform_helper/utils/deep_merge.py +10 -0
  44. dbt_platform_helper/utils/git.py +1 -1
  45. {dbt_platform_helper-15.3.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/METADATA +8 -17
  46. {dbt_platform_helper-15.3.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/RECORD +50 -41
  47. {dbt_platform_helper-15.3.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/WHEEL +1 -1
  48. platform_helper.py +2 -0
  49. {dbt_platform_helper-15.3.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/entry_points.txt +0 -0
  50. {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("sso_role_name = AdministratorAccess\n")
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"No such environment '{env}'. Available environments are: {', '.join(environments.keys())}"
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
- [{"Key": "service", "Value": svc.name}],
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
- [{"Key": "service", "Value": svc.name}],
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
- [{"Key": "service", "Value": svc.name}],
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.constants import (
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
- environment_variable_provider: EnvironmentVariableProvider = None,
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.environment_variable_provider = (
50
- environment_variable_provider or EnvironmentVariableProvider()
51
- )
52
- self.platform_helper_version_override = (
53
- platform_helper_version_override
54
- or self.environment_variable_provider.get(PLATFORM_HELPER_VERSION_OVERRIDE_KEY)
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
- environment_pipelines = platform_config[ENVIRONMENT_PIPELINES_KEY]
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 account in accounts:
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
- account,
122
- env_pipeline_module_source,
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
- platform_helper_version_for_template,
120
+ self.platform_helper_versioning.get_template_version(),
149
121
  ecrs_that_need_importing,
150
122
  deploy_repository,
151
- codebase_pipeline_module_source,
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)