dbt-platform-helper 11.0.0__py3-none-any.whl → 11.1.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.

@@ -0,0 +1,220 @@
1
+ import re
2
+ from collections.abc import Callable
3
+ from pathlib import Path
4
+
5
+ import boto3
6
+ import click
7
+ from boto3 import Session
8
+
9
+ from dbt_platform_helper.constants import PLATFORM_CONFIG_FILE
10
+ from dbt_platform_helper.domain.maintenance_page import MaintenancePageProvider
11
+ from dbt_platform_helper.exceptions import AWSException
12
+ from dbt_platform_helper.utils.application import Application
13
+ from dbt_platform_helper.utils.application import ApplicationNotFoundError
14
+ from dbt_platform_helper.utils.application import load_application
15
+ from dbt_platform_helper.utils.aws import Vpc
16
+ from dbt_platform_helper.utils.aws import get_connection_string
17
+ from dbt_platform_helper.utils.aws import get_vpc_info_by_name
18
+ from dbt_platform_helper.utils.messages import abort_with_error
19
+ from dbt_platform_helper.utils.validation import load_and_validate_platform_config
20
+
21
+
22
+ class DatabaseCopy:
23
+ def __init__(
24
+ self,
25
+ app: str,
26
+ database: str,
27
+ auto_approve: bool = False,
28
+ load_application_fn: Callable[[str], Application] = load_application,
29
+ vpc_config_fn: Callable[[Session, str, str, str], Vpc] = get_vpc_info_by_name,
30
+ db_connection_string_fn: Callable[
31
+ [Session, str, str, str, Callable], str
32
+ ] = get_connection_string,
33
+ maintenance_page_provider: Callable[
34
+ [str, str, list[str], str, str], None
35
+ ] = MaintenancePageProvider(),
36
+ input_fn: Callable[[str], str] = click.prompt,
37
+ echo_fn: Callable[[str], str] = click.secho,
38
+ abort_fn: Callable[[str], None] = abort_with_error,
39
+ ):
40
+ self.app = app
41
+ self.database = database
42
+ self.auto_approve = auto_approve
43
+ self.vpc_config_fn = vpc_config_fn
44
+ self.db_connection_string_fn = db_connection_string_fn
45
+ self.maintenance_page_provider = maintenance_page_provider
46
+ self.input_fn = input_fn
47
+ self.echo_fn = echo_fn
48
+ self.abort_fn = abort_fn
49
+
50
+ if not self.app:
51
+ if not Path(PLATFORM_CONFIG_FILE).exists():
52
+ self.abort_fn("You must either be in a deploy repo, or provide the --app option.")
53
+
54
+ config = load_and_validate_platform_config(disable_aws_validation=True)
55
+ self.app = config["application"]
56
+
57
+ try:
58
+ self.application = load_application_fn(self.app)
59
+ except ApplicationNotFoundError:
60
+ abort_fn(f"No such application '{app}'.")
61
+
62
+ def _execute_operation(self, is_dump: bool, env: str, vpc_name: str):
63
+ vpc_name = self.enrich_vpc_name(env, vpc_name)
64
+
65
+ environments = self.application.environments
66
+ environment = environments.get(env)
67
+ if not environment:
68
+ self.abort_fn(
69
+ f"No such environment '{env}'. Available environments are: {', '.join(environments.keys())}"
70
+ )
71
+
72
+ env_session = environment.session
73
+
74
+ try:
75
+ vpc_config = self.vpc_config_fn(env_session, self.app, env, vpc_name)
76
+ except AWSException as ex:
77
+ self.abort_fn(str(ex))
78
+
79
+ database_identifier = f"{self.app}-{env}-{self.database}"
80
+
81
+ try:
82
+ db_connection_string = self.db_connection_string_fn(
83
+ env_session, self.app, env, database_identifier
84
+ )
85
+ except Exception as exc:
86
+ self.abort_fn(f"{exc} (Database: {database_identifier})")
87
+
88
+ try:
89
+ task_arn = self.run_database_copy_task(
90
+ env_session, env, vpc_config, is_dump, db_connection_string
91
+ )
92
+ except Exception as exc:
93
+ self.abort_fn(f"{exc} (Account id: {self.account_id(env)})")
94
+
95
+ if is_dump:
96
+ message = f"Dumping {self.database} from the {env} environment into S3"
97
+ else:
98
+ message = f"Loading data into {self.database} in the {env} environment from S3"
99
+
100
+ self.echo_fn(message, fg="white", bold=True)
101
+ self.echo_fn(
102
+ f"Task {task_arn} started. Waiting for it to complete (this may take some time)...",
103
+ fg="white",
104
+ )
105
+ self.tail_logs(is_dump, env)
106
+
107
+ def enrich_vpc_name(self, env, vpc_name):
108
+ if not vpc_name:
109
+ if not Path(PLATFORM_CONFIG_FILE).exists():
110
+ self.abort_fn(
111
+ "You must either be in a deploy repo, or provide the vpc name option."
112
+ )
113
+ config = load_and_validate_platform_config(disable_aws_validation=True)
114
+ vpc_name = config.get("environments", {}).get(env, {}).get("vpc")
115
+ return vpc_name
116
+
117
+ def run_database_copy_task(
118
+ self,
119
+ session: boto3.session.Session,
120
+ env: str,
121
+ vpc_config: Vpc,
122
+ is_dump: bool,
123
+ db_connection_string: str,
124
+ ) -> str:
125
+ client = session.client("ecs")
126
+ action = "dump" if is_dump else "load"
127
+ env_vars = [
128
+ {"name": "DATA_COPY_OPERATION", "value": action.upper()},
129
+ {"name": "DB_CONNECTION_STRING", "value": db_connection_string},
130
+ ]
131
+ if not is_dump:
132
+ env_vars.append({"name": "ECS_CLUSTER", "value": f"{self.app}-{env}"})
133
+
134
+ response = client.run_task(
135
+ taskDefinition=f"arn:aws:ecs:eu-west-2:{self.account_id(env)}:task-definition/{self.app}-{env}-{self.database}-{action}",
136
+ cluster=f"{self.app}-{env}",
137
+ capacityProviderStrategy=[
138
+ {"capacityProvider": "FARGATE", "weight": 1, "base": 0},
139
+ ],
140
+ networkConfiguration={
141
+ "awsvpcConfiguration": {
142
+ "subnets": vpc_config.subnets,
143
+ "securityGroups": vpc_config.security_groups,
144
+ "assignPublicIp": "DISABLED",
145
+ }
146
+ },
147
+ overrides={
148
+ "containerOverrides": [
149
+ {
150
+ "name": f"{self.app}-{env}-{self.database}-{action}",
151
+ "environment": env_vars,
152
+ }
153
+ ]
154
+ },
155
+ )
156
+
157
+ return response.get("tasks", [{}])[0].get("taskArn")
158
+
159
+ def dump(self, env: str, vpc_name: str):
160
+ self._execute_operation(True, env, vpc_name)
161
+
162
+ def load(self, env: str, vpc_name: str):
163
+ if self.is_confirmed_ready_to_load(env):
164
+ self._execute_operation(False, env, vpc_name)
165
+
166
+ def copy(
167
+ self,
168
+ from_env: str,
169
+ to_env: str,
170
+ from_vpc: str,
171
+ to_vpc: str,
172
+ services: tuple[str],
173
+ template: str,
174
+ no_maintenance_page: bool = False,
175
+ ):
176
+ to_vpc = self.enrich_vpc_name(to_env, to_vpc)
177
+ if not no_maintenance_page:
178
+ self.maintenance_page_provider.activate(self.app, to_env, services, template, to_vpc)
179
+ self.dump(from_env, from_vpc)
180
+ self.load(to_env, to_vpc)
181
+ if not no_maintenance_page:
182
+ self.maintenance_page_provider.deactivate(self.app, to_env)
183
+
184
+ def is_confirmed_ready_to_load(self, env: str) -> bool:
185
+ if self.auto_approve:
186
+ return True
187
+
188
+ user_input = self.input_fn(
189
+ f"\nWARNING: the load operation is destructive and will delete the {self.database} database in the {env} environment. Continue? (y/n)"
190
+ )
191
+ return user_input.lower().strip() in ["y", "yes"]
192
+
193
+ def tail_logs(self, is_dump: bool, env: str):
194
+ action = "dump" if is_dump else "load"
195
+ log_group_name = f"/ecs/{self.app}-{env}-{self.database}-{action}"
196
+ log_group_arn = f"arn:aws:logs:eu-west-2:{self.account_id(env)}:log-group:{log_group_name}"
197
+ self.echo_fn(f"Tailing {log_group_name} logs", fg="yellow")
198
+ session = self.application.environments[env].session
199
+ response = session.client("logs").start_live_tail(logGroupIdentifiers=[log_group_arn])
200
+
201
+ stopped = False
202
+ for data in response["responseStream"]:
203
+ if stopped:
204
+ break
205
+ results = data.get("sessionUpdate", {}).get("sessionResults", [])
206
+ for result in results:
207
+ message = result.get("message")
208
+
209
+ if message:
210
+ match = re.match(r"(Stopping|Aborting) data (load|dump).*", message)
211
+ if match:
212
+ if match.group(1) == "Aborting":
213
+ self.abort_fn("Task aborted abnormally. See logs above for details.")
214
+ stopped = True
215
+ self.echo_fn(message)
216
+
217
+ def account_id(self, env):
218
+ envs = self.application.environments
219
+ if env in envs:
220
+ return envs.get(env).account_id
@@ -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(",")]