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
|
@@ -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(
|
|
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(
|
|
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,
|
|
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"] =
|
|
98
|
-
terraform["provider"]["aws"]["
|
|
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,
|
|
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":
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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}"
|
|
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
|
|
dbt_platform_helper/utils/aws.py
CHANGED
|
@@ -158,7 +158,17 @@ def get_ssm_secrets(app, env, session=None, path=None):
|
|
|
158
158
|
secrets = []
|
|
159
159
|
|
|
160
160
|
while True:
|
|
161
|
-
|
|
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")
|
dbt_platform_helper/utils/git.py
CHANGED
|
@@ -1,40 +1,31 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: dbt-platform-helper
|
|
3
|
-
Version: 15.
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|