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
|
@@ -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
|
|
|
@@ -55,21 +54,7 @@ class FailedToActivateMaintenancePageException(MaintenancePageException):
|
|
|
55
54
|
)
|
|
56
55
|
|
|
57
56
|
|
|
58
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
57
|
+
# TODO should this be in its own provider, inside the VPC one, what logic is this sepcific too?
|
|
73
58
|
def get_env_ips(vpc: str, application_environment: Environment) -> List[str]:
|
|
74
59
|
account_name = f"{application_environment.session.profile_name}-vpc"
|
|
75
60
|
vpc_name = vpc if vpc else account_name
|
|
@@ -84,164 +69,19 @@ def get_env_ips(vpc: str, application_environment: Environment) -> List[str]:
|
|
|
84
69
|
return [ip.strip() for ip in param_value.split(",")]
|
|
85
70
|
|
|
86
71
|
|
|
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
72
|
class MaintenancePage:
|
|
221
73
|
def __init__(
|
|
222
74
|
self,
|
|
223
75
|
application: Application,
|
|
224
76
|
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,
|
|
77
|
+
load_balancer_provider: LoadBalancerProvider = LoadBalancerProvider,
|
|
232
78
|
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
79
|
):
|
|
238
80
|
self.application = application
|
|
239
|
-
self.get_https_listener_for_application = get_https_listener_for_application
|
|
240
81
|
self.io = io
|
|
241
|
-
self.
|
|
82
|
+
self.load_balancer_provider = load_balancer_provider # TODO requires session from environment in application object which is only known during method execution
|
|
83
|
+
self.load_balancer: LoadBalancerProvider = None
|
|
242
84
|
self.get_env_ips = get_env_ips
|
|
243
|
-
self.add_maintenance_page = add_maintenance_page
|
|
244
|
-
self.remove_maintenance_page = remove_maintenance_page
|
|
245
85
|
|
|
246
86
|
def _get_deployed_load_balanced_web_services(self, app: Application, svc: List[str]):
|
|
247
87
|
if "*" in svc:
|
|
@@ -253,155 +93,229 @@ class MaintenancePage:
|
|
|
253
93
|
raise LoadBalancedWebServiceNotFoundException(app.name)
|
|
254
94
|
return services
|
|
255
95
|
|
|
96
|
+
# TODO: inject load balancer provider in activate method to avoid passing load balancer provider in init?
|
|
256
97
|
def activate(self, env: str, services: List[str], template: str, vpc: Union[str, None]):
|
|
257
98
|
|
|
258
99
|
services = self._get_deployed_load_balanced_web_services(self.application, services)
|
|
259
100
|
application_environment = get_app_environment(self.application, env)
|
|
101
|
+
self.load_balancer = self.load_balancer_provider(application_environment.session)
|
|
260
102
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
103
|
+
https_listener = self.load_balancer.get_https_listener_for_application(
|
|
104
|
+
self.application.name, env
|
|
105
|
+
)
|
|
106
|
+
current_maintenance_page = self.__get_maintenance_page_type(https_listener)
|
|
107
|
+
remove_current_maintenance_page = False
|
|
108
|
+
if current_maintenance_page:
|
|
109
|
+
remove_current_maintenance_page = self.io.confirm(
|
|
110
|
+
f"There is currently a '{current_maintenance_page}' maintenance page for the {env} "
|
|
111
|
+
f"environment in {self.application.name}.\nWould you like to replace it with a '{template}' "
|
|
112
|
+
f"maintenance page?"
|
|
267
113
|
)
|
|
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
|
-
)
|
|
114
|
+
if not remove_current_maintenance_page:
|
|
115
|
+
return
|
|
299
116
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
self.
|
|
303
|
-
|
|
117
|
+
if remove_current_maintenance_page or self.io.confirm(
|
|
118
|
+
f"You are about to enable the '{template}' maintenance page for the {env} "
|
|
119
|
+
f"environment in {self.application.name}.\nWould you like to continue?"
|
|
120
|
+
):
|
|
121
|
+
if current_maintenance_page and remove_current_maintenance_page:
|
|
122
|
+
self.__remove_maintenance_page(https_listener)
|
|
123
|
+
|
|
124
|
+
allowed_ips = self.get_env_ips(vpc, application_environment)
|
|
125
|
+
|
|
126
|
+
self.add_maintenance_page(
|
|
127
|
+
https_listener,
|
|
128
|
+
self.application.name,
|
|
129
|
+
env,
|
|
130
|
+
services,
|
|
131
|
+
allowed_ips,
|
|
132
|
+
template,
|
|
304
133
|
)
|
|
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}.",
|
|
134
|
+
self.io.info(
|
|
135
|
+
f"Maintenance page '{template}' added for environment {env} in application {self.application.name}",
|
|
310
136
|
)
|
|
311
137
|
|
|
312
138
|
def deactivate(self, env: str):
|
|
313
139
|
application_environment = get_app_environment(self.application, env)
|
|
314
140
|
|
|
141
|
+
self.load_balancer = self.load_balancer_provider(application_environment.session)
|
|
142
|
+
|
|
143
|
+
https_listener = self.load_balancer.get_https_listener_for_application(
|
|
144
|
+
self.application.name, env
|
|
145
|
+
)
|
|
146
|
+
current_maintenance_page = self.__get_maintenance_page_type(https_listener)
|
|
147
|
+
|
|
148
|
+
if not current_maintenance_page:
|
|
149
|
+
self.io.warn("There is no current maintenance page to remove")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
if not self.io.confirm(
|
|
153
|
+
f"There is currently a '{current_maintenance_page}' maintenance page, "
|
|
154
|
+
f"would you like to remove it?"
|
|
155
|
+
):
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
self.__remove_maintenance_page(https_listener)
|
|
159
|
+
self.io.info(
|
|
160
|
+
f"Maintenance page removed from environment {env} in application {self.application.name}",
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def add_maintenance_page(
|
|
164
|
+
self,
|
|
165
|
+
listener_arn: str,
|
|
166
|
+
app: str,
|
|
167
|
+
env: str,
|
|
168
|
+
services: List[Service],
|
|
169
|
+
allowed_ips: List[str],
|
|
170
|
+
template: str = "default",
|
|
171
|
+
):
|
|
172
|
+
|
|
173
|
+
maintenance_page_content = get_maintenance_page_template(template)
|
|
174
|
+
bypass_value = "".join(random.choices(string.ascii_lowercase + string.digits, k=12))
|
|
175
|
+
|
|
176
|
+
rule_priority = itertools.count(start=1)
|
|
177
|
+
maintenance_page_host_header_conditions = []
|
|
315
178
|
try:
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
)
|
|
319
|
-
current_maintenance_page = self.get_maintenance_page_type(
|
|
320
|
-
application_environment.session, https_listener
|
|
321
|
-
)
|
|
179
|
+
for svc in services:
|
|
180
|
+
target_group_arn = self.load_balancer.find_target_group(app, env, svc.name)
|
|
322
181
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
return
|
|
182
|
+
# not all of an application's services are guaranteed to have been deployed to an environment
|
|
183
|
+
if not target_group_arn:
|
|
184
|
+
continue
|
|
327
185
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
186
|
+
service_conditions = self.load_balancer.get_host_header_conditions(
|
|
187
|
+
listener_arn, target_group_arn
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
for ip in allowed_ips:
|
|
191
|
+
self.load_balancer.create_header_rule(
|
|
192
|
+
listener_arn,
|
|
193
|
+
target_group_arn,
|
|
194
|
+
"X-Forwarded-For",
|
|
195
|
+
[ip],
|
|
196
|
+
"AllowedIps",
|
|
197
|
+
next(rule_priority),
|
|
198
|
+
service_conditions,
|
|
199
|
+
)
|
|
200
|
+
self.load_balancer.create_source_ip_rule(
|
|
201
|
+
listener_arn,
|
|
202
|
+
target_group_arn,
|
|
203
|
+
[ip],
|
|
204
|
+
"AllowedSourceIps",
|
|
205
|
+
next(rule_priority),
|
|
206
|
+
service_conditions,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
self.load_balancer.create_header_rule(
|
|
210
|
+
listener_arn,
|
|
211
|
+
target_group_arn,
|
|
212
|
+
"Bypass-Key",
|
|
213
|
+
[bypass_value],
|
|
214
|
+
"BypassIpFilter",
|
|
215
|
+
next(rule_priority),
|
|
216
|
+
service_conditions,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# add to accumilating list of conditions for maintenace page rule
|
|
220
|
+
maintenance_page_host_header_conditions.extend(service_conditions)
|
|
333
221
|
|
|
334
|
-
self.remove_maintenance_page(application_environment.session, https_listener)
|
|
335
222
|
self.io.info(
|
|
336
|
-
f"
|
|
223
|
+
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
224
|
)
|
|
338
225
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
226
|
+
self.load_balancer.create_rule(
|
|
227
|
+
listener_arn=listener_arn,
|
|
228
|
+
priority=next(rule_priority),
|
|
229
|
+
conditions=[
|
|
230
|
+
{
|
|
231
|
+
"Field": "path-pattern",
|
|
232
|
+
"PathPatternConfig": {"Values": ["/*"]},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"Field": "host-header",
|
|
236
|
+
"HostHeaderConfig": {
|
|
237
|
+
"Values": sorted(
|
|
238
|
+
list(
|
|
239
|
+
{
|
|
240
|
+
value
|
|
241
|
+
for condition in maintenance_page_host_header_conditions
|
|
242
|
+
for value in condition["HostHeaderConfig"]["Values"]
|
|
243
|
+
}
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
actions=[
|
|
250
|
+
{
|
|
251
|
+
"Type": "fixed-response",
|
|
252
|
+
"FixedResponseConfig": {
|
|
253
|
+
"StatusCode": "503",
|
|
254
|
+
"ContentType": "text/html",
|
|
255
|
+
"MessageBody": maintenance_page_content,
|
|
256
|
+
},
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
tags=[
|
|
260
|
+
{"Key": "name", "Value": "MaintenancePage"},
|
|
261
|
+
{"Key": "type", "Value": template},
|
|
262
|
+
],
|
|
343
263
|
)
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
f"No HTTPS listener found for environment {env} in the application {self.application.name}.",
|
|
264
|
+
except Exception as e:
|
|
265
|
+
deleted_rules = self.__clean_up_maintenance_page_rules(listener_arn)
|
|
266
|
+
raise FailedToActivateMaintenancePageException(
|
|
267
|
+
app, env, f"{e}:\n {traceback.format_exc()}", deleted_rules
|
|
349
268
|
)
|
|
350
269
|
|
|
270
|
+
def __clean_up_maintenance_page_rules(
|
|
271
|
+
self, listener_arn: str, fail_when_not_deleted: bool = False
|
|
272
|
+
) -> dict[str, bool]:
|
|
351
273
|
|
|
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",
|
|
274
|
+
tag_descriptions = self.load_balancer.get_rules_tag_descriptions_by_listener_arn(
|
|
275
|
+
listener_arn
|
|
361
276
|
)
|
|
362
|
-
raise click.Abort
|
|
363
277
|
|
|
364
|
-
|
|
278
|
+
deletes = {}
|
|
279
|
+
for name in ["MaintenancePage", "AllowedIps", "BypassIpFilter", "AllowedSourceIps"]:
|
|
280
|
+
deleted = self.load_balancer.delete_listener_rule_by_tags(tag_descriptions, name)
|
|
281
|
+
deletes[name] = bool(deleted)
|
|
282
|
+
if fail_when_not_deleted and name == "MaintenancePage" and not deleted:
|
|
283
|
+
raise ListenerRuleNotFoundException()
|
|
365
284
|
|
|
285
|
+
return deletes
|
|
366
286
|
|
|
367
|
-
def
|
|
368
|
-
|
|
287
|
+
def __remove_maintenance_page(self, listener_arn: str) -> dict[str, bool]:
|
|
288
|
+
self.__clean_up_maintenance_page_rules(listener_arn, True)
|
|
369
289
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
f"It either does not exist, or has not been deployed.",
|
|
374
|
-
fg="red",
|
|
290
|
+
def __get_maintenance_page_type(self, listener_arn: str) -> Union[str, None]:
|
|
291
|
+
tag_descriptions = self.load_balancer.get_rules_tag_descriptions_by_listener_arn(
|
|
292
|
+
listener_arn
|
|
375
293
|
)
|
|
376
|
-
|
|
294
|
+
maintenance_page_type = None
|
|
295
|
+
for description in tag_descriptions:
|
|
296
|
+
tags = {t["Key"]: t["Value"] for t in description["Tags"]}
|
|
297
|
+
if tags.get("name") == "MaintenancePage":
|
|
298
|
+
maintenance_page_type = tags.get("type")
|
|
377
299
|
|
|
378
|
-
|
|
300
|
+
return maintenance_page_type
|
|
379
301
|
|
|
380
302
|
|
|
381
|
-
def
|
|
382
|
-
|
|
383
|
-
chunk_size = 20
|
|
303
|
+
def get_app_service(application: Application, svc_name: str) -> Service:
|
|
304
|
+
application_service = application.services.get(svc_name)
|
|
384
305
|
|
|
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"])
|
|
306
|
+
if not application_service:
|
|
307
|
+
raise ApplicationServiceNotFoundException(application.name, svc_name)
|
|
390
308
|
|
|
391
|
-
return
|
|
309
|
+
return application_service
|
|
392
310
|
|
|
393
311
|
|
|
394
|
-
def
|
|
395
|
-
|
|
312
|
+
def get_app_environment(application: Application, env_name: str) -> Environment:
|
|
313
|
+
application_environment = application.environments.get(env_name)
|
|
396
314
|
|
|
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)
|
|
315
|
+
if not application_environment:
|
|
316
|
+
raise ApplicationEnvironmentNotFoundException(application.name, env_name)
|
|
403
317
|
|
|
404
|
-
return
|
|
318
|
+
return application_environment
|
|
405
319
|
|
|
406
320
|
|
|
407
321
|
def get_maintenance_page_template(template) -> str:
|
|
@@ -416,148 +330,3 @@ def get_maintenance_page_template(template) -> str:
|
|
|
416
330
|
|
|
417
331
|
# [^\S]\s+ - Remove any space that is not preceded by a non-space character.
|
|
418
332
|
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
|