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.

Files changed (42) hide show
  1. dbt_platform_helper/commands/application.py +3 -5
  2. dbt_platform_helper/commands/codebase.py +2 -4
  3. dbt_platform_helper/commands/conduit.py +2 -4
  4. dbt_platform_helper/commands/config.py +19 -17
  5. dbt_platform_helper/commands/copilot.py +13 -390
  6. dbt_platform_helper/commands/environment.py +6 -6
  7. dbt_platform_helper/commands/generate.py +2 -3
  8. dbt_platform_helper/commands/notify.py +2 -4
  9. dbt_platform_helper/commands/pipeline.py +2 -4
  10. dbt_platform_helper/commands/secrets.py +2 -4
  11. dbt_platform_helper/commands/version.py +2 -2
  12. dbt_platform_helper/domain/codebase.py +14 -11
  13. dbt_platform_helper/domain/copilot.py +397 -0
  14. dbt_platform_helper/domain/copilot_environment.py +6 -6
  15. dbt_platform_helper/domain/maintenance_page.py +227 -431
  16. dbt_platform_helper/domain/pipelines.py +1 -1
  17. dbt_platform_helper/domain/terraform_environment.py +1 -1
  18. dbt_platform_helper/domain/versioning.py +157 -0
  19. dbt_platform_helper/providers/aws/interfaces.py +13 -0
  20. dbt_platform_helper/providers/aws/opensearch.py +23 -0
  21. dbt_platform_helper/providers/aws/redis.py +21 -0
  22. dbt_platform_helper/providers/cache.py +40 -4
  23. dbt_platform_helper/providers/config_validator.py +15 -14
  24. dbt_platform_helper/providers/copilot.py +1 -1
  25. dbt_platform_helper/providers/io.py +17 -0
  26. dbt_platform_helper/providers/kms.py +22 -0
  27. dbt_platform_helper/providers/load_balancers.py +269 -43
  28. dbt_platform_helper/providers/semantic_version.py +33 -10
  29. dbt_platform_helper/providers/version.py +42 -0
  30. dbt_platform_helper/providers/yaml_file.py +0 -1
  31. dbt_platform_helper/utils/application.py +14 -0
  32. dbt_platform_helper/utils/aws.py +27 -4
  33. dbt_platform_helper/utils/tool_versioning.py +96 -0
  34. {dbt_platform_helper-13.1.1.dist-info → dbt_platform_helper-13.2.0.dist-info}/METADATA +3 -4
  35. {dbt_platform_helper-13.1.1.dist-info → dbt_platform_helper-13.2.0.dist-info}/RECORD +39 -35
  36. dbt_platform_helper/providers/opensearch.py +0 -36
  37. dbt_platform_helper/providers/redis.py +0 -34
  38. dbt_platform_helper/utils/versioning.py +0 -238
  39. /dbt_platform_helper/providers/{aws.py → aws/exceptions.py} +0 -0
  40. {dbt_platform_helper-13.1.1.dist-info → dbt_platform_helper-13.2.0.dist-info}/LICENSE +0 -0
  41. {dbt_platform_helper-13.1.1.dist-info → dbt_platform_helper-13.2.0.dist-info}/WHEEL +0 -0
  42. {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 LoadBalancerNotFoundException
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
- get_https_listener_for_application: Callable[
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.get_maintenance_page_type = get_maintenance_page_type
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
- try:
262
- https_listener = self.get_https_listener_for_application(
263
- application_environment.session, self.application.name, env
264
- )
265
- current_maintenance_page = self.get_maintenance_page_type(
266
- application_environment.session, https_listener
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
- remove_current_maintenance_page = False
269
- if current_maintenance_page:
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
- except LoadBalancerNotFoundException:
301
- # TODO push exception to command layer
302
- self.io.abort_with_error(
303
- f"No load balancer found for environment {env} in the application {self.application.name}.",
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
- except ListenerNotFoundException:
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
- https_listener = self.get_https_listener_for_application(
317
- application_environment.session, self.application.name, env
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
- # TODO discuss, reduce number of return statements but more nested if statements
324
- if not current_maintenance_page:
325
- self.io.warn("There is no current maintenance page to remove")
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
- if not self.io.confirm(
329
- f"There is currently a '{current_maintenance_page}' maintenance page, "
330
- f"would you like to remove it?"
331
- ):
332
- return
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"Maintenance page removed from environment {env} in application {self.application.name}",
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
- except LoadBalancerNotFoundException:
340
- # TODO push exception to command layer
341
- self.io.abort_with_error(
342
- f"No load balancer found for environment {env} in the application {self.application.name}.",
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
- except ListenerNotFoundException:
346
- # TODO push exception to command layer
347
- self.io.abort_with_error(
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
- def get_app_service(application: Application, svc_name: str) -> Service:
353
- application_service = application.services.get(svc_name)
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
- return application_service
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 get_app_environment(application: Application, env_name: str) -> Environment:
368
- application_environment = application.environments.get(env_name)
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
- if not application_environment:
371
- click.secho(
372
- f"The environment {env_name} was not found in the application {application.name}. "
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
- raise click.Abort
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
- return application_environment
327
+ return maintenance_page_type
379
328
 
380
329
 
381
- def get_rules_tag_descriptions(rules: list, lb_client):
382
- tag_descriptions = []
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
- for i in range(0, len(rules), chunk_size):
386
- chunk = rules[i : i + chunk_size]
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 tag_descriptions
336
+ return application_service
392
337
 
393
338
 
394
- def delete_listener_rule(tag_descriptions: list, tag_name: str, lb_client: boto3.client):
395
- current_rule_arn = None
339
+ def get_app_environment(application: Application, env_name: str) -> Environment:
340
+ application_environment = application.environments.get(env_name)
396
341
 
397
- for description in tag_descriptions:
398
- tags = {t["Key"]: t["Value"] for t in description["Tags"]}
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 current_rule_arn
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