import os import sys from datetime import datetime, timedelta, timezone from typing import Any, List, Optional, Sequence, Tuple from sqlalchemy import Row, select from app import app from app.extensions import db from app.models import AbstractResource from app.models.bridges import Bridge, BridgeConf from app.models.cloud import CloudAccount, CloudProvider from app.terraform.terraform import TerraformAutomation BridgeResourceRow = Row[Tuple[AbstractResource, BridgeConf, CloudAccount]] def active_bridges_by_provider(provider: CloudProvider) -> Sequence[BridgeResourceRow]: stmt = ( select(Bridge, BridgeConf, CloudAccount) .join_from(Bridge, BridgeConf) .join_from(Bridge, CloudAccount) .where( CloudAccount.provider == provider, Bridge.destroyed.is_(None), ) ) bridges: Sequence[BridgeResourceRow] = db.session.execute(stmt).all() return bridges def recently_destroyed_bridges_by_provider( provider: CloudProvider, ) -> Sequence[BridgeResourceRow]: cutoff = datetime.now(tz=timezone.utc) - timedelta(hours=72) stmt = ( select(Bridge, BridgeConf, CloudAccount) .join_from(Bridge, BridgeConf) .join_from(Bridge, CloudAccount) .where( CloudAccount.provider == provider, Bridge.destroyed.is_not(None), Bridge.destroyed >= cutoff, ) ) bridges: Sequence[BridgeResourceRow] = db.session.execute(stmt).all() return bridges class BridgeAutomation(TerraformAutomation): template: str """ Terraform configuration template using Jinja 2. """ template_parameters: List[str] """ List of parameters to be read from the application configuration for use in the templating of the Terraform configuration. """ max_bridges = sys.maxsize # TODO: Only enable providers that have details configured enabled = True def tf_prehook(self) -> Optional[Any]: # pylint: disable=useless-return return None def tf_generate(self) -> None: self.tf_write( self.template, active_resources=active_bridges_by_provider(self.provider), destroyed_resources=recently_destroyed_bridges_by_provider(self.provider), global_namespace=app.config["GLOBAL_NAMESPACE"], terraform_modules_path=os.path.join( *list(os.path.split(app.root_path))[:-1], "terraform-modules" ), backend_config=f"""backend "http" {{ lock_address = "{app.config['TFSTATE_BACKEND']}/{self.short_name}" unlock_address = "{app.config['TFSTATE_BACKEND']}/{self.short_name}" address = "{app.config['TFSTATE_BACKEND']}/{self.short_name}" }}""", **{k: app.config[k.upper()] for k in self.template_parameters}, ) def tf_posthook(self, *, prehook_result: Any = None) -> None: outputs = self.tf_output() for output in outputs: if output.startswith("bridge_hashed_fingerprint_"): parts = outputs[output]["value"].split(" ") if len(parts) < 2: continue bridge = Bridge.query.filter( Bridge.id == output[len("bridge_hashed_fingerprint_") :] ).first() bridge.nickname = parts[0] bridge.hashed_fingerprint = parts[1] bridge.terraform_updated = datetime.now(tz=timezone.utc) if output.startswith("bridge_bridgeline_"): parts = outputs[output]["value"].split(" ") if len(parts) < 4: continue bridge = Bridge.query.filter( Bridge.id == output[len("bridge_bridgeline_") :] ).first() del parts[3] bridge.bridgeline = " ".join(parts) bridge.terraform_updated = datetime.now(tz=timezone.utc) db.session.commit() @classmethod def active_bridges_count(self) -> int: active_bridges = Bridge.query.filter( Bridge.provider == self.provider, Bridge.destroyed.is_(None), ).all() return len(active_bridges)