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
@@ -0,0 +1,21 @@
1
+ import boto3
2
+ from botocore.exceptions import ClientError
3
+
4
+ from dbt_platform_helper.platform_exception import PlatformException
5
+
6
+
7
+ class S3Provider:
8
+
9
+ def __init__(self, client: boto3.client):
10
+ self.client = client
11
+
12
+ def get_object(self, bucket_name: str, object_key: str) -> str:
13
+ """Returns an object from an S3 bucket."""
14
+
15
+ try:
16
+ content = self.client.get_object(Bucket=bucket_name, Key=object_key)
17
+ return content["Body"].read().decode("utf-8")
18
+ except ClientError as e:
19
+ raise PlatformException(
20
+ f"Failed to get '{object_key}' from '{bucket_name}'. Error: {e}"
21
+ )
@@ -3,6 +3,7 @@ from datetime import datetime
3
3
  from importlib.metadata import version
4
4
  from pathlib import Path
5
5
 
6
+ from dbt_platform_helper.constants import EXTENSIONS_MODULE_PATH
6
7
  from dbt_platform_helper.constants import SUPPORTED_AWS_PROVIDER_VERSION
7
8
  from dbt_platform_helper.constants import SUPPORTED_TERRAFORM_VERSION
8
9
  from dbt_platform_helper.providers.config import ConfigProvider
@@ -17,6 +18,74 @@ class TerraformManifestProvider:
17
18
  self.file_provider = file_provider
18
19
  self.io = io
19
20
 
21
+ def generate_service_config(
22
+ self,
23
+ config_object,
24
+ environment,
25
+ platform_helper_version: str,
26
+ platform_config,
27
+ module_source_override: str = None,
28
+ ):
29
+
30
+ service_dir = f"terraform/services/{environment}/{config_object.name}"
31
+ platform_config = ConfigProvider.apply_environment_defaults(platform_config)
32
+ account = self._get_account_for_env(environment, platform_config)
33
+ deploy_to_account_id = self._get_account_id_for_account(account, platform_config)
34
+ application_name = platform_config["application"]
35
+
36
+ terraform = {}
37
+ self._add_header(terraform)
38
+
39
+ self._add_service_locals(terraform, environment)
40
+
41
+ self._add_provider(terraform, account, deploy_to_account_id)
42
+ self._add_backend(
43
+ terraform,
44
+ platform_config,
45
+ account,
46
+ f"tfstate/application/{application_name}/services/{environment}/{config_object.name}.tfstate",
47
+ )
48
+
49
+ self._add_service_module(terraform, platform_helper_version, module_source_override)
50
+
51
+ self._write_terraform_json(terraform, service_dir)
52
+
53
+ def _add_service_locals(self, terraform, environment):
54
+ terraform["locals"] = {
55
+ "environment": environment,
56
+ "platform_config": '${yamldecode(file("../../../../platform-config.yml"))}',
57
+ "application": '${local.platform_config["application"]}',
58
+ "environments": '${local.platform_config["environments"]}',
59
+ "env_config": '${{for name, config in local.environments: name => merge(lookup(local.environments, "*", {}), config)}}',
60
+ "service_config": '${yamldecode(file("./service-config.yml"))}',
61
+ "raw_env_config": '${local.platform_config["environments"]}',
62
+ "combined_env_config": '${{for name, config in local.raw_env_config: name => merge(lookup(local.raw_env_config, "*", {}), config)}}',
63
+ "service_deployment_mode": '${lookup(local.combined_env_config[local.environment], "service-deployment-mode", "copilot")}',
64
+ "non_copilot_service_deployment_mode": '${local.service_deployment_mode == "dual-deploy-copilot-traffic" || local.service_deployment_mode == "dual-deploy-platform-traffic" || local.service_deployment_mode == "platform" ? 1 : 0}',
65
+ "custom_iam_policy_path": '${abspath(format("%s/../../../../services/%s/custom-iam-policy/%s.yml", path.module, local.service_config.name, local.environment))}',
66
+ "custom_iam_policy_json": "${fileexists(local.custom_iam_policy_path) ? jsonencode(yamldecode(file(local.custom_iam_policy_path))) : null}",
67
+ }
68
+
69
+ def _add_service_module(
70
+ self, terraform: dict, platform_helper_version: str, module_source_override: str = None
71
+ ):
72
+ source = (
73
+ module_source_override
74
+ or f"git::git@github.com:uktrade/platform-tools.git//terraform/ecs-service?depth=1&ref={platform_helper_version}"
75
+ )
76
+ terraform["module"] = {
77
+ "ecs-service": {
78
+ "source": source,
79
+ "count": "${local.non_copilot_service_deployment_mode}",
80
+ "application": "${local.application}",
81
+ "environment": "${local.environment}",
82
+ "service_config": "${local.service_config}",
83
+ "env_config": "${local.env_config}",
84
+ "platform_extensions": '${local.platform_config["extensions"]}',
85
+ "custom_iam_policy_json": "${local.custom_iam_policy_json}",
86
+ }
87
+ }
88
+
20
89
  def generate_codebase_pipeline_config(
21
90
  self,
22
91
  platform_config: dict,
@@ -26,13 +95,19 @@ class TerraformManifestProvider:
26
95
  module_source: str,
27
96
  ):
28
97
  default_account = self._get_account_for_env("*", platform_config)
98
+ deploy_to_account_id = self._get_account_id_for_account(default_account, platform_config)
29
99
  state_key_suffix = f"{platform_config['application']}-codebase-pipelines"
30
100
 
31
101
  terraform = {}
32
102
  self._add_header(terraform)
33
103
  self._add_codebase_pipeline_locals(terraform)
34
- self._add_provider(terraform, default_account)
35
- self._add_backend(terraform, platform_config, default_account, state_key_suffix)
104
+ self._add_provider(terraform, default_account, deploy_to_account_id)
105
+ self._add_backend(
106
+ terraform,
107
+ platform_config,
108
+ default_account,
109
+ f"tfstate/application/{state_key_suffix}.tfstate",
110
+ )
36
111
  self._add_codebase_pipeline_module(
37
112
  terraform, platform_helper_version, deploy_repository, module_source
38
113
  )
@@ -56,7 +131,9 @@ class TerraformManifestProvider:
56
131
  terraform = {}
57
132
  self._add_header(terraform)
58
133
  self._add_environment_locals(terraform, application_name)
59
- self._add_backend(terraform, platform_config, account, state_key_suffix)
134
+ self._add_backend(
135
+ terraform, platform_config, account, f"tfstate/application/{state_key_suffix}.tfstate"
136
+ )
60
137
  self._add_extensions_module(terraform, platform_helper_version, env, module_source_override)
61
138
  self._add_moved(terraform, platform_config)
62
139
  self._ensure_no_hcl_manifest_file(env_dir)
@@ -73,6 +150,16 @@ class TerraformManifestProvider:
73
150
  )
74
151
  return account
75
152
 
153
+ @staticmethod
154
+ def _get_account_id_for_account(account_name, platform_config):
155
+ environment_config = platform_config["environments"]
156
+ account_id_lookup = {
157
+ env["accounts"]["deploy"]["name"]: env["accounts"]["deploy"]["id"]
158
+ for env in environment_config.values()
159
+ if env is not None and "accounts" in env and "deploy" in env["accounts"]
160
+ }
161
+ return account_id_lookup.get(account_name)
162
+
76
163
  @staticmethod
77
164
  def _add_header(terraform: dict):
78
165
  time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
@@ -91,21 +178,20 @@ class TerraformManifestProvider:
91
178
  }
92
179
 
93
180
  @staticmethod
94
- def _add_provider(terraform: dict, default_account: str):
181
+ def _add_provider(terraform: dict, deploy_to_account: str, deploy_to_account_id: str):
95
182
  terraform["provider"] = {"aws": {}}
96
183
  terraform["provider"]["aws"]["region"] = "eu-west-2"
97
- terraform["provider"]["aws"]["profile"] = default_account
98
- terraform["provider"]["aws"]["alias"] = default_account
99
- terraform["provider"]["aws"]["shared_credentials_files"] = ["~/.aws/config"]
184
+ terraform["provider"]["aws"]["profile"] = deploy_to_account
185
+ terraform["provider"]["aws"]["allowed_account_ids"] = [deploy_to_account_id]
100
186
 
101
187
  @staticmethod
102
- def _add_backend(terraform: dict, platform_config: dict, account: str, state_key_suffix: str):
188
+ def _add_backend(terraform: dict, platform_config: dict, account: str, state_key: str):
103
189
  terraform["terraform"] = {
104
190
  "required_version": SUPPORTED_TERRAFORM_VERSION,
105
191
  "backend": {
106
192
  "s3": {
107
193
  "bucket": f"terraform-platform-state-{account}",
108
- "key": f"tfstate/application/{state_key_suffix}.tfstate",
194
+ "key": state_key,
109
195
  "region": "eu-west-2",
110
196
  "encrypt": True,
111
197
  "kms_key_id": f"alias/terraform-platform-state-s3-key-{account}",
@@ -135,6 +221,7 @@ class TerraformManifestProvider:
135
221
  "deploy_repository": f"{deploy_repository}",
136
222
  "deploy_repository_branch": '${lookup(each.value, "deploy_repository_branch", "main")}',
137
223
  "additional_ecr_repository": '${lookup(each.value, "additional_ecr_repository", null)}',
224
+ "cache_invalidation": '${lookup(each.value, "cache_invalidation", null)}',
138
225
  "pipelines": '${lookup(each.value, "pipelines", [])}',
139
226
  "services": "${each.value.services}",
140
227
  "requires_image_build": '${lookup(each.value, "requires_image_build", true)}',
@@ -148,10 +235,7 @@ class TerraformManifestProvider:
148
235
  def _add_extensions_module(
149
236
  terraform: dict, platform_helper_version: str, env: str, module_source_override: str = None
150
237
  ):
151
- source = (
152
- module_source_override
153
- or f"git::git@github.com:uktrade/platform-tools.git//terraform/extensions?depth=1&ref={platform_helper_version}"
154
- )
238
+ source = module_source_override or f"{EXTENSIONS_MODULE_PATH}{platform_helper_version}"
155
239
  terraform["module"] = {
156
240
  "extensions": {
157
241
  "source": source,
@@ -78,11 +78,42 @@ class VpcProvider:
78
78
 
79
79
  def _get_security_groups(self, app: str, env: str, vpc_id: str) -> list:
80
80
  vpc_filter = {"Name": "vpc-id", "Values": [vpc_id]}
81
- # TODO Handle terraformed environment SG https://uktrade.atlassian.net/browse/DBTP-2074
82
- tag_filter = {"Name": f"tag:Name", "Values": [f"copilot-{app}-{env}-env"]}
83
- response = self.ec2_client.describe_security_groups(Filters=[vpc_filter, tag_filter])
84
-
85
- return [sg.get("GroupId") for sg in response.get("SecurityGroups")]
81
+ platform_sg_name = f"platform-{app}-{env}-env-sg"
82
+ copilot_sg_name = f"copilot-{app}-{env}-env"
83
+ tag_filter = {"Name": f"tag:Name", "Values": [copilot_sg_name, platform_sg_name]}
84
+
85
+ filtered_security_groups = self.ec2_client.describe_security_groups(
86
+ Filters=[vpc_filter, tag_filter]
87
+ )
88
+
89
+ platform_security_groups = self._get_matching_security_groups(
90
+ filtered_security_groups, platform_sg_name
91
+ )
92
+
93
+ if platform_security_groups:
94
+ print(
95
+ f"using {platform_security_groups}"
96
+ ) # TODO remove this once decopilotiing has been completed
97
+ return platform_security_groups
98
+
99
+ copilot_security_groups = self._get_matching_security_groups(
100
+ filtered_security_groups, copilot_sg_name
101
+ )
102
+
103
+ print(
104
+ f"using {copilot_security_groups}"
105
+ ) # TODO remove this once decopilotiing has been completed
106
+ return copilot_security_groups
107
+
108
+ def _get_matching_security_groups(
109
+ self, filtered_security_groups: list[dict], security_group_name: str
110
+ ):
111
+ matching_sec_groups = filtered_security_groups.get("SecurityGroups")
112
+ return [
113
+ sg.get("GroupId")
114
+ for sg in matching_sec_groups
115
+ if {"Key": "Name", "Value": security_group_name} in sg.get("Tags", [])
116
+ ]
86
117
 
87
118
  def get_vpc(self, app: str, env: str, vpc_name: str) -> Vpc:
88
119
 
@@ -1,3 +1,4 @@
1
+ from collections import OrderedDict
1
2
  from pathlib import Path
2
3
 
3
4
  import yaml
@@ -84,6 +85,40 @@ class YamlFileProvider:
84
85
  if duplicate_keys:
85
86
  raise DuplicateKeysException(",".join(duplicate_keys))
86
87
 
88
+ @staticmethod
89
+ def remove_empty_keys(config: (dict, OrderedDict)) -> (dict, OrderedDict):
90
+ cleaned = config.__class__()
91
+
92
+ for k, v in config.items():
93
+ if isinstance(v, (dict, OrderedDict)):
94
+ v = YamlFileProvider.remove_empty_keys(v)
95
+ if v not in (None, [], {}, ()):
96
+ cleaned[k] = v
97
+
98
+ return cleaned
99
+
100
+ @staticmethod
101
+ def find_and_replace(config, strings: list, replacements: list):
102
+ if len(strings) != len(replacements):
103
+ raise ValueError("'strings' and 'replacements' must be the same length.")
104
+ if not isinstance(strings, list) or not isinstance(replacements, list):
105
+ raise ValueError("'strings' and 'replacements' must both be lists.")
106
+ if isinstance(config, (dict, OrderedDict)):
107
+ return {
108
+ k: YamlFileProvider.find_and_replace(v, strings, replacements)
109
+ for k, v in config.items()
110
+ }
111
+ elif isinstance(config, list):
112
+ return [
113
+ YamlFileProvider.find_and_replace(item, strings, replacements) for item in config
114
+ ]
115
+ elif isinstance(config, str):
116
+ for s, r in zip(strings, replacements):
117
+ config = config.replace(s, r)
118
+ return config
119
+ else:
120
+ return replacements if config == strings else config
121
+
87
122
 
88
123
  def account_number_representer(dumper, data):
89
124
  if data.isdigit():
@@ -10,10 +10,10 @@ locals {
10
10
  provider "aws" {
11
11
  region = "eu-west-2"
12
12
  profile = "{{ aws_account }}"
13
- alias = "{{ aws_account }}"
14
- shared_credentials_files = ["~/.aws/config"]
13
+ allowed_account_ids = ["{{ deploy_account_id }}"]
15
14
  }
16
15
 
16
+
17
17
  terraform {
18
18
  required_version = "{{ terraform_version }}"
19
19
  backend "s3" {
@@ -49,4 +49,5 @@ module "environment-pipelines" {
49
49
  slack_channel = each.value.slack_channel
50
50
  trigger_on_push = each.value.trigger_on_push
51
51
  pipeline_to_trigger = lookup(each.value, "pipeline_to_trigger", null)
52
+ pinned_version = {% if pinned_version %}"{{ pinned_version }}"{% else %}null{% endif %}
52
53
  }
@@ -24,3 +24,8 @@
24
24
  Condition:
25
25
  StringEquals:
26
26
  'ssm:ResourceTag/copilot-application': '__all__'
27
+
28
+ - op: add
29
+ path: /Resources/TaskDefinition/Properties/pidMode
30
+ value:
31
+ task
@@ -59,23 +59,11 @@ class Application:
59
59
  return str(self) == str(other)
60
60
 
61
61
 
62
- def load_application(app=None, default_session=None) -> Application:
62
+ def load_application(app=None, default_session=None, env=None) -> Application:
63
63
  application = Application(app if app else get_application_name())
64
64
  current_session = default_session if default_session else get_aws_session_or_abort()
65
65
 
66
66
  ssm_client = current_session.client("ssm")
67
-
68
- try:
69
- ssm_client.get_parameter(
70
- Name=f"/copilot/applications/{application.name}",
71
- WithDecryption=False,
72
- )
73
- except ssm_client.exceptions.ParameterNotFound:
74
- raise ApplicationNotFoundException(application.name)
75
-
76
- path = f"/copilot/applications/{application.name}/environments"
77
- secrets = get_ssm_secrets(app, None, current_session, path)
78
-
79
67
  sts_client = current_session.client("sts")
80
68
  account_id = sts_client.get_caller_identity()["Account"]
81
69
  sessions = {account_id: current_session}
@@ -86,20 +74,112 @@ def load_application(app=None, default_session=None) -> Application:
86
74
  nesting.
87
75
 
88
76
  e.g.
77
+ - /platform/applications/test/environments/my_env will match.
89
78
  - /copilot/applications/test/environments/my_env will match.
90
79
  - /copilot/applications/test/environments/my_env/addons will not match.
91
80
  """
92
- environment_key_regex = r"^/copilot/applications/{}/environments/[^/]*$".format(
81
+ environment_key_regex = r"^/(copilot|platform)/applications/{}/environments/[^/]*$".format(
93
82
  application.name
94
83
  )
95
84
  return bool(re.match(environment_key_regex, name))
96
85
 
97
- environments = {
86
+ environments_data = []
87
+
88
+ # Try to load all /platform SSM parameters that are present
89
+ env_params = get_ssm_secrets(
90
+ app=app,
91
+ env=None,
92
+ session=current_session,
93
+ path=f"/platform/applications/{application.name}/environments",
94
+ )
95
+
96
+ if env_params:
97
+ for name, value in env_params:
98
+ try:
99
+ param_data = json.loads(value)
100
+ except json.JSONDecodeError:
101
+ continue
102
+
103
+ # Each /platform SSM parameter contains data about all the environments of an application
104
+ if "allEnvironments" in param_data:
105
+ environments_data = param_data["allEnvironments"]
106
+ break # Only need one
107
+ else:
108
+ try:
109
+ # Check that the Copilot application exists
110
+ ssm_client.get_parameter(
111
+ Name=f"/copilot/applications/{application.name}",
112
+ WithDecryption=False,
113
+ )
114
+
115
+ # Legacy /copilot SSM parameters for each environment
116
+ env_params = get_ssm_secrets(
117
+ app, None, current_session, f"/copilot/applications/{application.name}/environments"
118
+ )
119
+
120
+ for name, value in env_params:
121
+ try:
122
+ param_data = json.loads(value)
123
+ except json.JSONDecodeError:
124
+ continue
125
+
126
+ if is_environment_key(name):
127
+ environments_data.append(param_data)
128
+
129
+ except ssm_client.exceptions.ParameterNotFound:
130
+ raise ApplicationNotFoundException(
131
+ application_name=application.name, environment_name=env
132
+ )
133
+
134
+ application.environments = {
98
135
  env["name"]: Environment(env["name"], env["accountID"], sessions)
99
- for env in [json.loads(s[1]) for s in secrets if is_environment_key(s[0])]
136
+ for env in environments_data
100
137
  }
101
- application.environments = environments
102
138
 
139
+ application.services = _load_services(ssm_client, application)
140
+
141
+ return application
142
+
143
+
144
+ def _load_services(ssm_client, application: Application) -> Dict[str, Service]:
145
+ """
146
+ Try to load
147
+ /platform/applications/{app}/environments/{env}/services/{service}
148
+ parameters if present.
149
+
150
+ Otherwise, fall back to legacy /copilot/applications/{app}/components
151
+ parameters.
152
+ """
153
+ services: Dict[str, Service] = {}
154
+
155
+ # Try /platform SSM parameter
156
+ for env_name in application.environments.keys():
157
+ params = dict(
158
+ Path=f"/platform/applications/{application.name}/environments/{env_name}/services",
159
+ Recursive=False,
160
+ WithDecryption=False,
161
+ )
162
+
163
+ while True:
164
+ response = ssm_client.get_parameters_by_path(**params)
165
+ for ssm_param in response.get("Parameters", []):
166
+ try:
167
+ data = json.loads(ssm_param["Value"])
168
+ name = data["name"]
169
+ kind = data["type"]
170
+ services.setdefault(name, Service(name, kind)) # Avoid duplicates
171
+ except (json.JSONDecodeError, KeyError):
172
+ continue
173
+
174
+ if "NextToken" in response:
175
+ params["NextToken"] = response["NextToken"]
176
+ else:
177
+ break
178
+
179
+ if services:
180
+ return services
181
+
182
+ # Fallback to legacy /copilot SSM parameter
103
183
  response = ssm_client.get_parameters_by_path(
104
184
  Path=f"/copilot/applications/{application.name}/components",
105
185
  Recursive=False,
@@ -115,12 +195,12 @@ def load_application(app=None, default_session=None) -> Application:
115
195
  )
116
196
  results.extend(response["Parameters"])
117
197
 
118
- application.services = {
198
+ legacy_services = {
119
199
  svc["name"]: Service(svc["name"], svc["type"])
120
200
  for svc in [json.loads(parameter["Value"]) for parameter in results]
121
201
  }
122
202
 
123
- return application
203
+ return legacy_services
124
204
 
125
205
 
126
206
  def get_application_name(abort=abort_with_error):
@@ -142,9 +222,12 @@ class ApplicationException(PlatformException):
142
222
 
143
223
 
144
224
  class ApplicationNotFoundException(ApplicationException):
145
- def __init__(self, application_name: str):
225
+ def __init__(self, application_name: str, environment_name: str):
146
226
  super().__init__(
147
- f"""The account "{os.environ.get("AWS_PROFILE")}" does not contain the application "{application_name}"; ensure you have set the environment variable "AWS_PROFILE" correctly."""
227
+ f"""The account "{os.environ.get("AWS_PROFILE")}" does not contain the application "{application_name}".
228
+ Please ensure that the environment variable "AWS_PROFILE" is set correctly. If the issue persists, verify that one of the following AWS SSM parameters exists:
229
+ - /platform/applications/{application_name}/environments/{environment_name}
230
+ - /copilot/applications/{application_name}"""
148
231
  )
149
232
 
150
233
 
@@ -158,7 +158,17 @@ def get_ssm_secrets(app, env, session=None, path=None):
158
158
  secrets = []
159
159
 
160
160
  while True:
161
- response = client.get_parameters_by_path(**params)
161
+ try:
162
+ response = client.get_parameters_by_path(**params)
163
+ except ClientError as e:
164
+ if e.response["Error"]["Code"] == "AccessDeniedException":
165
+ click.secho(
166
+ "Access denied on SSM, due to missing permissions. Please update your environment infrastructure.",
167
+ fg="magenta",
168
+ )
169
+ break
170
+ else:
171
+ raise e
162
172
 
163
173
  for secret in response["Parameters"]:
164
174
  secrets.append((secret["Name"], secret["Value"]))
@@ -202,15 +212,6 @@ def set_ssm_param(
202
212
  client.put_parameter(**parameter_args)
203
213
 
204
214
 
205
- def get_codestar_connection_arn(app_name):
206
- session = get_aws_session_or_abort()
207
- response = session.client("codestar-connections").list_connections()
208
-
209
- for connection in response["Connections"]:
210
- if connection["ConnectionName"] == app_name:
211
- return connection["ConnectionArn"]
212
-
213
-
214
215
  def get_account_details(sts_client=None):
215
216
  if not sts_client:
216
217
  sts_client = get_aws_session_or_abort().client("sts")
@@ -0,0 +1,10 @@
1
+ def deep_merge(base, override):
2
+ result = base.copy()
3
+
4
+ for k, v in override.items():
5
+ if k in result and isinstance(result[k], dict) and isinstance(v, dict):
6
+ result[k] = deep_merge(result[k], v)
7
+ else:
8
+ result[k] = v
9
+
10
+ return result
@@ -13,4 +13,4 @@ def extract_repository_name(repository_url):
13
13
  if not repository_url:
14
14
  return
15
15
 
16
- return re.search(r"([^/:]*/[^/]*)\.git", repository_url).group(1)
16
+ return re.search(r"([^/:]*/[^/]*?)(?:\.git)?$", repository_url).group(1)
@@ -1,40 +1,31 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: dbt-platform-helper
3
- Version: 15.3.0
3
+ Version: 15.16.0
4
4
  Summary: Set of tools to help transfer applications/services from GOV.UK PaaS to DBT PaaS augmenting AWS Copilot.
5
5
  License: MIT
6
+ License-File: LICENSE
6
7
  Author: Department for Business and Trade Platform Team
7
8
  Author-email: sre-team@digital.trade.gov.uk
8
- Requires-Python: >=3.9, !=2.7.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*, !=3.7.*, !=3.8.*
9
+ Requires-Python: >3.9.1,<4.0
9
10
  Classifier: License :: OSI Approved :: MIT License
10
11
  Classifier: Programming Language :: Python :: 3
11
12
  Classifier: Programming Language :: Python :: 3.10
12
13
  Classifier: Programming Language :: Python :: 3.11
13
14
  Classifier: Programming Language :: Python :: 3.12
14
15
  Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
15
17
  Requires-Dist: Jinja2 (==3.1.6)
16
- Requires-Dist: PyYAML (==6.0.1)
17
- Requires-Dist: aiohttp (>=3.11.16,<4.0.0)
18
18
  Requires-Dist: boto3 (>=1.35.2,<2.0.0)
19
- Requires-Dist: boto3-stubs (>=1.26.148,<2.0.0)
20
19
  Requires-Dist: botocore (>=1.34.85,<2.0.0)
21
- Requires-Dist: certifi (>=2023.7.22,<2025.0.0)
22
- Requires-Dist: cfn-flip (==1.3.0)
23
- Requires-Dist: cfn-lint (>=1.4.2,<2.0.0)
24
- Requires-Dist: checkov (>=3.2.405,<4.0.0)
20
+ Requires-Dist: cfn-flip (>=1.3.0,<2.0.0)
25
21
  Requires-Dist: click (>=8.1.3,<9.0.0)
26
- Requires-Dist: cloudfoundry-client (==1.35.2)
27
- Requires-Dist: cryptography (>=44.0.1,<45)
28
- Requires-Dist: jinja2-simple-tags (>=0.5.0,<0.6.0)
29
- Requires-Dist: jsonschema (>=4.17.0,<4.18.0)
30
- Requires-Dist: mypy-boto3-codebuild (>=1.26.0.post1,<2.0.0)
22
+ Requires-Dist: jinja2-simple-tags (>=0.5,<0.7)
31
23
  Requires-Dist: prettytable (>=3.9.0,<4.0.0)
32
24
  Requires-Dist: psycopg2-binary (>=2.9.9,<3.0.0)
25
+ Requires-Dist: pydantic (>=2.11.7,<3.0.0)
33
26
  Requires-Dist: requests (>=2.31.0,<3.0.0)
34
27
  Requires-Dist: schema (==0.7.5)
35
- Requires-Dist: semver (>=3.0.2,<4.0.0)
36
28
  Requires-Dist: slack-sdk (>=3.27.1,<4.0.0)
37
- Requires-Dist: tomlkit (>=0.12.2,<0.13.0)
38
29
  Requires-Dist: yamllint (>=1.35.1,<2.0.0)
39
30
  Description-Content-Type: text/markdown
40
31