dbt-platform-helper 13.1.0__py3-none-any.whl → 13.1.2__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 +4 -4
- dbt_platform_helper/commands/codebase.py +4 -4
- dbt_platform_helper/commands/conduit.py +4 -4
- dbt_platform_helper/commands/config.py +7 -5
- dbt_platform_helper/commands/copilot.py +12 -391
- dbt_platform_helper/commands/environment.py +4 -4
- dbt_platform_helper/commands/generate.py +1 -1
- dbt_platform_helper/commands/notify.py +4 -4
- dbt_platform_helper/commands/pipeline.py +4 -4
- dbt_platform_helper/commands/secrets.py +4 -4
- dbt_platform_helper/commands/version.py +1 -1
- dbt_platform_helper/domain/codebase.py +4 -9
- dbt_platform_helper/domain/copilot.py +394 -0
- dbt_platform_helper/domain/copilot_environment.py +6 -6
- dbt_platform_helper/domain/maintenance_page.py +193 -424
- dbt_platform_helper/domain/versioning.py +67 -0
- dbt_platform_helper/providers/io.py +14 -0
- dbt_platform_helper/providers/load_balancers.py +258 -43
- dbt_platform_helper/providers/platform_config_schema.py +3 -1
- dbt_platform_helper/providers/platform_helper_versioning.py +107 -0
- dbt_platform_helper/providers/semantic_version.py +27 -7
- dbt_platform_helper/providers/version.py +24 -0
- dbt_platform_helper/utils/application.py +14 -0
- dbt_platform_helper/utils/files.py +6 -0
- dbt_platform_helper/utils/versioning.py +11 -158
- {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-13.1.2.dist-info}/METADATA +3 -4
- {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-13.1.2.dist-info}/RECORD +30 -27
- {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-13.1.2.dist-info}/LICENSE +0 -0
- {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-13.1.2.dist-info}/WHEEL +0 -0
- {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-13.1.2.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
2
|
+
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
3
|
+
from dbt_platform_helper.providers.platform_helper_versioning import (
|
|
4
|
+
PlatformHelperVersioning,
|
|
5
|
+
)
|
|
6
|
+
from dbt_platform_helper.providers.semantic_version import PlatformHelperVersionStatus
|
|
7
|
+
from dbt_platform_helper.providers.semantic_version import SemanticVersion
|
|
8
|
+
from dbt_platform_helper.utils.files import running_as_installed_package
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PlatformHelperVersionNotFoundException(PlatformException):
|
|
12
|
+
def __init__(self):
|
|
13
|
+
super().__init__(f"""Platform helper version could not be resolved.""")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class RequiredVersion:
|
|
17
|
+
def __init__(self, io=None, platform_helper_versioning=None):
|
|
18
|
+
self.io = io or ClickIOProvider()
|
|
19
|
+
self.platform_helper_versioning = platform_helper_versioning or PlatformHelperVersioning(
|
|
20
|
+
io=self.io
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def get_required_platform_helper_version(
|
|
24
|
+
self, pipeline: str = None, version_status: PlatformHelperVersionStatus = None
|
|
25
|
+
) -> str:
|
|
26
|
+
pipeline_version = version_status.pipeline_overrides.get(pipeline)
|
|
27
|
+
version_precedence = [
|
|
28
|
+
pipeline_version,
|
|
29
|
+
version_status.platform_config_default,
|
|
30
|
+
version_status.deprecated_version_file,
|
|
31
|
+
]
|
|
32
|
+
non_null_version_precedence = [
|
|
33
|
+
f"{v}" if isinstance(v, SemanticVersion) else v for v in version_precedence if v
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
out = non_null_version_precedence[0] if non_null_version_precedence else None
|
|
37
|
+
|
|
38
|
+
if not out:
|
|
39
|
+
raise PlatformHelperVersionNotFoundException
|
|
40
|
+
|
|
41
|
+
return out
|
|
42
|
+
|
|
43
|
+
def get_required_version(self, pipeline=None):
|
|
44
|
+
version_status = self.platform_helper_versioning.get_status()
|
|
45
|
+
self.io.process_messages(version_status.validate())
|
|
46
|
+
required_version = self.get_required_platform_helper_version(pipeline, version_status)
|
|
47
|
+
self.io.info(required_version)
|
|
48
|
+
return required_version
|
|
49
|
+
|
|
50
|
+
# Used in the generate command
|
|
51
|
+
def check_platform_helper_version_mismatch(self):
|
|
52
|
+
if not running_as_installed_package():
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
version_status = self.platform_helper_versioning.get_status()
|
|
56
|
+
self.io.process_messages(version_status.validate())
|
|
57
|
+
|
|
58
|
+
required_version = SemanticVersion.from_string(
|
|
59
|
+
self.get_required_platform_helper_version(version_status=version_status)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if not version_status.local == required_version:
|
|
63
|
+
message = (
|
|
64
|
+
f"WARNING: You are running platform-helper v{version_status.local} against "
|
|
65
|
+
f"v{required_version} specified for the project."
|
|
66
|
+
)
|
|
67
|
+
self.io.warn(message)
|
|
@@ -26,6 +26,20 @@ class ClickIOProvider:
|
|
|
26
26
|
click.secho(f"Error: {message}", err=True, fg="red")
|
|
27
27
|
exit(1)
|
|
28
28
|
|
|
29
|
+
# TODO messages will be a ValidationMessages class rather than a free-rein dictionary
|
|
30
|
+
def process_messages(self, messages: dict):
|
|
31
|
+
if not messages:
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
if messages.get("errors"):
|
|
35
|
+
self.error("\n".join(messages["errors"]))
|
|
36
|
+
|
|
37
|
+
if messages.get("warnings"):
|
|
38
|
+
self.warn("\n".join(messages["warnings"]))
|
|
39
|
+
|
|
40
|
+
if messages.get("info"):
|
|
41
|
+
self.info("\n".join(messages["info"]))
|
|
42
|
+
|
|
29
43
|
|
|
30
44
|
class ClickIOProviderException(PlatformException):
|
|
31
45
|
pass
|
|
@@ -1,65 +1,269 @@
|
|
|
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
|
+
):
|
|
196
|
+
return self.create_rule(
|
|
197
|
+
listener_arn=listener_arn,
|
|
198
|
+
priority=priority,
|
|
199
|
+
conditions=conditions,
|
|
200
|
+
actions=[{"Type": "forward", "TargetGroupArn": target_group_arn}],
|
|
201
|
+
tags=[
|
|
202
|
+
{"Key": "name", "Value": rule_name},
|
|
203
|
+
],
|
|
204
|
+
)
|
|
61
205
|
|
|
62
|
-
|
|
206
|
+
def create_header_rule(
|
|
207
|
+
self,
|
|
208
|
+
listener_arn: str,
|
|
209
|
+
target_group_arn: str,
|
|
210
|
+
header_name: str,
|
|
211
|
+
values: list,
|
|
212
|
+
rule_name: str,
|
|
213
|
+
priority: int,
|
|
214
|
+
conditions: 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, target_group_arn, rule_name, priority, combined_conditions
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
self.io.info(
|
|
229
|
+
f"Creating listener rule {rule_name} for HTTPS Listener with arn {listener_arn}.\n\nIf request header {header_name} contains one of the values {values}, the request will be forwarded to target group with arn {target_group_arn}.",
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
def create_source_ip_rule(
|
|
233
|
+
self,
|
|
234
|
+
listener_arn: str,
|
|
235
|
+
target_group_arn: str,
|
|
236
|
+
values: list,
|
|
237
|
+
rule_name: str,
|
|
238
|
+
priority: int,
|
|
239
|
+
conditions: list,
|
|
240
|
+
):
|
|
241
|
+
combined_conditions = [
|
|
242
|
+
{
|
|
243
|
+
"Field": "source-ip",
|
|
244
|
+
"SourceIpConfig": {"Values": [normalise_to_cidr(value) for value in values]},
|
|
245
|
+
}
|
|
246
|
+
] + conditions
|
|
247
|
+
|
|
248
|
+
self.create_forward_rule(
|
|
249
|
+
listener_arn, target_group_arn, rule_name, priority, combined_conditions
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
self.io.info(
|
|
253
|
+
f"Creating listener rule {rule_name} for HTTPS Listener with arn {listener_arn}.\n\nIf request source ip matches one of the values {values}, the request will be forwarded to target group with arn {target_group_arn}.",
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
def delete_listener_rule_by_tags(self, tag_descriptions: list, tag_name: str) -> str:
|
|
257
|
+
current_rule_arn = None
|
|
258
|
+
|
|
259
|
+
for description in tag_descriptions:
|
|
260
|
+
tags = {t["Key"]: t["Value"] for t in description["Tags"]}
|
|
261
|
+
if tags.get("name") == tag_name:
|
|
262
|
+
current_rule_arn = description["ResourceArn"]
|
|
263
|
+
if current_rule_arn:
|
|
264
|
+
self.evlb_client.delete_rule(RuleArn=current_rule_arn)
|
|
265
|
+
|
|
266
|
+
return current_rule_arn
|
|
63
267
|
|
|
64
268
|
|
|
65
269
|
class LoadBalancerException(PlatformException):
|
|
@@ -67,17 +271,28 @@ class LoadBalancerException(PlatformException):
|
|
|
67
271
|
|
|
68
272
|
|
|
69
273
|
class LoadBalancerNotFoundException(LoadBalancerException):
|
|
70
|
-
|
|
274
|
+
def __init__(self, application_name, env):
|
|
275
|
+
super().__init__(
|
|
276
|
+
f"No load balancer found for environment {env} in the application {application_name}."
|
|
277
|
+
)
|
|
71
278
|
|
|
72
279
|
|
|
73
280
|
class ListenerNotFoundException(LoadBalancerException):
|
|
74
|
-
|
|
281
|
+
def __init__(self, application_name, env):
|
|
282
|
+
super().__init__(
|
|
283
|
+
f"No HTTPS listener found for environment {env} in the application {application_name}."
|
|
284
|
+
)
|
|
75
285
|
|
|
76
286
|
|
|
77
287
|
class ListenerRuleNotFoundException(LoadBalancerException):
|
|
78
288
|
pass
|
|
79
289
|
|
|
80
290
|
|
|
291
|
+
class ListenerRuleConditionsNotFoundException(LoadBalancerException):
|
|
292
|
+
def __init__(self, listener_arn):
|
|
293
|
+
super().__init__(f"No listener rule conditions found for listener ARN: {listener_arn}")
|
|
294
|
+
|
|
295
|
+
|
|
81
296
|
class CertificateNotFoundException(PlatformException):
|
|
82
297
|
def __init__(self, environment_name: str):
|
|
83
298
|
super().__init__(
|
|
@@ -506,7 +506,9 @@ class PlatformConfigSchema:
|
|
|
506
506
|
},
|
|
507
507
|
Optional("cross_environment_service_access"): {
|
|
508
508
|
PlatformConfigSchema.__valid_schema_key(): {
|
|
509
|
-
|
|
509
|
+
# Deprecated: We didn't implement cross application access, no service teams are asking for it.
|
|
510
|
+
# application should be removed once we can confirm that no-one is using it.
|
|
511
|
+
Optional("application"): str,
|
|
510
512
|
"environment": PlatformConfigSchema.__valid_environment_name(),
|
|
511
513
|
"account": str,
|
|
512
514
|
"service": str,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from dbt_platform_helper.constants import PLATFORM_HELPER_VERSION_FILE
|
|
5
|
+
from dbt_platform_helper.providers.config import ConfigProvider
|
|
6
|
+
from dbt_platform_helper.providers.files import FileProvider
|
|
7
|
+
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
8
|
+
from dbt_platform_helper.providers.semantic_version import (
|
|
9
|
+
IncompatibleMajorVersionException,
|
|
10
|
+
)
|
|
11
|
+
from dbt_platform_helper.providers.semantic_version import (
|
|
12
|
+
IncompatibleMinorVersionException,
|
|
13
|
+
)
|
|
14
|
+
from dbt_platform_helper.providers.semantic_version import PlatformHelperVersionStatus
|
|
15
|
+
from dbt_platform_helper.providers.semantic_version import SemanticVersion
|
|
16
|
+
from dbt_platform_helper.providers.version import LocalVersionProvider
|
|
17
|
+
from dbt_platform_helper.providers.version import LocalVersionProviderException
|
|
18
|
+
from dbt_platform_helper.providers.version import PyPiVersionProvider
|
|
19
|
+
from dbt_platform_helper.providers.yaml_file import FileProviderException
|
|
20
|
+
from dbt_platform_helper.providers.yaml_file import YamlFileProvider
|
|
21
|
+
from dbt_platform_helper.utils.files import running_as_installed_package
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PlatformHelperVersioning:
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
io: ClickIOProvider = ClickIOProvider(),
|
|
28
|
+
file_provider: FileProvider = YamlFileProvider,
|
|
29
|
+
config_provider: ConfigProvider = ConfigProvider(),
|
|
30
|
+
pypi_provider: PyPiVersionProvider = PyPiVersionProvider,
|
|
31
|
+
local_version_provider: LocalVersionProvider = LocalVersionProvider(),
|
|
32
|
+
):
|
|
33
|
+
self.io = io
|
|
34
|
+
self.file_provider = file_provider
|
|
35
|
+
self.config_provider = config_provider
|
|
36
|
+
self.pypi_provider = pypi_provider
|
|
37
|
+
self.local_version_provider = local_version_provider
|
|
38
|
+
|
|
39
|
+
def get_status(
|
|
40
|
+
self,
|
|
41
|
+
include_project_versions: bool = True,
|
|
42
|
+
) -> PlatformHelperVersionStatus:
|
|
43
|
+
try:
|
|
44
|
+
locally_installed_version = self.local_version_provider.get_installed_tool_version(
|
|
45
|
+
"dbt-platform-helper"
|
|
46
|
+
)
|
|
47
|
+
except LocalVersionProviderException:
|
|
48
|
+
locally_installed_version = None
|
|
49
|
+
|
|
50
|
+
latest_release = self.pypi_provider.get_latest_version("dbt-platform-helper")
|
|
51
|
+
|
|
52
|
+
if not include_project_versions:
|
|
53
|
+
return PlatformHelperVersionStatus(
|
|
54
|
+
local=locally_installed_version,
|
|
55
|
+
latest=latest_release,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
deprecated_version_file = Path(PLATFORM_HELPER_VERSION_FILE)
|
|
59
|
+
try:
|
|
60
|
+
loaded_version = self.file_provider.load(deprecated_version_file)
|
|
61
|
+
version_from_file = SemanticVersion.from_string(loaded_version)
|
|
62
|
+
except FileProviderException:
|
|
63
|
+
version_from_file = None
|
|
64
|
+
|
|
65
|
+
platform_config_default, pipeline_overrides = None, {}
|
|
66
|
+
|
|
67
|
+
platform_config = self.config_provider.load_unvalidated_config_file()
|
|
68
|
+
|
|
69
|
+
if platform_config:
|
|
70
|
+
platform_config_default = SemanticVersion.from_string(
|
|
71
|
+
platform_config.get("default_versions", {}).get("platform-helper")
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
pipeline_overrides = {
|
|
75
|
+
name: pipeline.get("versions", {}).get("platform-helper")
|
|
76
|
+
for name, pipeline in platform_config.get("environment_pipelines", {}).items()
|
|
77
|
+
if pipeline.get("versions", {}).get("platform-helper")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
out = PlatformHelperVersionStatus(
|
|
81
|
+
local=locally_installed_version,
|
|
82
|
+
latest=latest_release,
|
|
83
|
+
deprecated_version_file=version_from_file,
|
|
84
|
+
platform_config_default=platform_config_default,
|
|
85
|
+
pipeline_overrides=pipeline_overrides,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return out
|
|
89
|
+
|
|
90
|
+
def check_if_needs_update(self):
|
|
91
|
+
if not running_as_installed_package() or "PLATFORM_TOOLS_SKIP_VERSION_CHECK" in os.environ:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
version_status = self.get_status(include_project_versions=False)
|
|
95
|
+
|
|
96
|
+
message = (
|
|
97
|
+
f"You are running platform-helper v{version_status.local}, upgrade to "
|
|
98
|
+
f"v{version_status.latest} by running run `pip install "
|
|
99
|
+
"--upgrade dbt-platform-helper`."
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
version_status.local.validate_compatibility_with(version_status.latest)
|
|
104
|
+
except IncompatibleMajorVersionException:
|
|
105
|
+
self.io.error(message)
|
|
106
|
+
except IncompatibleMinorVersionException:
|
|
107
|
+
self.io.warn(message)
|
|
@@ -74,17 +74,22 @@ class SemanticVersion:
|
|
|
74
74
|
return SemanticVersion(output_version[0], output_version[1], output_version[2])
|
|
75
75
|
|
|
76
76
|
|
|
77
|
+
@dataclass
|
|
77
78
|
class VersionStatus:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
local: SemanticVersion = None
|
|
80
|
+
latest: SemanticVersion = None
|
|
81
|
+
|
|
82
|
+
def __str__(self):
|
|
83
|
+
attrs = {
|
|
84
|
+
key: value for key, value in vars(self).items() if isinstance(value, SemanticVersion)
|
|
85
|
+
}
|
|
86
|
+
attrs_str = ", ".join(f"{key}: {value}" for key, value in attrs.items())
|
|
87
|
+
return f"{self.__class__.__name__}: {attrs_str}"
|
|
83
88
|
|
|
84
89
|
def is_outdated(self):
|
|
85
90
|
return self.local != self.latest
|
|
86
91
|
|
|
87
|
-
def
|
|
92
|
+
def validate(self):
|
|
88
93
|
pass
|
|
89
94
|
|
|
90
95
|
|
|
@@ -96,7 +101,22 @@ class PlatformHelperVersionStatus(VersionStatus):
|
|
|
96
101
|
platform_config_default: Optional[SemanticVersion] = None
|
|
97
102
|
pipeline_overrides: Optional[Dict[str, str]] = field(default_factory=dict)
|
|
98
103
|
|
|
99
|
-
def
|
|
104
|
+
def __str__(self):
|
|
105
|
+
semantic_version_attrs = {
|
|
106
|
+
key: value for key, value in vars(self).items() if isinstance(value, SemanticVersion)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
class_str = ", ".join(f"{key}: {value}" for key, value in semantic_version_attrs.items())
|
|
110
|
+
|
|
111
|
+
if self.pipeline_overrides.items():
|
|
112
|
+
pipeline_overrides_str = "pipeline_overrides: " + ", ".join(
|
|
113
|
+
f"{key}: {value}" for key, value in self.pipeline_overrides.items()
|
|
114
|
+
)
|
|
115
|
+
class_str = ", ".join([class_str, pipeline_overrides_str])
|
|
116
|
+
|
|
117
|
+
return f"{self.__class__.__name__}: {class_str}"
|
|
118
|
+
|
|
119
|
+
def validate(self) -> dict:
|
|
100
120
|
if self.platform_config_default and not self.deprecated_version_file:
|
|
101
121
|
return {}
|
|
102
122
|
|
|
@@ -1,14 +1,38 @@
|
|
|
1
1
|
from abc import ABC
|
|
2
|
+
from importlib.metadata import PackageNotFoundError
|
|
3
|
+
from importlib.metadata import version
|
|
2
4
|
|
|
3
5
|
import requests
|
|
4
6
|
|
|
7
|
+
from dbt_platform_helper.platform_exception import PlatformException
|
|
5
8
|
from dbt_platform_helper.providers.semantic_version import SemanticVersion
|
|
6
9
|
|
|
7
10
|
|
|
11
|
+
class LocalVersionProviderException(PlatformException):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class InstalledToolNotFoundException(LocalVersionProviderException):
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
tool_name: str,
|
|
19
|
+
):
|
|
20
|
+
super().__init__(f"Package '{tool_name}' not found.")
|
|
21
|
+
|
|
22
|
+
|
|
8
23
|
class VersionProvider(ABC):
|
|
9
24
|
pass
|
|
10
25
|
|
|
11
26
|
|
|
27
|
+
class LocalVersionProvider:
|
|
28
|
+
@staticmethod
|
|
29
|
+
def get_installed_tool_version(tool_name: str) -> SemanticVersion:
|
|
30
|
+
try:
|
|
31
|
+
return SemanticVersion.from_string(version(tool_name))
|
|
32
|
+
except PackageNotFoundError:
|
|
33
|
+
raise InstalledToolNotFoundException(tool_name)
|
|
34
|
+
|
|
35
|
+
|
|
12
36
|
# TODO add timeouts and exception handling for requests
|
|
13
37
|
# TODO Alternatively use the gitpython package?
|
|
14
38
|
class GithubVersionProvider(VersionProvider):
|
|
@@ -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
|
+
)
|
|
@@ -51,3 +51,9 @@ def generate_override_files_from_template(base_path, overrides_path, output_dir,
|
|
|
51
51
|
|
|
52
52
|
generate_files_for_dir("*")
|
|
53
53
|
generate_files_for_dir("bin/*")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# TODO: we've moved this from versioning utils and removed the duplication in platform_helper_versioning
|
|
57
|
+
# Need to review if this is the correct place for this function to reside longer-term
|
|
58
|
+
def running_as_installed_package():
|
|
59
|
+
return "site-packages" in __file__
|