dbt-platform-helper 13.1.1__py3-none-any.whl → 13.2.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.
Potentially problematic release.
This version of dbt-platform-helper might be problematic. Click here for more details.
- dbt_platform_helper/commands/application.py +3 -5
- dbt_platform_helper/commands/codebase.py +2 -4
- dbt_platform_helper/commands/conduit.py +2 -4
- dbt_platform_helper/commands/config.py +19 -17
- dbt_platform_helper/commands/copilot.py +13 -390
- dbt_platform_helper/commands/environment.py +6 -6
- dbt_platform_helper/commands/generate.py +2 -3
- dbt_platform_helper/commands/notify.py +2 -4
- dbt_platform_helper/commands/pipeline.py +2 -4
- dbt_platform_helper/commands/secrets.py +2 -4
- dbt_platform_helper/commands/version.py +2 -2
- dbt_platform_helper/domain/codebase.py +14 -11
- dbt_platform_helper/domain/copilot.py +397 -0
- dbt_platform_helper/domain/copilot_environment.py +6 -6
- dbt_platform_helper/domain/maintenance_page.py +227 -431
- dbt_platform_helper/domain/pipelines.py +1 -1
- dbt_platform_helper/domain/terraform_environment.py +1 -1
- dbt_platform_helper/domain/versioning.py +157 -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/cache.py +40 -4
- dbt_platform_helper/providers/config_validator.py +15 -14
- dbt_platform_helper/providers/copilot.py +1 -1
- dbt_platform_helper/providers/io.py +17 -0
- dbt_platform_helper/providers/kms.py +22 -0
- dbt_platform_helper/providers/load_balancers.py +269 -43
- dbt_platform_helper/providers/semantic_version.py +33 -10
- dbt_platform_helper/providers/version.py +42 -0
- dbt_platform_helper/providers/yaml_file.py +0 -1
- dbt_platform_helper/utils/application.py +14 -0
- dbt_platform_helper/utils/aws.py +27 -4
- dbt_platform_helper/utils/tool_versioning.py +96 -0
- {dbt_platform_helper-13.1.1.dist-info → dbt_platform_helper-13.2.0.dist-info}/METADATA +3 -4
- {dbt_platform_helper-13.1.1.dist-info → dbt_platform_helper-13.2.0.dist-info}/RECORD +39 -35
- dbt_platform_helper/providers/opensearch.py +0 -36
- dbt_platform_helper/providers/redis.py +0 -34
- dbt_platform_helper/utils/versioning.py +0 -238
- /dbt_platform_helper/providers/{aws.py → aws/exceptions.py} +0 -0
- {dbt_platform_helper-13.1.1.dist-info → dbt_platform_helper-13.2.0.dist-info}/LICENSE +0 -0
- {dbt_platform_helper-13.1.1.dist-info → dbt_platform_helper-13.2.0.dist-info}/WHEEL +0 -0
- {dbt_platform_helper-13.1.1.dist-info → dbt_platform_helper-13.2.0.dist-info}/entry_points.txt +0 -0
|
@@ -1,65 +1,280 @@
|
|
|
1
|
-
import
|
|
1
|
+
from boto3 import Session
|
|
2
2
|
|
|
3
3
|
from dbt_platform_helper.platform_exception import PlatformException
|
|
4
|
+
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
5
|
+
from dbt_platform_helper.utils.aws import get_aws_session_or_abort
|
|
4
6
|
|
|
5
|
-
# TODO - a good candidate for a dataclass when this is refactored into a class.
|
|
6
|
-
# Below methods should also really be refactored to not be so tightly coupled with eachother.
|
|
7
7
|
|
|
8
|
+
def normalise_to_cidr(ip: str):
|
|
9
|
+
if "/" in ip:
|
|
10
|
+
return ip
|
|
11
|
+
SINGLE_IPV4_CIDR_PREFIX_LENGTH = "32"
|
|
12
|
+
return f"{ip}/{SINGLE_IPV4_CIDR_PREFIX_LENGTH}"
|
|
8
13
|
|
|
9
|
-
def get_load_balancer_for_application(session: boto3.Session, app: str, env: str) -> str:
|
|
10
|
-
lb_client = session.client("elbv2")
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
load_balancers = [lb["LoadBalancerArn"] for lb in describe_response["LoadBalancers"]]
|
|
15
|
+
class LoadBalancerProvider:
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
def __init__(self, session: Session = None, io: ClickIOProvider = ClickIOProvider()):
|
|
18
|
+
self.session = session
|
|
19
|
+
self.evlb_client = self._get_client("elbv2")
|
|
20
|
+
self.rg_tagging_client = self._get_client("resourcegroupstaggingapi")
|
|
21
|
+
self.io = io
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
load_balancer_arn = lb["ResourceArn"]
|
|
23
|
+
def _get_client(self, client: str):
|
|
24
|
+
if not self.session:
|
|
25
|
+
self.session = get_aws_session_or_abort()
|
|
26
|
+
return self.session.client(client)
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
def find_target_group(self, app: str, env: str, svc: str) -> str:
|
|
29
|
+
target_group_arn = None
|
|
30
|
+
|
|
31
|
+
response = self.rg_tagging_client.get_resources(
|
|
32
|
+
TagFilters=[
|
|
33
|
+
{
|
|
34
|
+
"Key": "copilot-application",
|
|
35
|
+
"Values": [
|
|
36
|
+
app,
|
|
37
|
+
],
|
|
38
|
+
"Key": "copilot-environment",
|
|
39
|
+
"Values": [
|
|
40
|
+
env,
|
|
41
|
+
],
|
|
42
|
+
"Key": "copilot-service",
|
|
43
|
+
"Values": [
|
|
44
|
+
svc,
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
ResourceTypeFilters=[
|
|
49
|
+
"elasticloadbalancing:targetgroup",
|
|
50
|
+
],
|
|
51
|
+
) # TODO should be paginated
|
|
52
|
+
for resource in response["ResourceTagMappingList"]:
|
|
53
|
+
tags = {tag["Key"]: tag["Value"] for tag in resource["Tags"]}
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
"copilot-service" in tags
|
|
57
|
+
and tags["copilot-service"] == svc
|
|
58
|
+
and "copilot-environment" in tags
|
|
59
|
+
and tags["copilot-environment"] == env
|
|
60
|
+
and "copilot-application" in tags
|
|
61
|
+
and tags["copilot-application"] == app
|
|
62
|
+
):
|
|
63
|
+
target_group_arn = resource["ResourceARN"]
|
|
64
|
+
|
|
65
|
+
if not target_group_arn:
|
|
66
|
+
self.io.error(
|
|
67
|
+
f"No target group found for application: {app}, environment: {env}, service: {svc}",
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return target_group_arn
|
|
71
|
+
|
|
72
|
+
def get_https_certificate_for_application(self, app: str, env: str) -> str:
|
|
73
|
+
listener_arn = self.get_https_listener_for_application(app, env)
|
|
74
|
+
certificates = self.evlb_client.describe_listener_certificates(ListenerArn=listener_arn)[
|
|
75
|
+
"Certificates"
|
|
76
|
+
] # TODO should be paginated
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
certificate_arn = next(c["CertificateArn"] for c in certificates if c["IsDefault"])
|
|
80
|
+
except StopIteration:
|
|
81
|
+
raise CertificateNotFoundException(env)
|
|
82
|
+
|
|
83
|
+
return certificate_arn
|
|
84
|
+
|
|
85
|
+
def get_https_listener_for_application(self, app: str, env: str) -> str:
|
|
86
|
+
load_balancer_arn = self.get_load_balancer_for_application(app, env)
|
|
87
|
+
|
|
88
|
+
listeners = self.evlb_client.describe_listeners(LoadBalancerArn=load_balancer_arn)[
|
|
89
|
+
"Listeners"
|
|
90
|
+
] # TODO should be paginated
|
|
91
|
+
|
|
92
|
+
listener_arn = None
|
|
27
93
|
|
|
28
|
-
|
|
94
|
+
try:
|
|
95
|
+
listener_arn = next(l["ListenerArn"] for l in listeners if l["Protocol"] == "HTTPS")
|
|
96
|
+
except StopIteration:
|
|
97
|
+
pass
|
|
29
98
|
|
|
99
|
+
if not listener_arn:
|
|
100
|
+
raise ListenerNotFoundException(app, env)
|
|
30
101
|
|
|
31
|
-
|
|
32
|
-
load_balancer_arn = get_load_balancer_for_application(session, app, env)
|
|
33
|
-
lb_client = session.client("elbv2")
|
|
34
|
-
listeners = lb_client.describe_listeners(LoadBalancerArn=load_balancer_arn)["Listeners"]
|
|
102
|
+
return listener_arn
|
|
35
103
|
|
|
36
|
-
|
|
104
|
+
def get_load_balancer_for_application(self, app: str, env: str) -> str:
|
|
105
|
+
describe_response = self.evlb_client.describe_load_balancers()
|
|
106
|
+
load_balancers = [lb["LoadBalancerArn"] for lb in describe_response["LoadBalancers"]]
|
|
37
107
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
108
|
+
tag_descriptions = []
|
|
109
|
+
for i in range(0, len(load_balancers), 20):
|
|
110
|
+
chunk = load_balancers[i : i + 20]
|
|
111
|
+
tag_descriptions.extend(
|
|
112
|
+
self.evlb_client.describe_tags(ResourceArns=chunk)["TagDescriptions"]
|
|
113
|
+
)
|
|
42
114
|
|
|
43
|
-
|
|
44
|
-
|
|
115
|
+
for lb in tag_descriptions:
|
|
116
|
+
tags = {t["Key"]: t["Value"] for t in lb["Tags"]}
|
|
117
|
+
# TODO copilot hangover, creates coupling to specific tags could update to check application and environment
|
|
118
|
+
if tags.get("copilot-application") == app and tags.get("copilot-environment") == env:
|
|
119
|
+
return lb["ResourceArn"]
|
|
45
120
|
|
|
46
|
-
|
|
121
|
+
raise LoadBalancerNotFoundException(app, env)
|
|
47
122
|
|
|
123
|
+
def get_host_header_conditions(self, listener_arn: str, target_group_arn: str) -> list:
|
|
124
|
+
rules = self.evlb_client.describe_rules(ListenerArn=listener_arn)[
|
|
125
|
+
"Rules"
|
|
126
|
+
] # TODO should be paginated
|
|
48
127
|
|
|
49
|
-
|
|
128
|
+
conditions = []
|
|
50
129
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
]
|
|
130
|
+
for rule in rules:
|
|
131
|
+
for action in rule["Actions"]:
|
|
132
|
+
if action["Type"] == "forward" and action["TargetGroupArn"] == target_group_arn:
|
|
133
|
+
conditions = rule["Conditions"]
|
|
56
134
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
135
|
+
if not conditions:
|
|
136
|
+
raise ListenerRuleConditionsNotFoundException(listener_arn)
|
|
137
|
+
|
|
138
|
+
# filter to host-header conditions
|
|
139
|
+
conditions = [
|
|
140
|
+
{i: condition[i] for i in condition if i != "Values"}
|
|
141
|
+
for condition in conditions
|
|
142
|
+
if condition["Field"] == "host-header"
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
# remove internal hosts
|
|
146
|
+
conditions[0]["HostHeaderConfig"]["Values"] = [
|
|
147
|
+
v for v in conditions[0]["HostHeaderConfig"]["Values"]
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
return conditions
|
|
151
|
+
|
|
152
|
+
def get_rules_tag_descriptions_by_listener_arn(self, listener_arn: str) -> list:
|
|
153
|
+
rules = self.evlb_client.describe_rules(ListenerArn=listener_arn)[
|
|
154
|
+
"Rules"
|
|
155
|
+
] # TODO should be paginated
|
|
156
|
+
return self.get_rules_tag_descriptions(rules)
|
|
157
|
+
|
|
158
|
+
def get_rules_tag_descriptions(self, rules: list) -> list:
|
|
159
|
+
tag_descriptions = []
|
|
160
|
+
chunk_size = 20
|
|
161
|
+
|
|
162
|
+
for i in range(0, len(rules), chunk_size):
|
|
163
|
+
chunk = rules[i : i + chunk_size]
|
|
164
|
+
resource_arns = [r["RuleArn"] for r in chunk]
|
|
165
|
+
response = self.evlb_client.describe_tags(
|
|
166
|
+
ResourceArns=resource_arns
|
|
167
|
+
) # TODO should be paginated
|
|
168
|
+
tag_descriptions.extend(response["TagDescriptions"])
|
|
169
|
+
|
|
170
|
+
return tag_descriptions
|
|
171
|
+
|
|
172
|
+
def create_rule(
|
|
173
|
+
self,
|
|
174
|
+
listener_arn: str,
|
|
175
|
+
actions: list,
|
|
176
|
+
conditions: list,
|
|
177
|
+
priority: int,
|
|
178
|
+
tags: list,
|
|
179
|
+
):
|
|
180
|
+
return self.evlb_client.create_rule(
|
|
181
|
+
ListenerArn=listener_arn,
|
|
182
|
+
Priority=priority,
|
|
183
|
+
Conditions=conditions,
|
|
184
|
+
Actions=actions,
|
|
185
|
+
Tags=tags,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def create_forward_rule(
|
|
189
|
+
self,
|
|
190
|
+
listener_arn: str,
|
|
191
|
+
target_group_arn: str,
|
|
192
|
+
rule_name: str,
|
|
193
|
+
priority: int,
|
|
194
|
+
conditions: list,
|
|
195
|
+
additional_tags: list = [],
|
|
196
|
+
):
|
|
197
|
+
return self.create_rule(
|
|
198
|
+
listener_arn=listener_arn,
|
|
199
|
+
priority=priority,
|
|
200
|
+
conditions=conditions,
|
|
201
|
+
actions=[{"Type": "forward", "TargetGroupArn": target_group_arn}],
|
|
202
|
+
tags=[{"Key": "name", "Value": rule_name}, *additional_tags],
|
|
203
|
+
)
|
|
61
204
|
|
|
62
|
-
|
|
205
|
+
def create_header_rule(
|
|
206
|
+
self,
|
|
207
|
+
listener_arn: str,
|
|
208
|
+
target_group_arn: str,
|
|
209
|
+
header_name: str,
|
|
210
|
+
values: list,
|
|
211
|
+
rule_name: str,
|
|
212
|
+
priority: int,
|
|
213
|
+
conditions: list,
|
|
214
|
+
additional_tags: list = [],
|
|
215
|
+
):
|
|
216
|
+
|
|
217
|
+
combined_conditions = [
|
|
218
|
+
{
|
|
219
|
+
"Field": "http-header",
|
|
220
|
+
"HttpHeaderConfig": {"HttpHeaderName": header_name, "Values": values},
|
|
221
|
+
}
|
|
222
|
+
] + conditions
|
|
223
|
+
|
|
224
|
+
self.create_forward_rule(
|
|
225
|
+
listener_arn,
|
|
226
|
+
target_group_arn,
|
|
227
|
+
rule_name,
|
|
228
|
+
priority,
|
|
229
|
+
combined_conditions,
|
|
230
|
+
additional_tags,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
self.io.debug(
|
|
234
|
+
f"Creating listener rule {rule_name} for HTTPS Listener with arn {listener_arn}.\nIf request header {header_name} contains one of the values {values}, the request will be forwarded to target group with arn {target_group_arn}.\n\n",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
def create_source_ip_rule(
|
|
238
|
+
self,
|
|
239
|
+
listener_arn: str,
|
|
240
|
+
target_group_arn: str,
|
|
241
|
+
values: list,
|
|
242
|
+
rule_name: str,
|
|
243
|
+
priority: int,
|
|
244
|
+
conditions: list,
|
|
245
|
+
additional_tags: list = [],
|
|
246
|
+
):
|
|
247
|
+
combined_conditions = [
|
|
248
|
+
{
|
|
249
|
+
"Field": "source-ip",
|
|
250
|
+
"SourceIpConfig": {"Values": [normalise_to_cidr(value) for value in values]},
|
|
251
|
+
}
|
|
252
|
+
] + conditions
|
|
253
|
+
|
|
254
|
+
self.create_forward_rule(
|
|
255
|
+
listener_arn,
|
|
256
|
+
target_group_arn,
|
|
257
|
+
rule_name,
|
|
258
|
+
priority,
|
|
259
|
+
combined_conditions,
|
|
260
|
+
additional_tags,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
self.io.debug(
|
|
264
|
+
f"Creating listener rule {rule_name} for HTTPS Listener with arn {listener_arn}.\nIf request source ip matches one of the values {values}, the request will be forwarded to target group with arn {target_group_arn}.\n\n",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
def delete_listener_rule_by_tags(self, tag_descriptions: list, tag_name: str) -> list:
|
|
268
|
+
deleted_rules = []
|
|
269
|
+
|
|
270
|
+
for description in tag_descriptions:
|
|
271
|
+
tags = {t["Key"]: t["Value"] for t in description["Tags"]}
|
|
272
|
+
if tags.get("name") == tag_name:
|
|
273
|
+
if description["ResourceArn"]:
|
|
274
|
+
self.evlb_client.delete_rule(RuleArn=description["ResourceArn"])
|
|
275
|
+
deleted_rules.append(description)
|
|
276
|
+
|
|
277
|
+
return deleted_rules
|
|
63
278
|
|
|
64
279
|
|
|
65
280
|
class LoadBalancerException(PlatformException):
|
|
@@ -67,17 +282,28 @@ class LoadBalancerException(PlatformException):
|
|
|
67
282
|
|
|
68
283
|
|
|
69
284
|
class LoadBalancerNotFoundException(LoadBalancerException):
|
|
70
|
-
|
|
285
|
+
def __init__(self, application_name, env):
|
|
286
|
+
super().__init__(
|
|
287
|
+
f"No load balancer found for environment {env} in the application {application_name}."
|
|
288
|
+
)
|
|
71
289
|
|
|
72
290
|
|
|
73
291
|
class ListenerNotFoundException(LoadBalancerException):
|
|
74
|
-
|
|
292
|
+
def __init__(self, application_name, env):
|
|
293
|
+
super().__init__(
|
|
294
|
+
f"No HTTPS listener found for environment {env} in the application {application_name}."
|
|
295
|
+
)
|
|
75
296
|
|
|
76
297
|
|
|
77
298
|
class ListenerRuleNotFoundException(LoadBalancerException):
|
|
78
299
|
pass
|
|
79
300
|
|
|
80
301
|
|
|
302
|
+
class ListenerRuleConditionsNotFoundException(LoadBalancerException):
|
|
303
|
+
def __init__(self, listener_arn):
|
|
304
|
+
super().__init__(f"No listener rule conditions found for listener ARN: {listener_arn}")
|
|
305
|
+
|
|
306
|
+
|
|
81
307
|
class CertificateNotFoundException(PlatformException):
|
|
82
308
|
def __init__(self, environment_name: str):
|
|
83
309
|
super().__init__(
|
|
@@ -35,6 +35,9 @@ class SemanticVersion:
|
|
|
35
35
|
return "unknown"
|
|
36
36
|
return ".".join([str(s) for s in [self.major, self.minor, self.patch]])
|
|
37
37
|
|
|
38
|
+
def __repr__(self) -> str:
|
|
39
|
+
return str(self)
|
|
40
|
+
|
|
38
41
|
def __lt__(self, other) -> bool:
|
|
39
42
|
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
|
|
40
43
|
|
|
@@ -74,29 +77,49 @@ class SemanticVersion:
|
|
|
74
77
|
return SemanticVersion(output_version[0], output_version[1], output_version[2])
|
|
75
78
|
|
|
76
79
|
|
|
80
|
+
@dataclass
|
|
77
81
|
class VersionStatus:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
installed: SemanticVersion = None
|
|
83
|
+
latest: SemanticVersion = None
|
|
84
|
+
|
|
85
|
+
def __str__(self):
|
|
86
|
+
attrs = {
|
|
87
|
+
key: value for key, value in vars(self).items() if isinstance(value, SemanticVersion)
|
|
88
|
+
}
|
|
89
|
+
attrs_str = ", ".join(f"{key}: {value}" for key, value in attrs.items())
|
|
90
|
+
return f"{self.__class__.__name__}: {attrs_str}"
|
|
83
91
|
|
|
84
92
|
def is_outdated(self):
|
|
85
|
-
return self.
|
|
93
|
+
return self.installed != self.latest
|
|
86
94
|
|
|
87
|
-
def
|
|
95
|
+
def validate(self):
|
|
88
96
|
pass
|
|
89
97
|
|
|
90
98
|
|
|
91
99
|
@dataclass
|
|
92
100
|
class PlatformHelperVersionStatus(VersionStatus):
|
|
93
|
-
|
|
101
|
+
installed: Optional[SemanticVersion] = None
|
|
94
102
|
latest: Optional[SemanticVersion] = None
|
|
95
103
|
deprecated_version_file: Optional[SemanticVersion] = None
|
|
96
104
|
platform_config_default: Optional[SemanticVersion] = None
|
|
97
105
|
pipeline_overrides: Optional[Dict[str, str]] = field(default_factory=dict)
|
|
98
106
|
|
|
99
|
-
def
|
|
107
|
+
def __str__(self):
|
|
108
|
+
semantic_version_attrs = {
|
|
109
|
+
key: value for key, value in vars(self).items() if isinstance(value, SemanticVersion)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
class_str = ", ".join(f"{key}: {value}" for key, value in semantic_version_attrs.items())
|
|
113
|
+
|
|
114
|
+
if self.pipeline_overrides.items():
|
|
115
|
+
pipeline_overrides_str = "pipeline_overrides: " + ", ".join(
|
|
116
|
+
f"{key}: {value}" for key, value in self.pipeline_overrides.items()
|
|
117
|
+
)
|
|
118
|
+
class_str = ", ".join([class_str, pipeline_overrides_str])
|
|
119
|
+
|
|
120
|
+
return f"{self.__class__.__name__}: {class_str}"
|
|
121
|
+
|
|
122
|
+
def validate(self) -> dict:
|
|
100
123
|
if self.platform_config_default and not self.deprecated_version_file:
|
|
101
124
|
return {}
|
|
102
125
|
|
|
@@ -117,7 +140,7 @@ class PlatformHelperVersionStatus(VersionStatus):
|
|
|
117
140
|
|
|
118
141
|
if not self.platform_config_default and not self.deprecated_version_file:
|
|
119
142
|
message = f"Cannot get dbt-platform-helper version from '{PLATFORM_CONFIG_FILE}'.\n"
|
|
120
|
-
message += f"{missing_default_version_message}{self.
|
|
143
|
+
message += f"{missing_default_version_message}{self.installed}\n"
|
|
121
144
|
errors.append(message)
|
|
122
145
|
|
|
123
146
|
return {
|
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
from abc import ABC
|
|
2
|
+
from importlib.metadata import PackageNotFoundError
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
from pathlib import Path
|
|
2
5
|
|
|
3
6
|
import requests
|
|
4
7
|
|
|
8
|
+
from dbt_platform_helper.constants import PLATFORM_HELPER_VERSION_FILE
|
|
9
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
5
10
|
from dbt_platform_helper.providers.semantic_version import SemanticVersion
|
|
11
|
+
from dbt_platform_helper.providers.yaml_file import FileProviderException
|
|
12
|
+
from dbt_platform_helper.providers.yaml_file import YamlFileProvider
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InstalledVersionProviderException(PlatformException):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InstalledToolNotFoundException(InstalledVersionProviderException):
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
tool_name: str,
|
|
23
|
+
):
|
|
24
|
+
super().__init__(f"Package '{tool_name}' not found.")
|
|
6
25
|
|
|
7
26
|
|
|
8
27
|
class VersionProvider(ABC):
|
|
9
28
|
pass
|
|
10
29
|
|
|
11
30
|
|
|
31
|
+
class InstalledVersionProvider:
|
|
32
|
+
@staticmethod
|
|
33
|
+
def get_installed_tool_version(tool_name: str) -> SemanticVersion:
|
|
34
|
+
try:
|
|
35
|
+
return SemanticVersion.from_string(version(tool_name))
|
|
36
|
+
except PackageNotFoundError:
|
|
37
|
+
raise InstalledToolNotFoundException(tool_name)
|
|
38
|
+
|
|
39
|
+
|
|
12
40
|
# TODO add timeouts and exception handling for requests
|
|
13
41
|
# TODO Alternatively use the gitpython package?
|
|
14
42
|
class GithubVersionProvider(VersionProvider):
|
|
@@ -34,3 +62,17 @@ class PyPiVersionProvider(VersionProvider):
|
|
|
34
62
|
parsed_released_versions = [SemanticVersion.from_string(v) for v in released_versions]
|
|
35
63
|
parsed_released_versions.sort(reverse=True)
|
|
36
64
|
return parsed_released_versions[0]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class DeprecatedVersionFileVersionProvider(VersionProvider):
|
|
68
|
+
def __init__(self, file_provider: YamlFileProvider):
|
|
69
|
+
self.file_provider = file_provider or YamlFileProvider
|
|
70
|
+
|
|
71
|
+
def get_required_version(self) -> SemanticVersion:
|
|
72
|
+
deprecated_version_file = Path(PLATFORM_HELPER_VERSION_FILE)
|
|
73
|
+
try:
|
|
74
|
+
loaded_version = self.file_provider.load(deprecated_version_file)
|
|
75
|
+
version_from_file = SemanticVersion.from_string(loaded_version)
|
|
76
|
+
except FileProviderException:
|
|
77
|
+
version_from_file = None
|
|
78
|
+
return version_from_file
|
|
@@ -146,3 +146,17 @@ class ApplicationNotFoundException(ApplicationException):
|
|
|
146
146
|
super().__init__(
|
|
147
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."""
|
|
148
148
|
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class ApplicationServiceNotFoundException(ApplicationException):
|
|
152
|
+
def __init__(self, application_name: str, svc_name: str):
|
|
153
|
+
super().__init__(
|
|
154
|
+
f"""The service {svc_name} was not found in the application {application_name}. It either does not exist, or has not been deployed."""
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ApplicationEnvironmentNotFoundException(ApplicationException):
|
|
159
|
+
def __init__(self, application_name: str, environment: str):
|
|
160
|
+
super().__init__(
|
|
161
|
+
f"""The environment "{environment}" either does not exist or has not been deployed for the application {application_name}."""
|
|
162
|
+
)
|
dbt_platform_helper/utils/aws.py
CHANGED
|
@@ -12,13 +12,16 @@ import botocore.exceptions
|
|
|
12
12
|
import click
|
|
13
13
|
import yaml
|
|
14
14
|
from boto3 import Session
|
|
15
|
+
from botocore.exceptions import ClientError
|
|
15
16
|
|
|
16
17
|
from dbt_platform_helper.constants import REFRESH_TOKEN_MESSAGE
|
|
17
18
|
from dbt_platform_helper.platform_exception import PlatformException
|
|
18
|
-
from dbt_platform_helper.providers.aws import
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
from dbt_platform_helper.providers.aws import
|
|
19
|
+
from dbt_platform_helper.providers.aws.exceptions import (
|
|
20
|
+
CopilotCodebaseNotFoundException,
|
|
21
|
+
)
|
|
22
|
+
from dbt_platform_helper.providers.aws.exceptions import ImageNotFoundException
|
|
23
|
+
from dbt_platform_helper.providers.aws.exceptions import LogGroupNotFoundException
|
|
24
|
+
from dbt_platform_helper.providers.aws.exceptions import RepositoryNotFoundException
|
|
22
25
|
from dbt_platform_helper.providers.validation import ValidationException
|
|
23
26
|
|
|
24
27
|
SSM_BASE_PATH = "/copilot/{app}/{env}/secrets/"
|
|
@@ -484,3 +487,23 @@ def wait_for_log_group_to_exist(log_client, log_group_name, attempts=30):
|
|
|
484
487
|
|
|
485
488
|
if not log_group_exists:
|
|
486
489
|
raise LogGroupNotFoundException(log_group_name)
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def get_image_build_project(codebuild_client, application, codebase):
|
|
493
|
+
project_name = f"{application}-{codebase}-codebase-image-build"
|
|
494
|
+
response = codebuild_client.batch_get_projects(names=[project_name])
|
|
495
|
+
|
|
496
|
+
if bool(response.get("projects")):
|
|
497
|
+
return project_name
|
|
498
|
+
else:
|
|
499
|
+
return f"{application}-{codebase}-codebase-pipeline-image-build"
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def get_manual_release_pipeline(codepipeline_client, application, codebase):
|
|
503
|
+
pipeline_name = f"{application}-{codebase}-manual-release"
|
|
504
|
+
try:
|
|
505
|
+
codepipeline_client.get_pipeline(name=pipeline_name)
|
|
506
|
+
return pipeline_name
|
|
507
|
+
except ClientError as e:
|
|
508
|
+
if e.response["Error"]["Code"] == "PipelineNotFoundException":
|
|
509
|
+
return f"{pipeline_name}-pipeline"
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from dbt_platform_helper.constants import DEFAULT_TERRAFORM_PLATFORM_MODULES_VERSION
|
|
6
|
+
from dbt_platform_helper.domain.versioning import PlatformHelperVersioning
|
|
7
|
+
from dbt_platform_helper.providers.config import ConfigProvider
|
|
8
|
+
from dbt_platform_helper.providers.semantic_version import PlatformHelperVersionStatus
|
|
9
|
+
from dbt_platform_helper.providers.semantic_version import SemanticVersion
|
|
10
|
+
from dbt_platform_helper.providers.semantic_version import VersionStatus
|
|
11
|
+
from dbt_platform_helper.providers.validation import ValidationException
|
|
12
|
+
from dbt_platform_helper.providers.version import DeprecatedVersionFileVersionProvider
|
|
13
|
+
from dbt_platform_helper.providers.version import GithubVersionProvider
|
|
14
|
+
from dbt_platform_helper.providers.version import InstalledVersionProvider
|
|
15
|
+
from dbt_platform_helper.providers.version import PyPiVersionProvider
|
|
16
|
+
from dbt_platform_helper.providers.yaml_file import YamlFileProvider
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# TODO to be removed after config tests are updated - temporary wrapper mid-refactor
|
|
20
|
+
def get_platform_helper_version_status(
|
|
21
|
+
include_project_versions=True,
|
|
22
|
+
yaml_provider=YamlFileProvider,
|
|
23
|
+
) -> PlatformHelperVersionStatus:
|
|
24
|
+
return PlatformHelperVersioning(
|
|
25
|
+
pypi_provider=PyPiVersionProvider,
|
|
26
|
+
installed_version_provider=InstalledVersionProvider(),
|
|
27
|
+
version_file_version_provider=DeprecatedVersionFileVersionProvider(yaml_provider),
|
|
28
|
+
config_provider=ConfigProvider(),
|
|
29
|
+
)._get_version_status(include_project_versions=include_project_versions)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_required_terraform_platform_modules_version(
|
|
33
|
+
cli_terraform_platform_modules_version, platform_config_terraform_modules_default_version
|
|
34
|
+
):
|
|
35
|
+
version_preference_order = [
|
|
36
|
+
cli_terraform_platform_modules_version,
|
|
37
|
+
platform_config_terraform_modules_default_version,
|
|
38
|
+
DEFAULT_TERRAFORM_PLATFORM_MODULES_VERSION,
|
|
39
|
+
]
|
|
40
|
+
return [version for version in version_preference_order if version][0]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
##################################################################################
|
|
44
|
+
# Only used in Config domain
|
|
45
|
+
# TODO Relocate along with tests when we refactor config command in DBTP-1538
|
|
46
|
+
##################################################################################
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Getting version from the "Generated by" comment in a file that was generated from a template
|
|
50
|
+
# TODO where does this belong? It sort of belongs to our platform-helper templating
|
|
51
|
+
def get_template_generated_with_version(template_file_path: str) -> SemanticVersion:
|
|
52
|
+
try:
|
|
53
|
+
template_contents = Path(template_file_path).read_text()
|
|
54
|
+
template_version = re.match(
|
|
55
|
+
r"# Generated by platform-helper ([v.\-0-9]+)", template_contents
|
|
56
|
+
).group(1)
|
|
57
|
+
return SemanticVersion.from_string(template_version)
|
|
58
|
+
except (IndexError, AttributeError):
|
|
59
|
+
raise ValidationException(f"Template {template_file_path} has no version information")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def validate_template_version(app_version: SemanticVersion, template_file_path: str):
|
|
63
|
+
app_version.validate_compatibility_with(get_template_generated_with_version(template_file_path))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# Local version and latest release of tool.
|
|
67
|
+
# Used only in config command.
|
|
68
|
+
# TODO Move to config domain
|
|
69
|
+
def get_copilot_versions() -> VersionStatus:
|
|
70
|
+
copilot_version = None
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
response = subprocess.run("copilot --version", capture_output=True, shell=True)
|
|
74
|
+
[copilot_version] = re.findall(r"[0-9.]+", response.stdout.decode("utf8"))
|
|
75
|
+
except ValueError:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
return VersionStatus(
|
|
79
|
+
SemanticVersion.from_string(copilot_version),
|
|
80
|
+
GithubVersionProvider.get_latest_version("aws/copilot-cli"),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# Local version and latest release of tool.
|
|
85
|
+
# Used only in config command.
|
|
86
|
+
# TODO Move to config domain
|
|
87
|
+
def get_aws_versions() -> VersionStatus:
|
|
88
|
+
aws_version = None
|
|
89
|
+
try:
|
|
90
|
+
response = subprocess.run("aws --version", capture_output=True, shell=True)
|
|
91
|
+
matched = re.match(r"aws-cli/([0-9.]+)", response.stdout.decode("utf8"))
|
|
92
|
+
aws_version = SemanticVersion.from_string(matched.group(1))
|
|
93
|
+
except ValueError:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
return VersionStatus(aws_version, GithubVersionProvider.get_latest_version("aws/aws-cli", True))
|