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
|
@@ -8,18 +8,17 @@ from typing import Callable
|
|
|
8
8
|
from typing import List
|
|
9
9
|
from typing import Union
|
|
10
10
|
|
|
11
|
-
import boto3
|
|
12
11
|
import click
|
|
13
12
|
|
|
14
13
|
from dbt_platform_helper.platform_exception import PlatformException
|
|
15
14
|
from dbt_platform_helper.providers.io import ClickIOProvider
|
|
16
|
-
from dbt_platform_helper.providers.load_balancers import ListenerNotFoundException
|
|
17
15
|
from dbt_platform_helper.providers.load_balancers import ListenerRuleNotFoundException
|
|
18
|
-
from dbt_platform_helper.providers.load_balancers import
|
|
19
|
-
from dbt_platform_helper.providers.load_balancers import (
|
|
20
|
-
get_https_listener_for_application,
|
|
21
|
-
)
|
|
16
|
+
from dbt_platform_helper.providers.load_balancers import LoadBalancerProvider
|
|
22
17
|
from dbt_platform_helper.utils.application import Application
|
|
18
|
+
from dbt_platform_helper.utils.application import (
|
|
19
|
+
ApplicationEnvironmentNotFoundException,
|
|
20
|
+
)
|
|
21
|
+
from dbt_platform_helper.utils.application import ApplicationServiceNotFoundException
|
|
23
22
|
from dbt_platform_helper.utils.application import Environment
|
|
24
23
|
from dbt_platform_helper.utils.application import Service
|
|
25
24
|
|
|
@@ -39,37 +38,17 @@ class FailedToActivateMaintenancePageException(MaintenancePageException):
|
|
|
39
38
|
application_name: str,
|
|
40
39
|
env: str,
|
|
41
40
|
original_exception: Exception,
|
|
42
|
-
rolled_back_rules: dict[str, bool] = {},
|
|
43
41
|
):
|
|
44
42
|
super().__init__(
|
|
45
43
|
f"Maintenance page failed to activate for the {application_name} application in environment {env}."
|
|
46
44
|
)
|
|
47
45
|
self.orginal_exception = original_exception
|
|
48
|
-
self.rolled_back_rules = rolled_back_rules
|
|
49
46
|
|
|
50
47
|
def __str__(self):
|
|
51
|
-
return (
|
|
52
|
-
f"{super().__str__()}\n"
|
|
53
|
-
f"Rolled-back rules: {self.rolled_back_rules }\n"
|
|
54
|
-
f"Original exception: {self.orginal_exception}"
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def get_maintenance_page_type(session: boto3.Session, listener_arn: str) -> Union[str, None]:
|
|
59
|
-
lb_client = session.client("elbv2")
|
|
60
|
-
|
|
61
|
-
rules = lb_client.describe_rules(ListenerArn=listener_arn)["Rules"]
|
|
62
|
-
tag_descriptions = get_rules_tag_descriptions(rules, lb_client)
|
|
63
|
-
|
|
64
|
-
maintenance_page_type = None
|
|
65
|
-
for description in tag_descriptions:
|
|
66
|
-
tags = {t["Key"]: t["Value"] for t in description["Tags"]}
|
|
67
|
-
if tags.get("name") == "MaintenancePage":
|
|
68
|
-
maintenance_page_type = tags.get("type")
|
|
69
|
-
|
|
70
|
-
return maintenance_page_type
|
|
48
|
+
return f"{super().__str__()}\n" f"Original exception: {self.orginal_exception}"
|
|
71
49
|
|
|
72
50
|
|
|
51
|
+
# TODO should this be in its own provider, inside the VPC one, what logic is this sepcific too?
|
|
73
52
|
def get_env_ips(vpc: str, application_environment: Environment) -> List[str]:
|
|
74
53
|
account_name = f"{application_environment.session.profile_name}-vpc"
|
|
75
54
|
vpc_name = vpc if vpc else account_name
|
|
@@ -84,164 +63,19 @@ def get_env_ips(vpc: str, application_environment: Environment) -> List[str]:
|
|
|
84
63
|
return [ip.strip() for ip in param_value.split(",")]
|
|
85
64
|
|
|
86
65
|
|
|
87
|
-
def add_maintenance_page(
|
|
88
|
-
session: boto3.Session,
|
|
89
|
-
listener_arn: str,
|
|
90
|
-
app: str,
|
|
91
|
-
env: str,
|
|
92
|
-
services: List[Service],
|
|
93
|
-
allowed_ips: List[str],
|
|
94
|
-
template: str = "default",
|
|
95
|
-
):
|
|
96
|
-
lb_client = session.client("elbv2")
|
|
97
|
-
maintenance_page_content = get_maintenance_page_template(template)
|
|
98
|
-
bypass_value = "".join(random.choices(string.ascii_lowercase + string.digits, k=12))
|
|
99
|
-
|
|
100
|
-
rule_priority = itertools.count(start=1)
|
|
101
|
-
maintenance_page_host_header_conditions = []
|
|
102
|
-
try:
|
|
103
|
-
for svc in services:
|
|
104
|
-
target_group_arn = find_target_group(app, env, svc.name, session)
|
|
105
|
-
|
|
106
|
-
# not all of an application's services are guaranteed to have been deployed to an environment
|
|
107
|
-
if not target_group_arn:
|
|
108
|
-
continue
|
|
109
|
-
|
|
110
|
-
service_conditions = get_host_header_conditions(
|
|
111
|
-
lb_client, listener_arn, target_group_arn
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
for ip in allowed_ips:
|
|
115
|
-
create_header_rule(
|
|
116
|
-
lb_client,
|
|
117
|
-
listener_arn,
|
|
118
|
-
target_group_arn,
|
|
119
|
-
"X-Forwarded-For",
|
|
120
|
-
[ip],
|
|
121
|
-
"AllowedIps",
|
|
122
|
-
next(rule_priority),
|
|
123
|
-
service_conditions,
|
|
124
|
-
)
|
|
125
|
-
create_source_ip_rule(
|
|
126
|
-
lb_client,
|
|
127
|
-
listener_arn,
|
|
128
|
-
target_group_arn,
|
|
129
|
-
[ip],
|
|
130
|
-
"AllowedSourceIps",
|
|
131
|
-
next(rule_priority),
|
|
132
|
-
service_conditions,
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
create_header_rule(
|
|
136
|
-
lb_client,
|
|
137
|
-
listener_arn,
|
|
138
|
-
target_group_arn,
|
|
139
|
-
"Bypass-Key",
|
|
140
|
-
[bypass_value],
|
|
141
|
-
"BypassIpFilter",
|
|
142
|
-
next(rule_priority),
|
|
143
|
-
service_conditions,
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
# add to accumilating list of conditions for maintenace page rule
|
|
147
|
-
maintenance_page_host_header_conditions.extend(service_conditions)
|
|
148
|
-
|
|
149
|
-
click.secho(
|
|
150
|
-
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/next-steps/put-a-service-under-maintenance/",
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
lb_client.create_rule(
|
|
154
|
-
ListenerArn=listener_arn,
|
|
155
|
-
Priority=next(rule_priority),
|
|
156
|
-
Conditions=[
|
|
157
|
-
{
|
|
158
|
-
"Field": "path-pattern",
|
|
159
|
-
"PathPatternConfig": {"Values": ["/*"]},
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
"Field": "host-header",
|
|
163
|
-
"HostHeaderConfig": {
|
|
164
|
-
"Values": sorted(
|
|
165
|
-
list(
|
|
166
|
-
{
|
|
167
|
-
value
|
|
168
|
-
for condition in maintenance_page_host_header_conditions
|
|
169
|
-
for value in condition["HostHeaderConfig"]["Values"]
|
|
170
|
-
}
|
|
171
|
-
)
|
|
172
|
-
)
|
|
173
|
-
},
|
|
174
|
-
},
|
|
175
|
-
],
|
|
176
|
-
Actions=[
|
|
177
|
-
{
|
|
178
|
-
"Type": "fixed-response",
|
|
179
|
-
"FixedResponseConfig": {
|
|
180
|
-
"StatusCode": "503",
|
|
181
|
-
"ContentType": "text/html",
|
|
182
|
-
"MessageBody": maintenance_page_content,
|
|
183
|
-
},
|
|
184
|
-
}
|
|
185
|
-
],
|
|
186
|
-
Tags=[
|
|
187
|
-
{"Key": "name", "Value": "MaintenancePage"},
|
|
188
|
-
{"Key": "type", "Value": template},
|
|
189
|
-
],
|
|
190
|
-
)
|
|
191
|
-
except Exception as e:
|
|
192
|
-
deleted_rules = clean_up_maintenance_page_rules(session, listener_arn)
|
|
193
|
-
raise FailedToActivateMaintenancePageException(
|
|
194
|
-
app, env, f"{e}:\n {traceback.format_exc()}", deleted_rules
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def clean_up_maintenance_page_rules(
|
|
199
|
-
session: boto3.Session, listener_arn: str, fail_when_not_deleted: bool = False
|
|
200
|
-
) -> dict[str, bool]:
|
|
201
|
-
lb_client = session.client("elbv2")
|
|
202
|
-
|
|
203
|
-
rules = lb_client.describe_rules(ListenerArn=listener_arn)["Rules"]
|
|
204
|
-
tag_descriptions = get_rules_tag_descriptions(rules, lb_client)
|
|
205
|
-
|
|
206
|
-
deletes = {}
|
|
207
|
-
for name in ["MaintenancePage", "AllowedIps", "BypassIpFilter", "AllowedSourceIps"]:
|
|
208
|
-
deleted = delete_listener_rule(tag_descriptions, name, lb_client)
|
|
209
|
-
deletes[name] = bool(deleted)
|
|
210
|
-
if fail_when_not_deleted and name == "MaintenancePage" and not deleted:
|
|
211
|
-
raise ListenerRuleNotFoundException()
|
|
212
|
-
|
|
213
|
-
return deletes
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def remove_maintenance_page(session: boto3.Session, listener_arn: str):
|
|
217
|
-
clean_up_maintenance_page_rules(session, listener_arn, True)
|
|
218
|
-
|
|
219
|
-
|
|
220
66
|
class MaintenancePage:
|
|
221
67
|
def __init__(
|
|
222
68
|
self,
|
|
223
69
|
application: Application,
|
|
224
70
|
io: ClickIOProvider = ClickIOProvider(),
|
|
225
|
-
|
|
226
|
-
[boto3.Session, str, str], str
|
|
227
|
-
] = get_https_listener_for_application,
|
|
228
|
-
# TODO refactor get_maintenance_page_type, add_maintenance_page, remove_maintenance_page into MaintenancePage class with LoadBalancerProvider as the dependency
|
|
229
|
-
get_maintenance_page_type: Callable[
|
|
230
|
-
[boto3.Session, str], Union[str, None]
|
|
231
|
-
] = get_maintenance_page_type,
|
|
71
|
+
load_balancer_provider: LoadBalancerProvider = LoadBalancerProvider,
|
|
232
72
|
get_env_ips: Callable[[str, Environment], List[str]] = get_env_ips,
|
|
233
|
-
add_maintenance_page: Callable[
|
|
234
|
-
[boto3.Session, str, str, str, List[Service], tuple, str], None
|
|
235
|
-
] = add_maintenance_page,
|
|
236
|
-
remove_maintenance_page: Callable[[boto3.Session, str], None] = remove_maintenance_page,
|
|
237
73
|
):
|
|
238
74
|
self.application = application
|
|
239
|
-
self.get_https_listener_for_application = get_https_listener_for_application
|
|
240
75
|
self.io = io
|
|
241
|
-
self.
|
|
76
|
+
self.load_balancer_provider = load_balancer_provider # TODO requires session from environment in application object which is only known during method execution
|
|
77
|
+
self.load_balancer: LoadBalancerProvider = None
|
|
242
78
|
self.get_env_ips = get_env_ips
|
|
243
|
-
self.add_maintenance_page = add_maintenance_page
|
|
244
|
-
self.remove_maintenance_page = remove_maintenance_page
|
|
245
79
|
|
|
246
80
|
def _get_deployed_load_balanced_web_services(self, app: Application, svc: List[str]):
|
|
247
81
|
if "*" in svc:
|
|
@@ -253,155 +87,262 @@ class MaintenancePage:
|
|
|
253
87
|
raise LoadBalancedWebServiceNotFoundException(app.name)
|
|
254
88
|
return services
|
|
255
89
|
|
|
90
|
+
# TODO: inject load balancer provider in activate method to avoid passing load balancer provider in init?
|
|
256
91
|
def activate(self, env: str, services: List[str], template: str, vpc: Union[str, None]):
|
|
257
92
|
|
|
258
93
|
services = self._get_deployed_load_balanced_web_services(self.application, services)
|
|
259
94
|
application_environment = get_app_environment(self.application, env)
|
|
95
|
+
self.load_balancer = self.load_balancer_provider(application_environment.session)
|
|
260
96
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
97
|
+
https_listener = self.load_balancer.get_https_listener_for_application(
|
|
98
|
+
self.application.name, env
|
|
99
|
+
)
|
|
100
|
+
current_maintenance_page = self.__get_maintenance_page_type(https_listener)
|
|
101
|
+
remove_current_maintenance_page = False
|
|
102
|
+
if current_maintenance_page:
|
|
103
|
+
remove_current_maintenance_page = self.io.confirm(
|
|
104
|
+
f"There is currently a '{current_maintenance_page}' maintenance page for the {env} "
|
|
105
|
+
f"environment in {self.application.name}.\nWould you like to replace it with a '{template}' "
|
|
106
|
+
f"maintenance page?"
|
|
267
107
|
)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
remove_current_maintenance_page = self.io.confirm(
|
|
271
|
-
f"There is currently a '{current_maintenance_page}' maintenance page for the {env} "
|
|
272
|
-
f"environment in {self.application.name}.\nWould you like to replace it with a '{template}' "
|
|
273
|
-
f"maintenance page?"
|
|
274
|
-
)
|
|
275
|
-
if not remove_current_maintenance_page:
|
|
276
|
-
return
|
|
277
|
-
|
|
278
|
-
if remove_current_maintenance_page or self.io.confirm(
|
|
279
|
-
f"You are about to enable the '{template}' maintenance page for the {env} "
|
|
280
|
-
f"environment in {self.application.name}.\nWould you like to continue?"
|
|
281
|
-
):
|
|
282
|
-
if current_maintenance_page and remove_current_maintenance_page:
|
|
283
|
-
self.remove_maintenance_page(application_environment.session, https_listener)
|
|
284
|
-
|
|
285
|
-
allowed_ips = self.get_env_ips(vpc, application_environment)
|
|
286
|
-
|
|
287
|
-
self.add_maintenance_page(
|
|
288
|
-
application_environment.session,
|
|
289
|
-
https_listener,
|
|
290
|
-
self.application.name,
|
|
291
|
-
env,
|
|
292
|
-
services,
|
|
293
|
-
allowed_ips,
|
|
294
|
-
template,
|
|
295
|
-
)
|
|
296
|
-
self.io.info(
|
|
297
|
-
f"Maintenance page '{template}' added for environment {env} in application {self.application.name}",
|
|
298
|
-
)
|
|
108
|
+
if not remove_current_maintenance_page:
|
|
109
|
+
return
|
|
299
110
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
self.
|
|
303
|
-
|
|
111
|
+
if remove_current_maintenance_page or self.io.confirm(
|
|
112
|
+
f"You are about to enable the '{template}' maintenance page for the {env} "
|
|
113
|
+
f"environment in {self.application.name}.\nWould you like to continue?"
|
|
114
|
+
):
|
|
115
|
+
if current_maintenance_page and remove_current_maintenance_page:
|
|
116
|
+
self.__remove_maintenance_page(https_listener)
|
|
117
|
+
|
|
118
|
+
allowed_ips = self.get_env_ips(vpc, application_environment)
|
|
119
|
+
|
|
120
|
+
self.add_maintenance_page(
|
|
121
|
+
https_listener,
|
|
122
|
+
self.application.name,
|
|
123
|
+
env,
|
|
124
|
+
services,
|
|
125
|
+
allowed_ips,
|
|
126
|
+
template,
|
|
304
127
|
)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
# TODO push exception to command layer
|
|
308
|
-
self.io.abort_with_error(
|
|
309
|
-
f"No HTTPS listener found for environment {env} in the application {self.application.name}.",
|
|
128
|
+
self.io.info(
|
|
129
|
+
f"Maintenance page '{template}' added for environment {env} in application {self.application.name}",
|
|
310
130
|
)
|
|
311
131
|
|
|
312
132
|
def deactivate(self, env: str):
|
|
313
133
|
application_environment = get_app_environment(self.application, env)
|
|
314
134
|
|
|
135
|
+
self.load_balancer = self.load_balancer_provider(application_environment.session)
|
|
136
|
+
|
|
137
|
+
https_listener = self.load_balancer.get_https_listener_for_application(
|
|
138
|
+
self.application.name, env
|
|
139
|
+
)
|
|
140
|
+
current_maintenance_page = self.__get_maintenance_page_type(https_listener)
|
|
141
|
+
|
|
142
|
+
if not current_maintenance_page:
|
|
143
|
+
self.io.warn("There is no current maintenance page to remove")
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
if not self.io.confirm(
|
|
147
|
+
f"There is currently a '{current_maintenance_page}' maintenance page, "
|
|
148
|
+
f"would you like to remove it?"
|
|
149
|
+
):
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
self.__remove_maintenance_page(https_listener)
|
|
153
|
+
self.io.info(
|
|
154
|
+
f"Maintenance page removed from environment {env} in application {self.application.name}",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def add_maintenance_page(
|
|
158
|
+
self,
|
|
159
|
+
listener_arn: str,
|
|
160
|
+
app: str,
|
|
161
|
+
env: str,
|
|
162
|
+
services: List[Service],
|
|
163
|
+
allowed_ips: List[str],
|
|
164
|
+
template: str = "default",
|
|
165
|
+
):
|
|
166
|
+
|
|
167
|
+
maintenance_page_content = get_maintenance_page_template(template)
|
|
168
|
+
bypass_value = "".join(random.choices(string.ascii_lowercase + string.digits, k=12))
|
|
169
|
+
|
|
170
|
+
rule_priority = itertools.count(start=1)
|
|
171
|
+
maintenance_page_host_header_conditions = []
|
|
315
172
|
try:
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
)
|
|
319
|
-
current_maintenance_page = self.get_maintenance_page_type(
|
|
320
|
-
application_environment.session, https_listener
|
|
321
|
-
)
|
|
173
|
+
for svc in services:
|
|
174
|
+
target_group_arn = self.load_balancer.find_target_group(app, env, svc.name)
|
|
322
175
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return
|
|
176
|
+
# not all of an application's services are guaranteed to have been deployed to an environment
|
|
177
|
+
if not target_group_arn:
|
|
178
|
+
continue
|
|
327
179
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
180
|
+
service_conditions = self.load_balancer.get_host_header_conditions(
|
|
181
|
+
listener_arn, target_group_arn
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
self.io.debug(
|
|
185
|
+
f"""
|
|
186
|
+
#----------------------------------------------------------#
|
|
187
|
+
# Creating listener rules for service {svc.name.ljust(21, " ")}#
|
|
188
|
+
#----------------------------------------------------------#
|
|
189
|
+
|
|
190
|
+
""",
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
for ip in allowed_ips:
|
|
194
|
+
self.load_balancer.create_header_rule(
|
|
195
|
+
listener_arn,
|
|
196
|
+
target_group_arn,
|
|
197
|
+
"X-Forwarded-For",
|
|
198
|
+
[ip],
|
|
199
|
+
"AllowedIps",
|
|
200
|
+
next(rule_priority),
|
|
201
|
+
service_conditions,
|
|
202
|
+
[{"Key": "service", "Value": svc.name}],
|
|
203
|
+
)
|
|
204
|
+
self.load_balancer.create_source_ip_rule(
|
|
205
|
+
listener_arn,
|
|
206
|
+
target_group_arn,
|
|
207
|
+
[ip],
|
|
208
|
+
"AllowedSourceIps",
|
|
209
|
+
next(rule_priority),
|
|
210
|
+
service_conditions,
|
|
211
|
+
[{"Key": "service", "Value": svc.name}],
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
self.load_balancer.create_header_rule(
|
|
215
|
+
listener_arn,
|
|
216
|
+
target_group_arn,
|
|
217
|
+
"Bypass-Key",
|
|
218
|
+
[bypass_value],
|
|
219
|
+
"BypassIpFilter",
|
|
220
|
+
next(rule_priority),
|
|
221
|
+
service_conditions,
|
|
222
|
+
[{"Key": "service", "Value": svc.name}],
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# add to accumilating list of conditions for maintenace page rule
|
|
226
|
+
maintenance_page_host_header_conditions.extend(service_conditions)
|
|
333
227
|
|
|
334
|
-
self.remove_maintenance_page(application_environment.session, https_listener)
|
|
335
228
|
self.io.info(
|
|
336
|
-
f"
|
|
229
|
+
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/next-steps/put-a-service-under-maintenance/",
|
|
337
230
|
)
|
|
338
231
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
232
|
+
self.load_balancer.create_rule(
|
|
233
|
+
listener_arn=listener_arn,
|
|
234
|
+
priority=next(rule_priority),
|
|
235
|
+
conditions=[
|
|
236
|
+
{
|
|
237
|
+
"Field": "path-pattern",
|
|
238
|
+
"PathPatternConfig": {"Values": ["/*"]},
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
"Field": "host-header",
|
|
242
|
+
"HostHeaderConfig": {
|
|
243
|
+
"Values": sorted(
|
|
244
|
+
list(
|
|
245
|
+
{
|
|
246
|
+
value
|
|
247
|
+
for condition in maintenance_page_host_header_conditions
|
|
248
|
+
for value in condition["HostHeaderConfig"]["Values"]
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
actions=[
|
|
256
|
+
{
|
|
257
|
+
"Type": "fixed-response",
|
|
258
|
+
"FixedResponseConfig": {
|
|
259
|
+
"StatusCode": "503",
|
|
260
|
+
"ContentType": "text/html",
|
|
261
|
+
"MessageBody": maintenance_page_content,
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
],
|
|
265
|
+
tags=[
|
|
266
|
+
{"Key": "name", "Value": "MaintenancePage"},
|
|
267
|
+
{"Key": "type", "Value": template},
|
|
268
|
+
],
|
|
343
269
|
)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
f"No HTTPS listener found for environment {env} in the application {self.application.name}.",
|
|
270
|
+
except Exception as e:
|
|
271
|
+
self.__clean_up_maintenance_page_rules(listener_arn)
|
|
272
|
+
raise FailedToActivateMaintenancePageException(
|
|
273
|
+
app, env, f"{e}:\n {traceback.format_exc()}"
|
|
349
274
|
)
|
|
350
275
|
|
|
276
|
+
def __clean_up_maintenance_page_rules(
|
|
277
|
+
self, listener_arn: str, fail_when_not_deleted: bool = False
|
|
278
|
+
) -> None:
|
|
351
279
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if not application_service:
|
|
356
|
-
# TODO raise exception instead of abort
|
|
357
|
-
click.secho(
|
|
358
|
-
f"The service {svc_name} was not found in the application {application.name}. "
|
|
359
|
-
f"It either does not exist, or has not been deployed.",
|
|
360
|
-
fg="red",
|
|
280
|
+
tag_descriptions = self.load_balancer.get_rules_tag_descriptions_by_listener_arn(
|
|
281
|
+
listener_arn
|
|
361
282
|
)
|
|
362
|
-
raise click.Abort
|
|
363
283
|
|
|
364
|
-
|
|
284
|
+
# keep track of rules deleted
|
|
285
|
+
deleted_rules = {"MaintenancePage": 0}
|
|
286
|
+
for name in ["MaintenancePage", "AllowedIps", "BypassIpFilter", "AllowedSourceIps"]:
|
|
287
|
+
deleted_list = self.load_balancer.delete_listener_rule_by_tags(tag_descriptions, name)
|
|
288
|
+
|
|
289
|
+
# track the rules deleted grouped by service
|
|
290
|
+
for deleted_rule in deleted_list:
|
|
291
|
+
tags = {t["Key"]: t["Value"] for t in deleted_rule["Tags"]}
|
|
292
|
+
if "service" in tags:
|
|
293
|
+
if tags["service"] not in deleted_rules:
|
|
294
|
+
deleted_rules[tags["service"]] = {
|
|
295
|
+
"AllowedIps": 0,
|
|
296
|
+
"BypassIpFilter": 0,
|
|
297
|
+
"AllowedSourceIps": 0,
|
|
298
|
+
}
|
|
299
|
+
deleted_rules[tags["service"]][name] += 1
|
|
300
|
+
elif tags.get("name") == "MaintenancePage":
|
|
301
|
+
deleted_rules["MaintenancePage"] += 1
|
|
302
|
+
|
|
303
|
+
if (
|
|
304
|
+
fail_when_not_deleted
|
|
305
|
+
and name == "MaintenancePage"
|
|
306
|
+
and deleted_rules["MaintenancePage"] == 0
|
|
307
|
+
):
|
|
308
|
+
raise ListenerRuleNotFoundException()
|
|
365
309
|
|
|
310
|
+
self.io.warn(
|
|
311
|
+
f"Rules deleted by type and grouped by service: {deleted_rules}",
|
|
312
|
+
)
|
|
366
313
|
|
|
367
|
-
def
|
|
368
|
-
|
|
314
|
+
def __remove_maintenance_page(self, listener_arn: str) -> dict[str, bool]:
|
|
315
|
+
self.__clean_up_maintenance_page_rules(listener_arn, True)
|
|
369
316
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
f"It either does not exist, or has not been deployed.",
|
|
374
|
-
fg="red",
|
|
317
|
+
def __get_maintenance_page_type(self, listener_arn: str) -> Union[str, None]:
|
|
318
|
+
tag_descriptions = self.load_balancer.get_rules_tag_descriptions_by_listener_arn(
|
|
319
|
+
listener_arn
|
|
375
320
|
)
|
|
376
|
-
|
|
321
|
+
maintenance_page_type = None
|
|
322
|
+
for description in tag_descriptions:
|
|
323
|
+
tags = {t["Key"]: t["Value"] for t in description["Tags"]}
|
|
324
|
+
if tags.get("name") == "MaintenancePage":
|
|
325
|
+
maintenance_page_type = tags.get("type")
|
|
377
326
|
|
|
378
|
-
|
|
327
|
+
return maintenance_page_type
|
|
379
328
|
|
|
380
329
|
|
|
381
|
-
def
|
|
382
|
-
|
|
383
|
-
chunk_size = 20
|
|
330
|
+
def get_app_service(application: Application, svc_name: str) -> Service:
|
|
331
|
+
application_service = application.services.get(svc_name)
|
|
384
332
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
resource_arns = [r["RuleArn"] for r in chunk]
|
|
388
|
-
response = lb_client.describe_tags(ResourceArns=resource_arns)
|
|
389
|
-
tag_descriptions.extend(response["TagDescriptions"])
|
|
333
|
+
if not application_service:
|
|
334
|
+
raise ApplicationServiceNotFoundException(application.name, svc_name)
|
|
390
335
|
|
|
391
|
-
return
|
|
336
|
+
return application_service
|
|
392
337
|
|
|
393
338
|
|
|
394
|
-
def
|
|
395
|
-
|
|
339
|
+
def get_app_environment(application: Application, env_name: str) -> Environment:
|
|
340
|
+
application_environment = application.environments.get(env_name)
|
|
396
341
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
if tags.get("name") == tag_name:
|
|
400
|
-
current_rule_arn = description["ResourceArn"]
|
|
401
|
-
if current_rule_arn:
|
|
402
|
-
lb_client.delete_rule(RuleArn=current_rule_arn)
|
|
342
|
+
if not application_environment:
|
|
343
|
+
raise ApplicationEnvironmentNotFoundException(application.name, env_name)
|
|
403
344
|
|
|
404
|
-
return
|
|
345
|
+
return application_environment
|
|
405
346
|
|
|
406
347
|
|
|
407
348
|
def get_maintenance_page_template(template) -> str:
|
|
@@ -416,148 +357,3 @@ def get_maintenance_page_template(template) -> str:
|
|
|
416
357
|
|
|
417
358
|
# [^\S]\s+ - Remove any space that is not preceded by a non-space character.
|
|
418
359
|
return re.sub(r"[^\S]\s+", "", template_contents)
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
def find_target_group(app: str, env: str, svc: str, session: boto3.Session) -> str:
|
|
422
|
-
rg_tagging_client = session.client("resourcegroupstaggingapi")
|
|
423
|
-
response = rg_tagging_client.get_resources(
|
|
424
|
-
TagFilters=[
|
|
425
|
-
{
|
|
426
|
-
"Key": "copilot-application",
|
|
427
|
-
"Values": [
|
|
428
|
-
app,
|
|
429
|
-
],
|
|
430
|
-
"Key": "copilot-environment",
|
|
431
|
-
"Values": [
|
|
432
|
-
env,
|
|
433
|
-
],
|
|
434
|
-
"Key": "copilot-service",
|
|
435
|
-
"Values": [
|
|
436
|
-
svc,
|
|
437
|
-
],
|
|
438
|
-
},
|
|
439
|
-
],
|
|
440
|
-
ResourceTypeFilters=[
|
|
441
|
-
"elasticloadbalancing:targetgroup",
|
|
442
|
-
],
|
|
443
|
-
)
|
|
444
|
-
for resource in response["ResourceTagMappingList"]:
|
|
445
|
-
tags = {tag["Key"]: tag["Value"] for tag in resource["Tags"]}
|
|
446
|
-
|
|
447
|
-
if (
|
|
448
|
-
"copilot-service" in tags
|
|
449
|
-
and tags["copilot-service"] == svc
|
|
450
|
-
and "copilot-environment" in tags
|
|
451
|
-
and tags["copilot-environment"] == env
|
|
452
|
-
and "copilot-application" in tags
|
|
453
|
-
and tags["copilot-application"] == app
|
|
454
|
-
):
|
|
455
|
-
return resource["ResourceARN"]
|
|
456
|
-
|
|
457
|
-
click.secho(
|
|
458
|
-
f"No target group found for application: {app}, environment: {env}, service: {svc}",
|
|
459
|
-
fg="red",
|
|
460
|
-
)
|
|
461
|
-
|
|
462
|
-
return None
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
def create_header_rule(
|
|
466
|
-
lb_client: boto3.client,
|
|
467
|
-
listener_arn: str,
|
|
468
|
-
target_group_arn: str,
|
|
469
|
-
header_name: str,
|
|
470
|
-
values: list,
|
|
471
|
-
rule_name: str,
|
|
472
|
-
priority: int,
|
|
473
|
-
conditions: list,
|
|
474
|
-
):
|
|
475
|
-
# add new condition to existing conditions
|
|
476
|
-
combined_conditions = [
|
|
477
|
-
{
|
|
478
|
-
"Field": "http-header",
|
|
479
|
-
"HttpHeaderConfig": {"HttpHeaderName": header_name, "Values": values},
|
|
480
|
-
}
|
|
481
|
-
] + conditions
|
|
482
|
-
|
|
483
|
-
lb_client.create_rule(
|
|
484
|
-
ListenerArn=listener_arn,
|
|
485
|
-
Priority=priority,
|
|
486
|
-
Conditions=combined_conditions,
|
|
487
|
-
Actions=[{"Type": "forward", "TargetGroupArn": target_group_arn}],
|
|
488
|
-
Tags=[
|
|
489
|
-
{"Key": "name", "Value": rule_name},
|
|
490
|
-
],
|
|
491
|
-
)
|
|
492
|
-
|
|
493
|
-
click.secho(
|
|
494
|
-
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}.",
|
|
495
|
-
fg="green",
|
|
496
|
-
)
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
def normalise_to_cidr(ip: str):
|
|
500
|
-
if "/" in ip:
|
|
501
|
-
return ip
|
|
502
|
-
SINGLE_IPV4_CIDR_PREFIX_LENGTH = "32"
|
|
503
|
-
return f"{ip}/{SINGLE_IPV4_CIDR_PREFIX_LENGTH}"
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
def create_source_ip_rule(
|
|
507
|
-
lb_client: boto3.client,
|
|
508
|
-
listener_arn: str,
|
|
509
|
-
target_group_arn: str,
|
|
510
|
-
values: list,
|
|
511
|
-
rule_name: str,
|
|
512
|
-
priority: int,
|
|
513
|
-
conditions: list,
|
|
514
|
-
):
|
|
515
|
-
# add new condition to existing conditions
|
|
516
|
-
|
|
517
|
-
combined_conditions = [
|
|
518
|
-
{
|
|
519
|
-
"Field": "source-ip",
|
|
520
|
-
"SourceIpConfig": {"Values": [normalise_to_cidr(value) for value in values]},
|
|
521
|
-
}
|
|
522
|
-
] + conditions
|
|
523
|
-
|
|
524
|
-
lb_client.create_rule(
|
|
525
|
-
ListenerArn=listener_arn,
|
|
526
|
-
Priority=priority,
|
|
527
|
-
Conditions=combined_conditions,
|
|
528
|
-
Actions=[{"Type": "forward", "TargetGroupArn": target_group_arn}],
|
|
529
|
-
Tags=[
|
|
530
|
-
{"Key": "name", "Value": rule_name},
|
|
531
|
-
],
|
|
532
|
-
)
|
|
533
|
-
|
|
534
|
-
click.secho(
|
|
535
|
-
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}.",
|
|
536
|
-
fg="green",
|
|
537
|
-
)
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
def get_host_header_conditions(
|
|
541
|
-
lb_client: boto3.client, listener_arn: str, target_group_arn: str
|
|
542
|
-
) -> list:
|
|
543
|
-
rules = lb_client.describe_rules(ListenerArn=listener_arn)["Rules"]
|
|
544
|
-
|
|
545
|
-
# Get current set of forwarding conditions for the target group
|
|
546
|
-
for rule in rules:
|
|
547
|
-
for action in rule["Actions"]:
|
|
548
|
-
if action["Type"] == "forward" and action["TargetGroupArn"] == target_group_arn:
|
|
549
|
-
conditions = rule["Conditions"]
|
|
550
|
-
|
|
551
|
-
# filter to host-header conditions
|
|
552
|
-
conditions = [
|
|
553
|
-
{i: condition[i] for i in condition if i != "Values"}
|
|
554
|
-
for condition in conditions
|
|
555
|
-
if condition["Field"] == "host-header"
|
|
556
|
-
]
|
|
557
|
-
|
|
558
|
-
# remove internal hosts
|
|
559
|
-
conditions[0]["HostHeaderConfig"]["Values"] = [
|
|
560
|
-
v for v in conditions[0]["HostHeaderConfig"]["Values"]
|
|
561
|
-
]
|
|
562
|
-
|
|
563
|
-
return conditions
|