dbt-platform-helper 13.1.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 +107 -27
- dbt_platform_helper/commands/application.py +5 -6
- dbt_platform_helper/commands/codebase.py +31 -10
- dbt_platform_helper/commands/conduit.py +3 -5
- dbt_platform_helper/commands/config.py +20 -311
- dbt_platform_helper/commands/copilot.py +18 -391
- dbt_platform_helper/commands/database.py +17 -9
- dbt_platform_helper/commands/environment.py +20 -14
- dbt_platform_helper/commands/generate.py +0 -3
- dbt_platform_helper/commands/internal.py +140 -0
- dbt_platform_helper/commands/notify.py +58 -78
- dbt_platform_helper/commands/pipeline.py +23 -19
- dbt_platform_helper/commands/secrets.py +39 -93
- dbt_platform_helper/commands/version.py +7 -12
- dbt_platform_helper/constants.py +52 -7
- dbt_platform_helper/domain/codebase.py +89 -39
- dbt_platform_helper/domain/conduit.py +335 -76
- dbt_platform_helper/domain/config.py +381 -0
- dbt_platform_helper/domain/copilot.py +398 -0
- dbt_platform_helper/domain/copilot_environment.py +8 -8
- dbt_platform_helper/domain/database_copy.py +2 -2
- dbt_platform_helper/domain/maintenance_page.py +254 -430
- dbt_platform_helper/domain/notify.py +64 -0
- dbt_platform_helper/domain/pipelines.py +43 -35
- dbt_platform_helper/domain/plans.py +41 -0
- dbt_platform_helper/domain/secrets.py +279 -0
- dbt_platform_helper/domain/service.py +570 -0
- dbt_platform_helper/domain/terraform_environment.py +14 -13
- dbt_platform_helper/domain/update_alb_rules.py +412 -0
- dbt_platform_helper/domain/versioning.py +249 -0
- dbt_platform_helper/{providers → entities}/platform_config_schema.py +75 -82
- dbt_platform_helper/entities/semantic_version.py +83 -0
- dbt_platform_helper/entities/service.py +339 -0
- dbt_platform_helper/platform_exception.py +4 -0
- dbt_platform_helper/providers/autoscaling.py +24 -0
- dbt_platform_helper/providers/aws/__init__.py +0 -0
- dbt_platform_helper/providers/aws/exceptions.py +70 -0
- dbt_platform_helper/providers/aws/interfaces.py +13 -0
- dbt_platform_helper/providers/aws/opensearch.py +23 -0
- dbt_platform_helper/providers/aws/redis.py +21 -0
- dbt_platform_helper/providers/aws/sso_auth.py +75 -0
- dbt_platform_helper/providers/cache.py +40 -4
- dbt_platform_helper/providers/cloudformation.py +1 -1
- dbt_platform_helper/providers/config.py +137 -19
- dbt_platform_helper/providers/config_validator.py +112 -51
- dbt_platform_helper/providers/copilot.py +24 -16
- dbt_platform_helper/providers/ecr.py +89 -7
- dbt_platform_helper/providers/ecs.py +228 -36
- dbt_platform_helper/providers/environment_variable.py +24 -0
- dbt_platform_helper/providers/files.py +1 -1
- dbt_platform_helper/providers/io.py +36 -4
- dbt_platform_helper/providers/kms.py +22 -0
- dbt_platform_helper/providers/load_balancers.py +402 -42
- dbt_platform_helper/providers/logs.py +72 -0
- dbt_platform_helper/providers/parameter_store.py +134 -0
- dbt_platform_helper/providers/s3.py +21 -0
- dbt_platform_helper/providers/schema_migrations/__init__.py +0 -0
- dbt_platform_helper/providers/schema_migrations/schema_v0_to_v1_migration.py +43 -0
- dbt_platform_helper/providers/schema_migrator.py +77 -0
- dbt_platform_helper/providers/secrets.py +5 -5
- dbt_platform_helper/providers/slack_channel_notifier.py +62 -0
- dbt_platform_helper/providers/terraform_manifest.py +121 -19
- dbt_platform_helper/providers/version.py +106 -23
- dbt_platform_helper/providers/version_status.py +27 -0
- dbt_platform_helper/providers/vpc.py +36 -5
- dbt_platform_helper/providers/yaml_file.py +58 -2
- dbt_platform_helper/templates/environment-pipelines/main.tf +4 -3
- dbt_platform_helper/templates/svc/overrides/cfn.patches.yml +5 -0
- dbt_platform_helper/utilities/decorators.py +103 -0
- dbt_platform_helper/utils/application.py +119 -22
- dbt_platform_helper/utils/aws.py +39 -150
- dbt_platform_helper/utils/deep_merge.py +10 -0
- dbt_platform_helper/utils/git.py +1 -14
- dbt_platform_helper/utils/validation.py +1 -1
- {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/METADATA +11 -20
- dbt_platform_helper-15.16.0.dist-info/RECORD +118 -0
- {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/WHEEL +1 -1
- platform_helper.py +3 -1
- terraform/elasticache-redis/plans.yml +85 -0
- terraform/opensearch/plans.yml +71 -0
- terraform/postgres/plans.yml +128 -0
- dbt_platform_helper/addon-plans.yml +0 -224
- dbt_platform_helper/providers/aws.py +0 -37
- dbt_platform_helper/providers/opensearch.py +0 -36
- dbt_platform_helper/providers/redis.py +0 -34
- dbt_platform_helper/providers/semantic_version.py +0 -126
- dbt_platform_helper/templates/svc/manifest-backend.yml +0 -69
- dbt_platform_helper/templates/svc/manifest-public.yml +0 -109
- dbt_platform_helper/utils/cloudfoundry.py +0 -14
- dbt_platform_helper/utils/files.py +0 -53
- dbt_platform_helper/utils/manifests.py +0 -18
- dbt_platform_helper/utils/versioning.py +0 -238
- dbt_platform_helper-13.1.0.dist-info/RECORD +0 -96
- {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/entry_points.txt +0 -0
- {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -77,12 +77,43 @@ class VpcProvider:
|
|
|
77
77
|
return vpc_id
|
|
78
78
|
|
|
79
79
|
def _get_security_groups(self, app: str, env: str, vpc_id: str) -> list:
|
|
80
|
-
|
|
81
80
|
vpc_filter = {"Name": "vpc-id", "Values": [vpc_id]}
|
|
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
|
|
@@ -29,6 +30,7 @@ class DuplicateKeysException(YamlFileProviderException):
|
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
class YamlFileProvider:
|
|
33
|
+
@staticmethod
|
|
32
34
|
def load(path: str) -> dict:
|
|
33
35
|
"""
|
|
34
36
|
Raises:
|
|
@@ -45,15 +47,25 @@ class YamlFileProvider:
|
|
|
45
47
|
|
|
46
48
|
if not yaml_content:
|
|
47
49
|
return {}
|
|
48
|
-
|
|
49
50
|
YamlFileProvider.lint_yaml_for_duplicate_keys(path)
|
|
50
51
|
|
|
51
52
|
return yaml_content
|
|
52
53
|
|
|
54
|
+
@staticmethod
|
|
53
55
|
def write(path: str, contents: dict, comment: str = ""):
|
|
54
56
|
with open(path, "w") as file:
|
|
55
57
|
file.write(comment)
|
|
56
|
-
yaml.
|
|
58
|
+
yaml.add_representer(str, account_number_representer)
|
|
59
|
+
yaml.add_representer(type(None), null_value_representer)
|
|
60
|
+
|
|
61
|
+
yaml.dump(
|
|
62
|
+
contents,
|
|
63
|
+
file,
|
|
64
|
+
canonical=False,
|
|
65
|
+
sort_keys=False,
|
|
66
|
+
default_style=None,
|
|
67
|
+
default_flow_style=False,
|
|
68
|
+
)
|
|
57
69
|
|
|
58
70
|
@staticmethod
|
|
59
71
|
def lint_yaml_for_duplicate_keys(path):
|
|
@@ -72,3 +84,47 @@ class YamlFileProvider:
|
|
|
72
84
|
]
|
|
73
85
|
if duplicate_keys:
|
|
74
86
|
raise DuplicateKeysException(",".join(duplicate_keys))
|
|
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
|
+
|
|
122
|
+
|
|
123
|
+
def account_number_representer(dumper, data):
|
|
124
|
+
if data.isdigit():
|
|
125
|
+
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="'")
|
|
126
|
+
return dumper.represent_scalar("tag:yaml.org,2002:str", data, style=None)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def null_value_representer(dumper, data):
|
|
130
|
+
return dumper.represent_scalar("tag:yaml.org,2002:null", "")
|
|
@@ -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" {
|
|
@@ -34,7 +34,7 @@ terraform {
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
module "environment-pipelines" {
|
|
37
|
-
source = "
|
|
37
|
+
source = "{{ module_source }}"
|
|
38
38
|
|
|
39
39
|
for_each = local.pipelines
|
|
40
40
|
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import time
|
|
3
|
+
from typing import Callable
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
7
|
+
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
8
|
+
|
|
9
|
+
SECONDS_BEFORE_RETRY = 3
|
|
10
|
+
RETRY_MAX_ATTEMPTS = 3
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RetryException(PlatformException):
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self, function_name: str, max_attempts: int, original_exception: Optional[Exception] = None
|
|
17
|
+
):
|
|
18
|
+
message = f"Function: {function_name} failed after {max_attempts} attempts"
|
|
19
|
+
self.original_exception = original_exception
|
|
20
|
+
if original_exception:
|
|
21
|
+
message += f": \n{str(original_exception)}"
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def retry(
|
|
26
|
+
exceptions_to_catch: tuple = (Exception,),
|
|
27
|
+
max_attempts: int = RETRY_MAX_ATTEMPTS,
|
|
28
|
+
delay: float = SECONDS_BEFORE_RETRY,
|
|
29
|
+
raise_custom_exception: bool = True,
|
|
30
|
+
custom_exception: type = RetryException,
|
|
31
|
+
io: ClickIOProvider = ClickIOProvider(),
|
|
32
|
+
):
|
|
33
|
+
def decorator(func):
|
|
34
|
+
func.__wrapped_by__ = "retry"
|
|
35
|
+
|
|
36
|
+
@functools.wraps(func)
|
|
37
|
+
def wrapper(*args, **kwargs):
|
|
38
|
+
last_exception = None
|
|
39
|
+
for attempt in range(max_attempts):
|
|
40
|
+
try:
|
|
41
|
+
return func(*args, **kwargs)
|
|
42
|
+
except exceptions_to_catch as e:
|
|
43
|
+
last_exception = e
|
|
44
|
+
io.debug(
|
|
45
|
+
f"Attempt {attempt+1}/{max_attempts} for {func.__name__} failed with exception {str(last_exception)}"
|
|
46
|
+
)
|
|
47
|
+
if attempt < max_attempts - 1:
|
|
48
|
+
time.sleep(delay)
|
|
49
|
+
if raise_custom_exception:
|
|
50
|
+
raise custom_exception(func.__name__, max_attempts, last_exception)
|
|
51
|
+
raise last_exception
|
|
52
|
+
|
|
53
|
+
return wrapper
|
|
54
|
+
|
|
55
|
+
return decorator
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def wait_until(
|
|
59
|
+
exceptions_to_catch: tuple = (PlatformException,),
|
|
60
|
+
max_attempts: int = RETRY_MAX_ATTEMPTS,
|
|
61
|
+
delay: float = SECONDS_BEFORE_RETRY,
|
|
62
|
+
raise_custom_exception: bool = True,
|
|
63
|
+
custom_exception=RetryException,
|
|
64
|
+
message_on_false="Condition not met",
|
|
65
|
+
io: ClickIOProvider = ClickIOProvider(),
|
|
66
|
+
):
|
|
67
|
+
"""Wrap a function which returns a boolean."""
|
|
68
|
+
|
|
69
|
+
def decorator(func: Callable[..., bool]):
|
|
70
|
+
func.__wrapped_by__ = "wait_until"
|
|
71
|
+
|
|
72
|
+
@functools.wraps(func)
|
|
73
|
+
def wrapper(*args, **kwargs):
|
|
74
|
+
last_exception = None
|
|
75
|
+
for attempt in range(max_attempts):
|
|
76
|
+
try:
|
|
77
|
+
result = func(*args, **kwargs)
|
|
78
|
+
if result:
|
|
79
|
+
return result
|
|
80
|
+
io.debug(
|
|
81
|
+
f"Attempt {attempt+1}/{max_attempts} for {func.__name__} returned falsy"
|
|
82
|
+
)
|
|
83
|
+
except exceptions_to_catch as e:
|
|
84
|
+
last_exception = e
|
|
85
|
+
io.debug(
|
|
86
|
+
f"Attempt {attempt+1}/{max_attempts} for {func.__name__} failed with exception {str(last_exception)}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if attempt < max_attempts - 1:
|
|
90
|
+
time.sleep(delay)
|
|
91
|
+
|
|
92
|
+
if not last_exception: # If func returns false set last_exception
|
|
93
|
+
last_exception = PlatformException(message_on_false)
|
|
94
|
+
if (
|
|
95
|
+
not raise_custom_exception
|
|
96
|
+
): # Raise last_exception when you don't want custom exception
|
|
97
|
+
raise last_exception
|
|
98
|
+
else:
|
|
99
|
+
raise custom_exception(func.__name__, max_attempts, last_exception)
|
|
100
|
+
|
|
101
|
+
return wrapper
|
|
102
|
+
|
|
103
|
+
return decorator
|
|
@@ -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,17 +195,17 @@ 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):
|
|
127
207
|
if Path(PLATFORM_CONFIG_FILE).exists():
|
|
128
|
-
config = ConfigProvider()
|
|
208
|
+
config = ConfigProvider(installed_version_provider="N/A")
|
|
129
209
|
try:
|
|
130
210
|
app_config = config.load_unvalidated_config_file()
|
|
131
211
|
return app_config["application"]
|
|
@@ -142,7 +222,24 @@ 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):
|
|
226
|
+
super().__init__(
|
|
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}"""
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class ApplicationServiceNotFoundException(ApplicationException):
|
|
235
|
+
def __init__(self, application_name: str, svc_name: str):
|
|
236
|
+
super().__init__(
|
|
237
|
+
f"""The service {svc_name} was not found in the application {application_name}. It either does not exist, or has not been deployed."""
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class ApplicationEnvironmentNotFoundException(ApplicationException):
|
|
242
|
+
def __init__(self, application_name: str, environment: str):
|
|
146
243
|
super().__init__(
|
|
147
|
-
f"""The
|
|
244
|
+
f"""The environment "{environment}" either does not exist or has not been deployed for the application {application_name}."""
|
|
148
245
|
)
|