dbt-platform-helper 11.0.1__py3-none-any.whl → 11.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.md +83 -20
- dbt_platform_helper/commands/database.py +97 -18
- dbt_platform_helper/commands/environment.py +6 -518
- dbt_platform_helper/commands/pipeline.py +103 -12
- dbt_platform_helper/domain/__init__.py +0 -0
- dbt_platform_helper/domain/database_copy.py +220 -0
- dbt_platform_helper/domain/maintenance_page.py +459 -0
- dbt_platform_helper/providers/load_balancers.py +51 -0
- dbt_platform_helper/templates/environment-pipelines/main.tf +52 -0
- dbt_platform_helper/utils/aws.py +47 -37
- dbt_platform_helper/utils/validation.py +29 -0
- {dbt_platform_helper-11.0.1.dist-info → dbt_platform_helper-11.2.0.dist-info}/METADATA +2 -1
- {dbt_platform_helper-11.0.1.dist-info → dbt_platform_helper-11.2.0.dist-info}/RECORD +16 -12
- dbt_platform_helper/commands/database_helpers.py +0 -145
- {dbt_platform_helper-11.0.1.dist-info → dbt_platform_helper-11.2.0.dist-info}/LICENSE +0 -0
- {dbt_platform_helper-11.0.1.dist-info → dbt_platform_helper-11.2.0.dist-info}/WHEEL +0 -0
- {dbt_platform_helper-11.0.1.dist-info → dbt_platform_helper-11.2.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import itertools
|
|
2
|
+
import random
|
|
3
|
+
import re
|
|
4
|
+
import string
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
import boto3
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from dbt_platform_helper.providers.load_balancers import ListenerNotFoundError
|
|
13
|
+
from dbt_platform_helper.providers.load_balancers import ListenerRuleNotFoundError
|
|
14
|
+
from dbt_platform_helper.providers.load_balancers import LoadBalancerNotFoundError
|
|
15
|
+
from dbt_platform_helper.providers.load_balancers import find_https_listener
|
|
16
|
+
from dbt_platform_helper.utils.application import Environment
|
|
17
|
+
from dbt_platform_helper.utils.application import Service
|
|
18
|
+
from dbt_platform_helper.utils.application import load_application
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MaintenancePageProvider:
|
|
22
|
+
def activate(self, app, env, svc, template, vpc):
|
|
23
|
+
application = load_application(app)
|
|
24
|
+
application_environment = get_app_environment(app, env)
|
|
25
|
+
|
|
26
|
+
if "*" in svc:
|
|
27
|
+
services = [
|
|
28
|
+
s for s in application.services.values() if s.kind == "Load Balanced Web Service"
|
|
29
|
+
]
|
|
30
|
+
else:
|
|
31
|
+
all_services = [get_app_service(app, s) for s in list(svc)]
|
|
32
|
+
services = [s for s in all_services if s.kind == "Load Balanced Web Service"]
|
|
33
|
+
|
|
34
|
+
if not services:
|
|
35
|
+
click.secho(f"No services deployed yet to {app} environment {env}", fg="red")
|
|
36
|
+
raise click.Abort
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
https_listener = find_https_listener(application_environment.session, app, env)
|
|
40
|
+
current_maintenance_page = get_maintenance_page(
|
|
41
|
+
application_environment.session, https_listener
|
|
42
|
+
)
|
|
43
|
+
remove_current_maintenance_page = False
|
|
44
|
+
if current_maintenance_page:
|
|
45
|
+
remove_current_maintenance_page = click.confirm(
|
|
46
|
+
f"There is currently a '{current_maintenance_page}' maintenance page for the {env} "
|
|
47
|
+
f"environment in {app}.\nWould you like to replace it with a '{template}' "
|
|
48
|
+
f"maintenance page?"
|
|
49
|
+
)
|
|
50
|
+
if not remove_current_maintenance_page:
|
|
51
|
+
raise click.Abort
|
|
52
|
+
|
|
53
|
+
if remove_current_maintenance_page or click.confirm(
|
|
54
|
+
f"You are about to enable the '{template}' maintenance page for the {env} "
|
|
55
|
+
f"environment in {app}.\nWould you like to continue?"
|
|
56
|
+
):
|
|
57
|
+
if current_maintenance_page and remove_current_maintenance_page:
|
|
58
|
+
remove_maintenance_page(application_environment.session, https_listener)
|
|
59
|
+
|
|
60
|
+
allowed_ips = get_env_ips(vpc, application_environment)
|
|
61
|
+
|
|
62
|
+
add_maintenance_page(
|
|
63
|
+
application_environment.session,
|
|
64
|
+
https_listener,
|
|
65
|
+
app,
|
|
66
|
+
env,
|
|
67
|
+
services,
|
|
68
|
+
allowed_ips,
|
|
69
|
+
template,
|
|
70
|
+
)
|
|
71
|
+
click.secho(
|
|
72
|
+
f"Maintenance page '{template}' added for environment {env} in application {app}",
|
|
73
|
+
fg="green",
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
raise click.Abort
|
|
77
|
+
|
|
78
|
+
except LoadBalancerNotFoundError:
|
|
79
|
+
click.secho(
|
|
80
|
+
f"No load balancer found for environment {env} in the application {app}.", fg="red"
|
|
81
|
+
)
|
|
82
|
+
raise click.Abort
|
|
83
|
+
|
|
84
|
+
except ListenerNotFoundError:
|
|
85
|
+
click.secho(
|
|
86
|
+
f"No HTTPS listener found for environment {env} in the application {app}.", fg="red"
|
|
87
|
+
)
|
|
88
|
+
raise click.Abort
|
|
89
|
+
|
|
90
|
+
def deactivate(self, app, env):
|
|
91
|
+
application_environment = get_app_environment(app, env)
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
https_listener = find_https_listener(application_environment.session, app, env)
|
|
95
|
+
current_maintenance_page = get_maintenance_page(
|
|
96
|
+
application_environment.session, https_listener
|
|
97
|
+
)
|
|
98
|
+
if not current_maintenance_page:
|
|
99
|
+
click.secho("There is no current maintenance page to remove", fg="red")
|
|
100
|
+
raise click.Abort
|
|
101
|
+
|
|
102
|
+
if not click.confirm(
|
|
103
|
+
f"There is currently a '{current_maintenance_page}' maintenance page, "
|
|
104
|
+
f"would you like to remove it?"
|
|
105
|
+
):
|
|
106
|
+
raise click.Abort
|
|
107
|
+
|
|
108
|
+
remove_maintenance_page(application_environment.session, https_listener)
|
|
109
|
+
click.secho(
|
|
110
|
+
f"Maintenance page removed from environment {env} in application {app}", fg="green"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
except LoadBalancerNotFoundError:
|
|
114
|
+
click.secho(
|
|
115
|
+
f"No load balancer found for environment {env} in the application {app}.", fg="red"
|
|
116
|
+
)
|
|
117
|
+
raise click.Abort
|
|
118
|
+
|
|
119
|
+
except ListenerNotFoundError:
|
|
120
|
+
click.secho(
|
|
121
|
+
f"No HTTPS listener found for environment {env} in the application {app}.", fg="red"
|
|
122
|
+
)
|
|
123
|
+
raise click.Abort
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_app_service(app_name: str, svc_name: str) -> Service:
|
|
127
|
+
application = load_application(app_name)
|
|
128
|
+
application_service = application.services.get(svc_name)
|
|
129
|
+
|
|
130
|
+
if not application_service:
|
|
131
|
+
click.secho(
|
|
132
|
+
f"The service {svc_name} was not found in the application {app_name}. "
|
|
133
|
+
f"It either does not exist, or has not been deployed.",
|
|
134
|
+
fg="red",
|
|
135
|
+
)
|
|
136
|
+
raise click.Abort
|
|
137
|
+
|
|
138
|
+
return application_service
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_app_environment(app_name: str, env_name: str) -> Environment:
|
|
142
|
+
application = load_application(app_name)
|
|
143
|
+
application_environment = application.environments.get(env_name)
|
|
144
|
+
|
|
145
|
+
if not application_environment:
|
|
146
|
+
click.secho(
|
|
147
|
+
f"The environment {env_name} was not found in the application {app_name}. "
|
|
148
|
+
f"It either does not exist, or has not been deployed.",
|
|
149
|
+
fg="red",
|
|
150
|
+
)
|
|
151
|
+
raise click.Abort
|
|
152
|
+
|
|
153
|
+
return application_environment
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_maintenance_page(session: boto3.Session, listener_arn: str) -> Union[str, None]:
|
|
157
|
+
lb_client = session.client("elbv2")
|
|
158
|
+
|
|
159
|
+
rules = lb_client.describe_rules(ListenerArn=listener_arn)["Rules"]
|
|
160
|
+
tag_descriptions = get_rules_tag_descriptions(rules, lb_client)
|
|
161
|
+
|
|
162
|
+
maintenance_page_type = None
|
|
163
|
+
for description in tag_descriptions:
|
|
164
|
+
tags = {t["Key"]: t["Value"] for t in description["Tags"]}
|
|
165
|
+
if tags.get("name") == "MaintenancePage":
|
|
166
|
+
maintenance_page_type = tags.get("type")
|
|
167
|
+
|
|
168
|
+
return maintenance_page_type
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def remove_maintenance_page(session: boto3.Session, listener_arn: str):
|
|
172
|
+
lb_client = session.client("elbv2")
|
|
173
|
+
|
|
174
|
+
rules = lb_client.describe_rules(ListenerArn=listener_arn)["Rules"]
|
|
175
|
+
# TODO: The next line doesn't appear to do anything.
|
|
176
|
+
tag_descriptions = get_rules_tag_descriptions(rules, lb_client)
|
|
177
|
+
# TODO: In fact the following line seems to do the same but better.
|
|
178
|
+
tag_descriptions = lb_client.describe_tags(ResourceArns=[r["RuleArn"] for r in rules])[
|
|
179
|
+
"TagDescriptions"
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
for name in ["MaintenancePage", "AllowedIps", "BypassIpFilter", "AllowedSourceIps"]:
|
|
183
|
+
deleted = delete_listener_rule(tag_descriptions, name, lb_client)
|
|
184
|
+
|
|
185
|
+
if name == "MaintenancePage" and not deleted:
|
|
186
|
+
raise ListenerRuleNotFoundError()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def get_rules_tag_descriptions(rules: list, lb_client):
|
|
190
|
+
tag_descriptions = []
|
|
191
|
+
chunk_size = 20
|
|
192
|
+
|
|
193
|
+
for i in range(0, len(rules), chunk_size):
|
|
194
|
+
chunk = rules[i : i + chunk_size]
|
|
195
|
+
resource_arns = [r["RuleArn"] for r in chunk]
|
|
196
|
+
response = lb_client.describe_tags(ResourceArns=resource_arns)
|
|
197
|
+
tag_descriptions.extend(response["TagDescriptions"])
|
|
198
|
+
|
|
199
|
+
return tag_descriptions
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def delete_listener_rule(tag_descriptions: list, tag_name: str, lb_client: boto3.client):
|
|
203
|
+
current_rule_arn = None
|
|
204
|
+
|
|
205
|
+
for description in tag_descriptions:
|
|
206
|
+
tags = {t["Key"]: t["Value"] for t in description["Tags"]}
|
|
207
|
+
if tags.get("name") == tag_name:
|
|
208
|
+
current_rule_arn = description["ResourceArn"]
|
|
209
|
+
if current_rule_arn:
|
|
210
|
+
lb_client.delete_rule(RuleArn=current_rule_arn)
|
|
211
|
+
|
|
212
|
+
return current_rule_arn
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def add_maintenance_page(
|
|
216
|
+
session: boto3.Session,
|
|
217
|
+
listener_arn: str,
|
|
218
|
+
app: str,
|
|
219
|
+
env: str,
|
|
220
|
+
services: List[Service],
|
|
221
|
+
allowed_ips: tuple,
|
|
222
|
+
template: str = "default",
|
|
223
|
+
):
|
|
224
|
+
lb_client = session.client("elbv2")
|
|
225
|
+
maintenance_page_content = get_maintenance_page_template(template)
|
|
226
|
+
bypass_value = "".join(random.choices(string.ascii_lowercase + string.digits, k=12))
|
|
227
|
+
|
|
228
|
+
rule_priority = itertools.count(start=1)
|
|
229
|
+
|
|
230
|
+
for svc in services:
|
|
231
|
+
target_group_arn = find_target_group(app, env, svc.name, session)
|
|
232
|
+
|
|
233
|
+
# not all of an application's services are guaranteed to have been deployed to an environment
|
|
234
|
+
if not target_group_arn:
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
for ip in allowed_ips:
|
|
238
|
+
create_header_rule(
|
|
239
|
+
lb_client,
|
|
240
|
+
listener_arn,
|
|
241
|
+
target_group_arn,
|
|
242
|
+
"X-Forwarded-For",
|
|
243
|
+
[ip],
|
|
244
|
+
"AllowedIps",
|
|
245
|
+
next(rule_priority),
|
|
246
|
+
)
|
|
247
|
+
create_source_ip_rule(
|
|
248
|
+
lb_client,
|
|
249
|
+
listener_arn,
|
|
250
|
+
target_group_arn,
|
|
251
|
+
[ip],
|
|
252
|
+
"AllowedSourceIps",
|
|
253
|
+
next(rule_priority),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
create_header_rule(
|
|
257
|
+
lb_client,
|
|
258
|
+
listener_arn,
|
|
259
|
+
target_group_arn,
|
|
260
|
+
"Bypass-Key",
|
|
261
|
+
[bypass_value],
|
|
262
|
+
"BypassIpFilter",
|
|
263
|
+
next(rule_priority),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
click.secho(
|
|
267
|
+
f"\nUse a browser plugin to add `Bypass-Key` header with value {bypass_value} to your requests. For more detail, visit https://platform.readme.trade.gov.uk/activities/holding-and-maintenance-pages/",
|
|
268
|
+
fg="green",
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
lb_client.create_rule(
|
|
272
|
+
ListenerArn=listener_arn,
|
|
273
|
+
Priority=next(rule_priority),
|
|
274
|
+
Conditions=[
|
|
275
|
+
{
|
|
276
|
+
"Field": "path-pattern",
|
|
277
|
+
"PathPatternConfig": {"Values": ["/*"]},
|
|
278
|
+
}
|
|
279
|
+
],
|
|
280
|
+
Actions=[
|
|
281
|
+
{
|
|
282
|
+
"Type": "fixed-response",
|
|
283
|
+
"FixedResponseConfig": {
|
|
284
|
+
"StatusCode": "503",
|
|
285
|
+
"ContentType": "text/html",
|
|
286
|
+
"MessageBody": maintenance_page_content,
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
],
|
|
290
|
+
Tags=[
|
|
291
|
+
{"Key": "name", "Value": "MaintenancePage"},
|
|
292
|
+
{"Key": "type", "Value": template},
|
|
293
|
+
],
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def get_maintenance_page_template(template) -> str:
|
|
298
|
+
template_contents = (
|
|
299
|
+
Path(__file__)
|
|
300
|
+
.parent.parent.joinpath(
|
|
301
|
+
f"templates/svc/maintenance_pages/{template}.html",
|
|
302
|
+
)
|
|
303
|
+
.read_text()
|
|
304
|
+
.replace("\n", "")
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# [^\S]\s+ - Remove any space that is not preceded by a non-space character.
|
|
308
|
+
return re.sub(r"[^\S]\s+", "", template_contents)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def find_target_group(app: str, env: str, svc: str, session: boto3.Session) -> str:
|
|
312
|
+
rg_tagging_client = session.client("resourcegroupstaggingapi")
|
|
313
|
+
response = rg_tagging_client.get_resources(
|
|
314
|
+
TagFilters=[
|
|
315
|
+
{
|
|
316
|
+
"Key": "copilot-application",
|
|
317
|
+
"Values": [
|
|
318
|
+
app,
|
|
319
|
+
],
|
|
320
|
+
"Key": "copilot-environment",
|
|
321
|
+
"Values": [
|
|
322
|
+
env,
|
|
323
|
+
],
|
|
324
|
+
"Key": "copilot-service",
|
|
325
|
+
"Values": [
|
|
326
|
+
svc,
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
ResourceTypeFilters=[
|
|
331
|
+
"elasticloadbalancing:targetgroup",
|
|
332
|
+
],
|
|
333
|
+
)
|
|
334
|
+
for resource in response["ResourceTagMappingList"]:
|
|
335
|
+
tags = {tag["Key"]: tag["Value"] for tag in resource["Tags"]}
|
|
336
|
+
|
|
337
|
+
if (
|
|
338
|
+
"copilot-service" in tags
|
|
339
|
+
and tags["copilot-service"] == svc
|
|
340
|
+
and "copilot-environment" in tags
|
|
341
|
+
and tags["copilot-environment"] == env
|
|
342
|
+
and "copilot-application" in tags
|
|
343
|
+
and tags["copilot-application"] == app
|
|
344
|
+
):
|
|
345
|
+
return resource["ResourceARN"]
|
|
346
|
+
|
|
347
|
+
click.secho(
|
|
348
|
+
f"No target group found for application: {app}, environment: {env}, service: {svc}",
|
|
349
|
+
fg="red",
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def create_header_rule(
|
|
356
|
+
lb_client: boto3.client,
|
|
357
|
+
listener_arn: str,
|
|
358
|
+
target_group_arn: str,
|
|
359
|
+
header_name: str,
|
|
360
|
+
values: list,
|
|
361
|
+
rule_name: str,
|
|
362
|
+
priority: int,
|
|
363
|
+
):
|
|
364
|
+
conditions = get_host_conditions(lb_client, listener_arn, target_group_arn)
|
|
365
|
+
|
|
366
|
+
# add new condition to existing conditions
|
|
367
|
+
combined_conditions = [
|
|
368
|
+
{
|
|
369
|
+
"Field": "http-header",
|
|
370
|
+
"HttpHeaderConfig": {"HttpHeaderName": header_name, "Values": values},
|
|
371
|
+
}
|
|
372
|
+
] + conditions
|
|
373
|
+
|
|
374
|
+
lb_client.create_rule(
|
|
375
|
+
ListenerArn=listener_arn,
|
|
376
|
+
Priority=priority,
|
|
377
|
+
Conditions=combined_conditions,
|
|
378
|
+
Actions=[{"Type": "forward", "TargetGroupArn": target_group_arn}],
|
|
379
|
+
Tags=[
|
|
380
|
+
{"Key": "name", "Value": rule_name},
|
|
381
|
+
],
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
click.secho(
|
|
385
|
+
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}.",
|
|
386
|
+
fg="green",
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def create_source_ip_rule(
|
|
391
|
+
lb_client: boto3.client,
|
|
392
|
+
listener_arn: str,
|
|
393
|
+
target_group_arn: str,
|
|
394
|
+
values: list,
|
|
395
|
+
rule_name: str,
|
|
396
|
+
priority: int,
|
|
397
|
+
):
|
|
398
|
+
conditions = get_host_conditions(lb_client, listener_arn, target_group_arn)
|
|
399
|
+
|
|
400
|
+
# add new condition to existing conditions
|
|
401
|
+
combined_conditions = [
|
|
402
|
+
{
|
|
403
|
+
"Field": "source-ip",
|
|
404
|
+
"SourceIpConfig": {"Values": [value + "/32" for value in values]},
|
|
405
|
+
}
|
|
406
|
+
] + conditions
|
|
407
|
+
|
|
408
|
+
lb_client.create_rule(
|
|
409
|
+
ListenerArn=listener_arn,
|
|
410
|
+
Priority=priority,
|
|
411
|
+
Conditions=combined_conditions,
|
|
412
|
+
Actions=[{"Type": "forward", "TargetGroupArn": target_group_arn}],
|
|
413
|
+
Tags=[
|
|
414
|
+
{"Key": "name", "Value": rule_name},
|
|
415
|
+
],
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
click.secho(
|
|
419
|
+
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}.",
|
|
420
|
+
fg="green",
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def get_host_conditions(lb_client: boto3.client, listener_arn: str, target_group_arn: str):
|
|
425
|
+
rules = lb_client.describe_rules(ListenerArn=listener_arn)["Rules"]
|
|
426
|
+
|
|
427
|
+
# Get current set of forwarding conditions for the target group
|
|
428
|
+
for rule in rules:
|
|
429
|
+
for action in rule["Actions"]:
|
|
430
|
+
if action["Type"] == "forward" and action["TargetGroupArn"] == target_group_arn:
|
|
431
|
+
conditions = rule["Conditions"]
|
|
432
|
+
|
|
433
|
+
# filter to host-header conditions
|
|
434
|
+
conditions = [
|
|
435
|
+
{i: condition[i] for i in condition if i != "Values"}
|
|
436
|
+
for condition in conditions
|
|
437
|
+
if condition["Field"] == "host-header"
|
|
438
|
+
]
|
|
439
|
+
|
|
440
|
+
# remove internal hosts
|
|
441
|
+
conditions[0]["HostHeaderConfig"]["Values"] = [
|
|
442
|
+
v for v in conditions[0]["HostHeaderConfig"]["Values"]
|
|
443
|
+
]
|
|
444
|
+
|
|
445
|
+
return conditions
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def get_env_ips(vpc: str, application_environment: Environment) -> List[str]:
|
|
449
|
+
account_name = f"{application_environment.session.profile_name}-vpc"
|
|
450
|
+
vpc_name = vpc if vpc else account_name
|
|
451
|
+
ssm_client = application_environment.session.client("ssm")
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
param_value = ssm_client.get_parameter(Name=f"/{vpc_name}/EGRESS_IPS")["Parameter"]["Value"]
|
|
455
|
+
except ssm_client.exceptions.ParameterNotFound:
|
|
456
|
+
click.secho(f"No parameter found with name: /{vpc_name}/EGRESS_IPS")
|
|
457
|
+
raise click.Abort
|
|
458
|
+
|
|
459
|
+
return [ip.strip() for ip in param_value.split(",")]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def find_load_balancer(session: boto3.Session, app: str, env: str) -> str:
|
|
5
|
+
lb_client = session.client("elbv2")
|
|
6
|
+
|
|
7
|
+
describe_response = lb_client.describe_load_balancers()
|
|
8
|
+
load_balancers = [lb["LoadBalancerArn"] for lb in describe_response["LoadBalancers"]]
|
|
9
|
+
|
|
10
|
+
load_balancers = lb_client.describe_tags(ResourceArns=load_balancers)["TagDescriptions"]
|
|
11
|
+
|
|
12
|
+
load_balancer_arn = None
|
|
13
|
+
for lb in load_balancers:
|
|
14
|
+
tags = {t["Key"]: t["Value"] for t in lb["Tags"]}
|
|
15
|
+
if tags.get("copilot-application") == app and tags.get("copilot-environment") == env:
|
|
16
|
+
load_balancer_arn = lb["ResourceArn"]
|
|
17
|
+
|
|
18
|
+
if not load_balancer_arn:
|
|
19
|
+
raise LoadBalancerNotFoundError()
|
|
20
|
+
|
|
21
|
+
return load_balancer_arn
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def find_https_listener(session: boto3.Session, app: str, env: str) -> str:
|
|
25
|
+
load_balancer_arn = find_load_balancer(session, app, env)
|
|
26
|
+
lb_client = session.client("elbv2")
|
|
27
|
+
listeners = lb_client.describe_listeners(LoadBalancerArn=load_balancer_arn)["Listeners"]
|
|
28
|
+
|
|
29
|
+
listener_arn = None
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
listener_arn = next(l["ListenerArn"] for l in listeners if l["Protocol"] == "HTTPS")
|
|
33
|
+
except StopIteration:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
if not listener_arn:
|
|
37
|
+
raise ListenerNotFoundError()
|
|
38
|
+
|
|
39
|
+
return listener_arn
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LoadBalancerNotFoundError(Exception):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ListenerNotFoundError(Exception):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class ListenerRuleNotFoundError(Exception):
|
|
51
|
+
pass
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# {% extra_header %}
|
|
2
|
+
# {% version_info %}
|
|
3
|
+
locals {
|
|
4
|
+
platform_config = yamldecode(file("../../../platform-config.yml"))
|
|
5
|
+
all_pipelines = local.platform_config["environment_pipelines"]
|
|
6
|
+
pipelines = { for pipeline, config in local.platform_config["environment_pipelines"] : pipeline => config if config.account == "{{ aws_account }}" }
|
|
7
|
+
environment_config = local.platform_config["environments"]
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
provider "aws" {
|
|
11
|
+
region = "eu-west-2"
|
|
12
|
+
profile = "{{ aws_account }}"
|
|
13
|
+
alias = "{{ aws_account }}"
|
|
14
|
+
shared_credentials_files = ["~/.aws/config"]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
terraform {
|
|
18
|
+
required_version = "~> 1.8"
|
|
19
|
+
backend "s3" {
|
|
20
|
+
bucket = "terraform-platform-state-{{ aws_account }}"
|
|
21
|
+
key = "tfstate/application/{{ application }}-pipelines.tfstate"
|
|
22
|
+
region = "eu-west-2"
|
|
23
|
+
encrypt = true
|
|
24
|
+
kms_key_id = "alias/terraform-platform-state-s3-key-{{ aws_account }}"
|
|
25
|
+
dynamodb_table = "terraform-platform-lockdb-{{ aws_account }}"
|
|
26
|
+
}
|
|
27
|
+
required_providers {
|
|
28
|
+
aws = {
|
|
29
|
+
source = "hashicorp/aws"
|
|
30
|
+
version = "~> 5"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
module "environment-pipelines" {
|
|
37
|
+
source = "git::https://github.com/uktrade/terraform-platform-modules.git//environment-pipelines?depth=1&ref={{ terraform_platform_modules_version }}"
|
|
38
|
+
|
|
39
|
+
for_each = local.pipelines
|
|
40
|
+
|
|
41
|
+
application = "{{ application }}"
|
|
42
|
+
pipeline_name = each.key
|
|
43
|
+
repository = "uktrade/{{ application }}-deploy"
|
|
44
|
+
|
|
45
|
+
environments = each.value.environments
|
|
46
|
+
all_pipelines = local.all_pipelines
|
|
47
|
+
environment_config = local.environment_config
|
|
48
|
+
branch = {% if deploy_branch %}"{{ deploy_branch }}"{% else %}each.value.branch{% endif %}
|
|
49
|
+
slack_channel = each.value.slack_channel
|
|
50
|
+
trigger_on_push = each.value.trigger_on_push
|
|
51
|
+
pipeline_to_trigger = lookup(each.value, "pipeline_to_trigger", null)
|
|
52
|
+
}
|
dbt_platform_helper/utils/aws.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import Tuple
|
|
|
7
7
|
|
|
8
8
|
import boto3
|
|
9
9
|
import botocore
|
|
10
|
+
import botocore.exceptions
|
|
10
11
|
import click
|
|
11
12
|
import yaml
|
|
12
13
|
from boto3 import Session
|
|
@@ -20,62 +21,71 @@ AWS_SESSION_CACHE = {}
|
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
def get_aws_session_or_abort(aws_profile: str = None) -> boto3.session.Session:
|
|
23
|
-
|
|
24
|
+
REFRESH_TOKEN_MESSAGE = (
|
|
25
|
+
"To refresh this SSO session run `aws sso login` with the corresponding profile"
|
|
26
|
+
)
|
|
27
|
+
aws_profile = aws_profile or os.getenv("AWS_PROFILE")
|
|
24
28
|
if aws_profile in AWS_SESSION_CACHE:
|
|
25
29
|
return AWS_SESSION_CACHE[aws_profile]
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
click.secho(f"""Checking AWS connection for profile "{aws_profile}"...""", fg="cyan")
|
|
31
|
+
click.secho(f'Checking AWS connection for profile "{aws_profile}"...', fg="cyan")
|
|
29
32
|
|
|
30
33
|
try:
|
|
31
34
|
session = boto3.session.Session(profile_name=aws_profile)
|
|
35
|
+
sts = session.client("sts")
|
|
36
|
+
account_id, user_id = get_account_details(sts)
|
|
37
|
+
click.secho("Credentials are valid.", fg="green")
|
|
38
|
+
|
|
32
39
|
except botocore.exceptions.ProfileNotFound:
|
|
33
|
-
|
|
34
|
-
exit(1)
|
|
40
|
+
_handle_error(f'AWS profile "{aws_profile}" is not configured.')
|
|
35
41
|
except botocore.exceptions.ClientError as e:
|
|
36
42
|
if e.response["Error"]["Code"] == "ExpiredToken":
|
|
37
|
-
|
|
38
|
-
f"Credentials are NOT valid. \nPlease login with: aws sso login --profile {aws_profile}"
|
|
39
|
-
fg="red",
|
|
43
|
+
_handle_error(
|
|
44
|
+
f"Credentials are NOT valid. \nPlease login with: aws sso login --profile {aws_profile}"
|
|
40
45
|
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
except
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
):
|
|
52
|
-
click.secho(
|
|
53
|
-
"The SSO session associated with this profile has expired or is otherwise invalid."
|
|
54
|
-
"To refresh this SSO session run `aws sso login` with the corresponding profile",
|
|
55
|
-
fg="red",
|
|
46
|
+
except botocore.exceptions.NoCredentialsError:
|
|
47
|
+
_handle_error("There are no credentials set for this session.", REFRESH_TOKEN_MESSAGE)
|
|
48
|
+
except botocore.exceptions.UnauthorizedSSOTokenError:
|
|
49
|
+
_handle_error("The SSO Token used for this session is unauthorised.", REFRESH_TOKEN_MESSAGE)
|
|
50
|
+
except botocore.exceptions.TokenRetrievalError:
|
|
51
|
+
_handle_error("Unable to retrieve the Token for this session.", REFRESH_TOKEN_MESSAGE)
|
|
52
|
+
except botocore.exceptions.SSOTokenLoadError:
|
|
53
|
+
_handle_error(
|
|
54
|
+
"The SSO session associated with this profile has expired, is not set or is otherwise invalid.",
|
|
55
|
+
REFRESH_TOKEN_MESSAGE,
|
|
56
56
|
)
|
|
57
|
-
exit(1)
|
|
58
57
|
|
|
59
58
|
alias_client = session.client("iam")
|
|
60
|
-
account_name = alias_client.list_account_aliases()
|
|
59
|
+
account_name = alias_client.list_account_aliases().get("AccountAliases", [])
|
|
60
|
+
|
|
61
|
+
_log_account_info(account_name, account_id)
|
|
62
|
+
|
|
63
|
+
click.echo(
|
|
64
|
+
click.style("User: ", fg="yellow")
|
|
65
|
+
+ click.style(f"{user_id.split(':')[-1]}\n", fg="white", bold=True)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
AWS_SESSION_CACHE[aws_profile] = session
|
|
69
|
+
return session
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _handle_error(message: str, refresh_token_message: str = None) -> None:
|
|
73
|
+
full_message = message + (" " + refresh_token_message if refresh_token_message else "")
|
|
74
|
+
click.secho(full_message, fg="red")
|
|
75
|
+
exit(1)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _log_account_info(account_name: list, account_id: str) -> None:
|
|
61
79
|
if account_name:
|
|
62
80
|
click.echo(
|
|
63
81
|
click.style("Logged in with AWS account: ", fg="yellow")
|
|
64
|
-
+ click.style(f"{account_name[0]}/{account_id}", fg="white", bold=True)
|
|
82
|
+
+ click.style(f"{account_name[0]}/{account_id}", fg="white", bold=True)
|
|
65
83
|
)
|
|
66
84
|
else:
|
|
67
85
|
click.echo(
|
|
68
86
|
click.style("Logged in with AWS account id: ", fg="yellow")
|
|
69
|
-
+ click.style(f"{account_id}", fg="white", bold=True)
|
|
87
|
+
+ click.style(f"{account_id}", fg="white", bold=True)
|
|
70
88
|
)
|
|
71
|
-
click.echo(
|
|
72
|
-
click.style("User: ", fg="yellow")
|
|
73
|
-
+ click.style(f"{user_id.split(':')[-1]}\n", fg="white", bold=True),
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
AWS_SESSION_CACHE[aws_profile] = session
|
|
77
|
-
|
|
78
|
-
return session
|
|
79
89
|
|
|
80
90
|
|
|
81
91
|
class NoProfileForAccountIdError(Exception):
|
|
@@ -362,12 +372,12 @@ def get_connection_string(
|
|
|
362
372
|
|
|
363
373
|
|
|
364
374
|
class Vpc:
|
|
365
|
-
def __init__(self, subnets, security_groups):
|
|
375
|
+
def __init__(self, subnets: list[str], security_groups: list[str]):
|
|
366
376
|
self.subnets = subnets
|
|
367
377
|
self.security_groups = security_groups
|
|
368
378
|
|
|
369
379
|
|
|
370
|
-
def get_vpc_info_by_name(session, app, env, vpc_name):
|
|
380
|
+
def get_vpc_info_by_name(session: Session, app: str, env: str, vpc_name: str) -> Vpc:
|
|
371
381
|
ec2_client = session.client("ec2")
|
|
372
382
|
vpc_response = ec2_client.describe_vpcs(Filters=[{"Name": "tag:Name", "Values": [vpc_name]}])
|
|
373
383
|
|