From 2bf4282416c5c19bf8f0aca899ae892df2a0b5a4 Mon Sep 17 00:00:00 2001 From: irl Date: Fri, 18 Apr 2025 17:03:26 +0100 Subject: [PATCH] fix: auto-resolve stuck distribution deletions --- app/terraform/proxy/__init__.py | 2 +- app/terraform/proxy/cloudfront.py | 30 +++++++++++++++++++++++++++++- app/terraform/terraform.py | 5 +++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/app/terraform/proxy/__init__.py b/app/terraform/proxy/__init__.py index 82225bd..ed6fa5e 100644 --- a/app/terraform/proxy/__init__.py +++ b/app/terraform/proxy/__init__.py @@ -83,7 +83,7 @@ class ProxyAutomation(TerraformAutomation): def tf_prehook(self) -> Optional[Any]: # pylint: disable=useless-return return None - def tf_posthook(self, *, prehook_result: Any = None) -> None: + def tf_posthook(self, *, prehook_result: Any = None, logs: Optional[str] = None) -> None: self.import_state(self.tf_show()) def tf_generate(self) -> None: diff --git a/app/terraform/proxy/cloudfront.py b/app/terraform/proxy/cloudfront.py index 436e54d..08af028 100644 --- a/app/terraform/proxy/cloudfront.py +++ b/app/terraform/proxy/cloudfront.py @@ -1,5 +1,10 @@ +import json +import re from datetime import datetime, timezone -from typing import Any +from typing import Any, Optional + +import boto3 +from flask import current_app from app.extensions import db from app.models.mirrors import Proxy @@ -108,6 +113,29 @@ class ProxyCloudfrontAutomation(ProxyAutomation): {% endfor %} """ + def tf_posthook(self, *, prehook_result: Any = None, logs: Optional[str] = None) -> None: + self.import_state(self.tf_show()) + failed_ids = [] + for line in logs.strip().split('\n'): + try: + log_entry = json.loads(line) + if log_entry.get("@level") == "error" and "CloudFront Distribution" in log_entry.get("@message", ""): + match = re.search(r'CloudFront Distribution (\w+) cannot be deleted', log_entry["@message"]) + if match: + failed_ids.append(match.group(1)) + except json.JSONDecodeError: + continue + client = boto3.client( + 'cloudfront', + aws_access_key_id=current_app.config["AWS_ACCESS_KEY"], + aws_secret_access_key=current_app.config["AWS_SECRET_KEY"], + region_name="us-east-1" + ) + for failed_id in failed_ids: + response = client.get_distribution_config(Id=failed_id) + etag = response['ETag'] + client.delete_distribution(Id=failed_id, IfMatch=etag) + def import_state(self, state: Any) -> None: if not isinstance(state, dict): raise RuntimeError("The Terraform state object returned was not a dict.") diff --git a/app/terraform/terraform.py b/app/terraform/terraform.py index 4b082b7..e08fc64 100644 --- a/app/terraform/terraform.py +++ b/app/terraform/terraform.py @@ -54,7 +54,7 @@ class TerraformAutomation(BaseAutomation): returncode, logs = self.tf_apply( self.working_dir, refresh=self.always_refresh or full ) - self.tf_posthook(prehook_result=prehook_result) + self.tf_posthook(prehook_result=prehook_result, logs=logs) return returncode == 0, logs def tf_apply( @@ -143,7 +143,7 @@ class TerraformAutomation(BaseAutomation): ) return tfcmd.returncode, tfcmd.stdout.decode("utf-8") - def tf_posthook(self, *, prehook_result: Any = None) -> None: + def tf_posthook(self, *, prehook_result: Any = None, logs: Optional[str] = None) -> None: """ This hook function is called as part of normal automation, after the completion of :func:`tf_apply`. @@ -151,6 +151,7 @@ class TerraformAutomation(BaseAutomation): The default, if not overridden by a subclass, is to do nothing. :param prehook_result: the returned value of :func:`tf_prehook` + :param logs: any available logs from the apply stage :return: None """