"""Logic for ECS task management.""" import json import boto3 from botocore.exceptions import ClientError from moneyhub.config import Config from moneyhub.schemas.ecs_task import DbtRefreshRequest from moneyhub.utils.logger import get_logger logger = get_logger() def trigger_dbt_refresh_task(dbt_refresh_request: DbtRefreshRequest) -> dict[str, str]: """Trigger a dbt refresh task via Step Functions. Args: dbt_refresh_request: request containing task configuration parameters Returns: dict: dict containing execution ARN and start date """ try: sfn_client = boto3.client('stepfunctions', region_name=Config.AWS_REGION) step_function_input = _build_step_function_input(dbt_refresh_request) response = sfn_client.start_execution( stateMachineArn=Config.DBT_REFRESH_STATE_MACHINE_ARN, input=json.dumps(step_function_input) ) logger.info(f'Successfully triggered dbt refresh Step Function: {response}') return { 'execution_arn': response['executionArn'], 'start_date': response['startDate'].isoformat() } except ClientError as e: logger.error(f'AWS Step Functions error: {e.response["Error"]["Message"]}') raise Exception(f'Failed to trigger dbt refresh task: {e.response["Error"]["Message"]}') except Exception as e: logger.error(f'Unexpected error triggering dbt refresh task: {str(e)}') raise Exception(f'Failed to trigger dbt refresh task: {str(e)}') def _get_task_definition(ecs_client, task_family: str) -> dict: """Get the ECS task definition. Args: ecs_client: boto3 ECS client task_family: ECS task family name Returns: dict: task definition """ try: response = ecs_client.describe_task_definition(taskDefinition=task_family) return response['taskDefinition'] except ClientError as e: if e.response['Error']['Code'] == 'ClientException': raise Exception(f"Task definition '{task_family}' not found") raise Exception(f'Failed to get task definition: {e.response["Error"]["Message"]}') def _get_container_from_task_definition(task_definition: dict, container_name: str) -> dict: """Get the target container from the task definition. Args: task_definition: ECS task definition container_name: target container name Returns: dict: container definition """ for container in task_definition['containerDefinitions']: if container['name'] == container_name: return container raise Exception(f"Container '{container_name}' not found in task definition") def _build_step_function_input(dbt_refresh_request: DbtRefreshRequest) -> dict: """Build input payload for the Step Function. Args: dbt_refresh_request: Request containing task configuration parameters Returns: dict: input payload for the Step Function """ return { 'DBT_FULL_REFRESH': str(dbt_refresh_request.full_refresh).lower(), 'DBT_SELECT_MODELS': dbt_refresh_request.select_models, 'DBT_EXCLUDE_MODELS': dbt_refresh_request.exclude_models } def _build_network_configuration() -> dict: """Build the network configuration for the ECS task. Returns: dict: network configuration """ return { 'subnets': ( Config.ECS_TASK_SUBNETS.split(',') if Config.ECS_TASK_SUBNETS else [] ), 'securityGroups': ( Config.ECS_TASK_SECURITY_GROUPS.split(',') if Config.ECS_TASK_SECURITY_GROUPS else [] ), 'assignPublicIp': Config.ECS_TASK_ASSIGN_PUBLIC_IP } def _build_ecs_task_configuration( ecs_client, task_family: str, container_name: str, cluster_name: str, environment_variables: dict[str, str], network_config: dict, tags: list[dict], command: list = None, launch_type: str = 'FARGATE' ) -> dict: """Build a complete ECS task configuration. Args: ecs_client: Boto3 ECS client task_family: ECS task family name container_name: target container name cluster_name: ECS cluster name environment_variables: environment variables to set/override network_config: network configuration for the task tags: tags to apply to the task command: optional command array to execute in the container launch_type: ECS launch type Returns: dict: complete task configuration for ecs client """ task_definition = _get_task_definition(ecs_client, task_family) target_container = _get_container_from_task_definition(task_definition, container_name) existing_env = {env['name']: env['value'] for env in target_container.get('environment', [])} existing_env.update(environment_variables) container_overrides = { 'name': container_name, 'environment': [{'name': key, 'value': value} for key, value in existing_env.items()] } if command: container_overrides['command'] = command overrides = { 'containerOverrides': [container_overrides] } return { 'cluster': cluster_name, 'taskDefinition': task_definition['taskDefinitionArn'], 'launchType': launch_type, 'networkConfiguration': {'awsvpcConfiguration': network_config}, 'overrides': overrides, 'tags': tags }