dbt-platform-helper 12.2.4__py3-none-any.whl → 12.4.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 +6 -1
- dbt_platform_helper/commands/codebase.py +9 -80
- dbt_platform_helper/commands/conduit.py +25 -45
- dbt_platform_helper/commands/config.py +4 -4
- dbt_platform_helper/commands/copilot.py +13 -15
- dbt_platform_helper/commands/database.py +17 -4
- dbt_platform_helper/commands/environment.py +3 -2
- dbt_platform_helper/commands/secrets.py +1 -1
- dbt_platform_helper/domain/codebase.py +81 -63
- dbt_platform_helper/domain/conduit.py +42 -93
- dbt_platform_helper/domain/database_copy.py +48 -42
- dbt_platform_helper/domain/maintenance_page.py +8 -8
- dbt_platform_helper/platform_exception.py +5 -0
- dbt_platform_helper/providers/aws.py +32 -0
- dbt_platform_helper/providers/cloudformation.py +129 -100
- dbt_platform_helper/providers/copilot.py +33 -16
- dbt_platform_helper/providers/ecs.py +97 -74
- dbt_platform_helper/providers/load_balancers.py +11 -5
- dbt_platform_helper/providers/secrets.py +100 -59
- dbt_platform_helper/providers/validation.py +19 -0
- dbt_platform_helper/utils/application.py +14 -2
- dbt_platform_helper/utils/arn_parser.py +1 -1
- dbt_platform_helper/utils/aws.py +38 -12
- dbt_platform_helper/utils/git.py +2 -2
- dbt_platform_helper/utils/validation.py +57 -18
- dbt_platform_helper/utils/versioning.py +8 -8
- {dbt_platform_helper-12.2.4.dist-info → dbt_platform_helper-12.4.0.dist-info}/METADATA +1 -1
- {dbt_platform_helper-12.2.4.dist-info → dbt_platform_helper-12.4.0.dist-info}/RECORD +31 -30
- dbt_platform_helper/addons-template-map.yml +0 -29
- dbt_platform_helper/exceptions.py +0 -81
- {dbt_platform_helper-12.2.4.dist-info → dbt_platform_helper-12.4.0.dist-info}/LICENSE +0 -0
- {dbt_platform_helper-12.2.4.dist-info → dbt_platform_helper-12.4.0.dist-info}/WHEEL +0 -0
- {dbt_platform_helper-12.2.4.dist-info → dbt_platform_helper-12.4.0.dist-info}/entry_points.txt +0 -0
|
@@ -4,12 +4,8 @@ import time
|
|
|
4
4
|
from botocore.exceptions import ClientError
|
|
5
5
|
|
|
6
6
|
from dbt_platform_helper.constants import CONDUIT_DOCKER_IMAGE_LOCATION
|
|
7
|
-
from dbt_platform_helper.
|
|
8
|
-
from dbt_platform_helper.providers.
|
|
9
|
-
from dbt_platform_helper.providers.secrets import get_connection_secret_arn
|
|
10
|
-
from dbt_platform_helper.providers.secrets import (
|
|
11
|
-
get_postgres_connection_data_updated_with_master_secret,
|
|
12
|
-
)
|
|
7
|
+
from dbt_platform_helper.providers.aws import CreateTaskTimeoutException
|
|
8
|
+
from dbt_platform_helper.providers.secrets import Secrets
|
|
13
9
|
from dbt_platform_helper.utils.application import Application
|
|
14
10
|
from dbt_platform_helper.utils.messages import abort_with_error
|
|
15
11
|
|
|
@@ -17,7 +13,6 @@ from dbt_platform_helper.utils.messages import abort_with_error
|
|
|
17
13
|
def create_addon_client_task(
|
|
18
14
|
iam_client,
|
|
19
15
|
ssm_client,
|
|
20
|
-
secrets_manager_client,
|
|
21
16
|
subprocess,
|
|
22
17
|
application: Application,
|
|
23
18
|
env: str,
|
|
@@ -36,7 +31,6 @@ def create_addon_client_task(
|
|
|
36
31
|
elif access == "admin":
|
|
37
32
|
create_postgres_admin_task(
|
|
38
33
|
ssm_client,
|
|
39
|
-
secrets_manager_client,
|
|
40
34
|
subprocess,
|
|
41
35
|
application,
|
|
42
36
|
addon_name,
|
|
@@ -59,7 +53,7 @@ def create_addon_client_task(
|
|
|
59
53
|
# We cannot check for botocore.errorfactory.NoSuchEntityException as botocore generates that class on the fly as part of errorfactory.
|
|
60
54
|
# factory. Checking the error code is the recommended way of handling these exceptions.
|
|
61
55
|
if ex.response.get("Error", {}).get("Code", None) != "NoSuchEntity":
|
|
62
|
-
# TODO
|
|
56
|
+
# TODO When we are refactoring this, raise an exception to be caught at the command layer
|
|
63
57
|
abort_with_error(
|
|
64
58
|
f"cannot obtain Role {role_name}: {ex.response.get('Error', {}).get('Message', '')}"
|
|
65
59
|
)
|
|
@@ -69,7 +63,7 @@ def create_addon_client_task(
|
|
|
69
63
|
f"--task-group-name {task_name} "
|
|
70
64
|
f"{execution_role}"
|
|
71
65
|
f"--image {CONDUIT_DOCKER_IMAGE_LOCATION}:{addon_type} "
|
|
72
|
-
f"--secrets CONNECTION_SECRET={
|
|
66
|
+
f"--secrets CONNECTION_SECRET={_get_secrets_provider(application, env).get_connection_secret_arn(secret_name)} "
|
|
73
67
|
"--platform-os linux "
|
|
74
68
|
"--platform-arch arm64",
|
|
75
69
|
shell=True,
|
|
@@ -78,7 +72,6 @@ def create_addon_client_task(
|
|
|
78
72
|
|
|
79
73
|
def create_postgres_admin_task(
|
|
80
74
|
ssm_client,
|
|
81
|
-
secrets_manager_client,
|
|
82
75
|
subprocess,
|
|
83
76
|
app: Application,
|
|
84
77
|
addon_name: str,
|
|
@@ -95,8 +88,8 @@ def create_postgres_admin_task(
|
|
|
95
88
|
"Parameter"
|
|
96
89
|
]["Value"]
|
|
97
90
|
connection_string = json.dumps(
|
|
98
|
-
get_postgres_connection_data_updated_with_master_secret(
|
|
99
|
-
|
|
91
|
+
_get_secrets_provider(app, env).get_postgres_connection_data_updated_with_master_secret(
|
|
92
|
+
read_only_secret_name, master_secret_arn
|
|
100
93
|
)
|
|
101
94
|
)
|
|
102
95
|
|
|
@@ -111,6 +104,19 @@ def create_postgres_admin_task(
|
|
|
111
104
|
)
|
|
112
105
|
|
|
113
106
|
|
|
107
|
+
def _temp_until_refactor_get_ecs_task_arns(ecs_client, cluster_arn: str, task_name: str):
|
|
108
|
+
tasks = ecs_client.list_tasks(
|
|
109
|
+
cluster=cluster_arn,
|
|
110
|
+
desiredStatus="RUNNING",
|
|
111
|
+
family=f"copilot-{task_name}",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if not tasks["taskArns"]:
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
return tasks["taskArns"]
|
|
118
|
+
|
|
119
|
+
|
|
114
120
|
def connect_to_addon_client_task(
|
|
115
121
|
ecs_client,
|
|
116
122
|
subprocess,
|
|
@@ -118,13 +124,14 @@ def connect_to_addon_client_task(
|
|
|
118
124
|
env,
|
|
119
125
|
cluster_arn,
|
|
120
126
|
task_name,
|
|
121
|
-
|
|
127
|
+
get_ecs_task_arns=_temp_until_refactor_get_ecs_task_arns,
|
|
122
128
|
):
|
|
123
129
|
running = False
|
|
124
130
|
tries = 0
|
|
125
131
|
while tries < 15 and not running:
|
|
126
132
|
tries += 1
|
|
127
|
-
|
|
133
|
+
# Todo: Use from ECS provider when we refactor this
|
|
134
|
+
if get_ecs_task_arns(ecs_client, cluster_arn, task_name):
|
|
128
135
|
subprocess.call(
|
|
129
136
|
"copilot task exec "
|
|
130
137
|
f"--app {application_name} --env {env} "
|
|
@@ -137,8 +144,18 @@ def connect_to_addon_client_task(
|
|
|
137
144
|
time.sleep(1)
|
|
138
145
|
|
|
139
146
|
if not running:
|
|
140
|
-
raise
|
|
147
|
+
raise CreateTaskTimeoutException(task_name, application_name, env)
|
|
141
148
|
|
|
142
149
|
|
|
143
150
|
def _normalise_secret_name(addon_name: str) -> str:
|
|
144
151
|
return addon_name.replace("-", "_").upper()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _get_secrets_provider(application: Application, env: str) -> Secrets:
|
|
155
|
+
# Todo: We instantiate the secrets provider here to avoid rabbit holing, but something better probably possible when we are refactoring this area
|
|
156
|
+
return Secrets(
|
|
157
|
+
application.environments[env].session.client("ssm"),
|
|
158
|
+
application.environments[env].session.client("secretsmanager"),
|
|
159
|
+
application.name,
|
|
160
|
+
env,
|
|
161
|
+
)
|
|
@@ -3,77 +3,100 @@ import string
|
|
|
3
3
|
import time
|
|
4
4
|
from typing import List
|
|
5
5
|
|
|
6
|
-
from dbt_platform_helper.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
6
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ECS:
|
|
10
|
+
def __init__(self, ecs_client, ssm_client, application_name: str, env: str):
|
|
11
|
+
self.ecs_client = ecs_client
|
|
12
|
+
self.ssm_client = ssm_client
|
|
13
|
+
self.application_name = application_name
|
|
14
|
+
self.env = env
|
|
15
|
+
|
|
16
|
+
def get_cluster_arn(self) -> str:
|
|
17
|
+
"""Returns the ARN of the ECS cluster for the given application and
|
|
18
|
+
environment."""
|
|
19
|
+
for cluster_arn in self.ecs_client.list_clusters()["clusterArns"]:
|
|
20
|
+
tags_response = self.ecs_client.list_tags_for_resource(resourceArn=cluster_arn)
|
|
21
|
+
tags = tags_response["tags"]
|
|
22
|
+
|
|
23
|
+
app_key_found = False
|
|
24
|
+
env_key_found = False
|
|
25
|
+
cluster_key_found = False
|
|
26
|
+
|
|
27
|
+
for tag in tags:
|
|
28
|
+
if tag["key"] == "copilot-application" and tag["value"] == self.application_name:
|
|
29
|
+
app_key_found = True
|
|
30
|
+
if tag["key"] == "copilot-environment" and tag["value"] == self.env:
|
|
31
|
+
env_key_found = True
|
|
32
|
+
if tag["key"] == "aws:cloudformation:logical-id" and tag["value"] == "Cluster":
|
|
33
|
+
cluster_key_found = True
|
|
34
|
+
|
|
35
|
+
if app_key_found and env_key_found and cluster_key_found:
|
|
36
|
+
return cluster_arn
|
|
37
|
+
|
|
38
|
+
raise NoClusterException(self.application_name, self.env)
|
|
39
|
+
|
|
40
|
+
def get_or_create_task_name(self, addon_name: str, parameter_name: str) -> str:
|
|
41
|
+
"""Fetches the task name from SSM or creates a new one if not found."""
|
|
42
|
+
try:
|
|
43
|
+
return self.ssm_client.get_parameter(Name=parameter_name)["Parameter"]["Value"]
|
|
44
|
+
except self.ssm_client.exceptions.ParameterNotFound:
|
|
45
|
+
random_id = "".join(random.choices(string.ascii_lowercase + string.digits, k=12))
|
|
46
|
+
return f"conduit-{self.application_name}-{self.env}-{addon_name}-{random_id}"
|
|
47
|
+
|
|
48
|
+
def get_ecs_task_arns(self, cluster_arn: str, task_name: str):
|
|
49
|
+
"""Gets the ECS task ARNs for a given task name and cluster ARN."""
|
|
50
|
+
tasks = self.ecs_client.list_tasks(
|
|
51
|
+
cluster=cluster_arn,
|
|
52
|
+
desiredStatus="RUNNING",
|
|
53
|
+
family=f"copilot-{task_name}",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if not tasks["taskArns"]:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
return tasks["taskArns"]
|
|
60
|
+
|
|
61
|
+
def ecs_exec_is_available(self, cluster_arn: str, task_arns: List[str]):
|
|
62
|
+
"""
|
|
63
|
+
Checks if the ExecuteCommandAgent is running on the specified ECS task.
|
|
64
|
+
|
|
65
|
+
Waits for up to 25 attempts, then raises ECSAgentNotRunning if still not
|
|
66
|
+
running.
|
|
67
|
+
"""
|
|
68
|
+
current_attempts = 0
|
|
69
|
+
execute_command_agent_status = ""
|
|
70
|
+
|
|
71
|
+
while execute_command_agent_status != "RUNNING" and current_attempts < 25:
|
|
72
|
+
current_attempts += 1
|
|
73
|
+
|
|
74
|
+
task_details = self.ecs_client.describe_tasks(cluster=cluster_arn, tasks=task_arns)
|
|
75
|
+
|
|
76
|
+
managed_agents = task_details["tasks"][0]["containers"][0]["managedAgents"]
|
|
77
|
+
execute_command_agent_status = [
|
|
78
|
+
agent["lastStatus"]
|
|
79
|
+
for agent in managed_agents
|
|
80
|
+
if agent["name"] == "ExecuteCommandAgent"
|
|
81
|
+
][0]
|
|
82
|
+
if execute_command_agent_status != "RUNNING":
|
|
83
|
+
time.sleep(1)
|
|
84
|
+
|
|
85
|
+
if execute_command_agent_status != "RUNNING":
|
|
86
|
+
raise ECSAgentNotRunningException
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ECSException(PlatformException):
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ECSAgentNotRunningException(ECSException):
|
|
94
|
+
def __init__(self):
|
|
95
|
+
super().__init__("""ECS exec agent never reached "RUNNING" status""")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class NoClusterException(ECSException):
|
|
99
|
+
def __init__(self, application_name: str, environment: str):
|
|
100
|
+
super().__init__(
|
|
101
|
+
f"""No ECS cluster found for "{application_name}" in "{environment}" environment."""
|
|
102
|
+
)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import boto3
|
|
2
2
|
|
|
3
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
def find_load_balancer(session: boto3.Session, app: str, env: str) -> str:
|
|
5
7
|
lb_client = session.client("elbv2")
|
|
@@ -16,7 +18,7 @@ def find_load_balancer(session: boto3.Session, app: str, env: str) -> str:
|
|
|
16
18
|
load_balancer_arn = lb["ResourceArn"]
|
|
17
19
|
|
|
18
20
|
if not load_balancer_arn:
|
|
19
|
-
raise
|
|
21
|
+
raise LoadBalancerNotFoundException()
|
|
20
22
|
|
|
21
23
|
return load_balancer_arn
|
|
22
24
|
|
|
@@ -34,18 +36,22 @@ def find_https_listener(session: boto3.Session, app: str, env: str) -> str:
|
|
|
34
36
|
pass
|
|
35
37
|
|
|
36
38
|
if not listener_arn:
|
|
37
|
-
raise
|
|
39
|
+
raise ListenerNotFoundException()
|
|
38
40
|
|
|
39
41
|
return listener_arn
|
|
40
42
|
|
|
41
43
|
|
|
42
|
-
class
|
|
44
|
+
class LoadBalancerException(PlatformException):
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class LoadBalancerNotFoundException(LoadBalancerException):
|
|
43
49
|
pass
|
|
44
50
|
|
|
45
51
|
|
|
46
|
-
class
|
|
52
|
+
class ListenerNotFoundException(LoadBalancerException):
|
|
47
53
|
pass
|
|
48
54
|
|
|
49
55
|
|
|
50
|
-
class
|
|
56
|
+
class ListenerRuleNotFoundException(LoadBalancerException):
|
|
51
57
|
pass
|
|
@@ -2,84 +2,125 @@ import json
|
|
|
2
2
|
import urllib
|
|
3
3
|
|
|
4
4
|
from dbt_platform_helper.constants import CONDUIT_ADDON_TYPES
|
|
5
|
-
from dbt_platform_helper.
|
|
6
|
-
from dbt_platform_helper.exceptions import AddonTypeMissingFromConfigError
|
|
7
|
-
from dbt_platform_helper.exceptions import InvalidAddonTypeError
|
|
8
|
-
from dbt_platform_helper.exceptions import ParameterNotFoundError
|
|
9
|
-
from dbt_platform_helper.exceptions import SecretNotFoundError
|
|
5
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
10
6
|
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
ssm_client, secrets_manager_client,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
class Secrets:
|
|
9
|
+
def __init__(self, ssm_client, secrets_manager_client, application_name, env):
|
|
10
|
+
self.ssm_client = ssm_client
|
|
11
|
+
self.secrets_manager_client = secrets_manager_client
|
|
12
|
+
self.application_name = application_name
|
|
13
|
+
self.env = env
|
|
17
14
|
|
|
18
|
-
|
|
15
|
+
def get_postgres_connection_data_updated_with_master_secret(self, parameter_name, secret_arn):
|
|
16
|
+
response = self.ssm_client.get_parameter(Name=parameter_name, WithDecryption=True)
|
|
17
|
+
parameter_value = response["Parameter"]["Value"]
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
secret_value = json.loads(secret_response["SecretString"])
|
|
19
|
+
parameter_data = json.loads(parameter_value)
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
secret_response = self.secrets_manager_client.get_secret_value(SecretId=secret_arn)
|
|
22
|
+
secret_value = json.loads(secret_response["SecretString"])
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
parameter_data["username"] = urllib.parse.quote(secret_value["username"])
|
|
25
|
+
parameter_data["password"] = urllib.parse.quote(secret_value["password"])
|
|
27
26
|
|
|
27
|
+
return parameter_data
|
|
28
28
|
|
|
29
|
-
def get_connection_secret_arn(
|
|
29
|
+
def get_connection_secret_arn(self, secret_name: str) -> str:
|
|
30
|
+
try:
|
|
31
|
+
return self.ssm_client.get_parameter(Name=secret_name, WithDecryption=False)[
|
|
32
|
+
"Parameter"
|
|
33
|
+
]["ARN"]
|
|
34
|
+
except self.ssm_client.exceptions.ParameterNotFound:
|
|
35
|
+
pass
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
try:
|
|
38
|
+
return self.secrets_manager_client.describe_secret(SecretId=secret_name)["ARN"]
|
|
39
|
+
except self.secrets_manager_client.exceptions.ResourceNotFoundException:
|
|
40
|
+
pass
|
|
35
41
|
|
|
36
|
-
|
|
37
|
-
return secrets_manager_client.describe_secret(SecretId=secret_name)["ARN"]
|
|
38
|
-
except secrets_manager_client.exceptions.ResourceNotFoundException:
|
|
39
|
-
pass
|
|
42
|
+
raise SecretNotFoundException(secret_name)
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
# Todo: This probably does not belong in the secrets provider. When it moves, take the Todoed exceptions from below
|
|
45
|
+
def get_addon_type(self, addon_name: str) -> str:
|
|
46
|
+
addon_type = None
|
|
47
|
+
try:
|
|
48
|
+
addon_config = json.loads(
|
|
49
|
+
self.ssm_client.get_parameter(
|
|
50
|
+
Name=f"/copilot/applications/{self.application_name}/environments/{self.env}/addons"
|
|
51
|
+
)["Parameter"]["Value"]
|
|
52
|
+
)
|
|
53
|
+
except self.ssm_client.exceptions.ParameterNotFound:
|
|
54
|
+
raise ParameterNotFoundException(self.application_name, self.env)
|
|
42
55
|
|
|
56
|
+
if addon_name not in addon_config.keys():
|
|
57
|
+
raise AddonNotFoundException(addon_name)
|
|
58
|
+
|
|
59
|
+
for name, config in addon_config.items():
|
|
60
|
+
if name == addon_name:
|
|
61
|
+
if not config.get("type"):
|
|
62
|
+
raise AddonTypeMissingFromConfigException(addon_name)
|
|
63
|
+
addon_type = config["type"]
|
|
64
|
+
|
|
65
|
+
if not addon_type or addon_type not in CONDUIT_ADDON_TYPES:
|
|
66
|
+
raise InvalidAddonTypeException(addon_type)
|
|
67
|
+
|
|
68
|
+
if "postgres" in addon_type:
|
|
69
|
+
addon_type = "postgres"
|
|
70
|
+
|
|
71
|
+
return addon_type
|
|
72
|
+
|
|
73
|
+
def get_parameter_name(self, addon_type: str, addon_name: str, access: str) -> str:
|
|
74
|
+
if addon_type == "postgres":
|
|
75
|
+
return f"/copilot/{self.application_name}/{self.env}/conduits/{self._normalise_secret_name(addon_name)}_{access.upper()}"
|
|
76
|
+
elif addon_type == "redis" or addon_type == "opensearch":
|
|
77
|
+
return f"/copilot/{self.application_name}/{self.env}/conduits/{self._normalise_secret_name(addon_name)}_ENDPOINT"
|
|
78
|
+
else:
|
|
79
|
+
return f"/copilot/{self.application_name}/{self.env}/conduits/{self._normalise_secret_name(addon_name)}"
|
|
80
|
+
|
|
81
|
+
def _normalise_secret_name(self, addon_name: str) -> str:
|
|
82
|
+
return addon_name.replace("-", "_").upper()
|
|
43
83
|
|
|
44
|
-
def get_addon_type(ssm_client, application_name: str, env: str, addon_name: str) -> str:
|
|
45
|
-
addon_type = None
|
|
46
|
-
try:
|
|
47
|
-
addon_config = json.loads(
|
|
48
|
-
ssm_client.get_parameter(
|
|
49
|
-
Name=f"/copilot/applications/{application_name}/environments/{env}/addons"
|
|
50
|
-
)["Parameter"]["Value"]
|
|
51
|
-
)
|
|
52
|
-
except ssm_client.exceptions.ParameterNotFound:
|
|
53
|
-
raise ParameterNotFoundError
|
|
54
84
|
|
|
55
|
-
|
|
56
|
-
|
|
85
|
+
# Todo: This probably does not belong in the secrets provider. Move it when we find a better home for get_addon_type()
|
|
86
|
+
class AddonException(PlatformException):
|
|
87
|
+
pass
|
|
57
88
|
|
|
58
|
-
for name, config in addon_config.items():
|
|
59
|
-
if name == addon_name:
|
|
60
|
-
if not config.get("type"):
|
|
61
|
-
raise AddonTypeMissingFromConfigError()
|
|
62
|
-
addon_type = config["type"]
|
|
63
89
|
|
|
64
|
-
|
|
65
|
-
|
|
90
|
+
# Todo: This probably does not belong in the secrets provider. Move it when we find a better home for get_addon_type()
|
|
91
|
+
class AddonNotFoundException(AddonException):
|
|
92
|
+
def __init__(self, addon_name: str):
|
|
93
|
+
super().__init__(f"""Addon "{addon_name}" does not exist.""")
|
|
66
94
|
|
|
67
|
-
if "postgres" in addon_type:
|
|
68
|
-
addon_type = "postgres"
|
|
69
95
|
|
|
70
|
-
|
|
96
|
+
# Todo: This probably does not belong in the secrets provider. Move it when we find a better home for get_addon_type()
|
|
97
|
+
class AddonTypeMissingFromConfigException(AddonException):
|
|
98
|
+
def __init__(self, addon_name: str):
|
|
99
|
+
super().__init__(
|
|
100
|
+
f"""The configuration for the addon {addon_name}, is misconfigured and missing the addon type."""
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Todo: This probably does not belong in the secrets provider. Move it when we find a better home for get_addon_type()
|
|
105
|
+
class InvalidAddonTypeException(AddonException):
|
|
106
|
+
def __init__(self, addon_type):
|
|
107
|
+
self.addon_type = addon_type
|
|
108
|
+
super().__init__(
|
|
109
|
+
f"""Addon type "{self.addon_type}" is not supported, we support: {", ".join(CONDUIT_ADDON_TYPES)}."""
|
|
110
|
+
)
|
|
71
111
|
|
|
72
112
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
113
|
+
class SecretException(PlatformException):
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ParameterNotFoundException(SecretException):
|
|
118
|
+
def __init__(self, application_name: str, environment: str):
|
|
119
|
+
super().__init__(
|
|
120
|
+
f"""No parameter called "/copilot/applications/{application_name}/environments/{environment}/addons". Try deploying the "{application_name}" "{environment}" environment."""
|
|
121
|
+
)
|
|
82
122
|
|
|
83
123
|
|
|
84
|
-
|
|
85
|
-
|
|
124
|
+
class SecretNotFoundException(SecretException):
|
|
125
|
+
def __init__(self, secret_name: str):
|
|
126
|
+
super().__init__(f"""No secret called "{secret_name}".""")
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ValidationException(PlatformException):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class IncompatibleMajorVersionException(ValidationException):
|
|
9
|
+
def __init__(self, app_version: str, check_version: str):
|
|
10
|
+
super().__init__()
|
|
11
|
+
self.app_version = app_version
|
|
12
|
+
self.check_version = check_version
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class IncompatibleMinorVersionException(ValidationException):
|
|
16
|
+
def __init__(self, app_version: str, check_version: str):
|
|
17
|
+
super().__init__()
|
|
18
|
+
self.app_version = app_version
|
|
19
|
+
self.check_version = check_version
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import os
|
|
2
3
|
import re
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from typing import Dict
|
|
@@ -8,7 +9,7 @@ import yaml
|
|
|
8
9
|
from boto3 import Session
|
|
9
10
|
from yaml.parser import ParserError
|
|
10
11
|
|
|
11
|
-
from dbt_platform_helper.
|
|
12
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
12
13
|
from dbt_platform_helper.utils.aws import get_aws_session_or_abort
|
|
13
14
|
from dbt_platform_helper.utils.aws import get_profile_name_from_account_id
|
|
14
15
|
from dbt_platform_helper.utils.aws import get_ssm_secrets
|
|
@@ -80,7 +81,7 @@ def load_application(app: str = None, default_session: Session = None) -> Applic
|
|
|
80
81
|
WithDecryption=False,
|
|
81
82
|
)
|
|
82
83
|
except ssm_client.exceptions.ParameterNotFound:
|
|
83
|
-
raise
|
|
84
|
+
raise ApplicationNotFoundException(app)
|
|
84
85
|
|
|
85
86
|
path = f"/copilot/applications/{application.name}/environments"
|
|
86
87
|
secrets = get_ssm_secrets(app, None, current_session, path)
|
|
@@ -135,3 +136,14 @@ def get_application_name():
|
|
|
135
136
|
abort_with_error("Cannot get application name. No copilot/.workspace file found")
|
|
136
137
|
|
|
137
138
|
return app_name
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ApplicationException(PlatformException):
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ApplicationNotFoundException(ApplicationException):
|
|
146
|
+
def __init__(self, application_name: str):
|
|
147
|
+
super().__init__(
|
|
148
|
+
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."""
|
|
149
|
+
)
|