from typing import Optional, Any, Dict, List, Union from typing_extensions import Literal from phi.aws.api_client import AwsApiClient from phi.aws.resource.base import AwsResource from phi.aws.resource.ec2.subnet import Subnet from phi.aws.resource.ec2.security_group import SecurityGroup from phi.aws.resource.ecs.cluster import EcsCluster from phi.aws.resource.ecs.task_definition import EcsTaskDefinition from phi.aws.resource.elb.target_group import TargetGroup from phi.cli.console import print_info from phi.utils.log import logger class EcsService(AwsResource): """ Reference: - https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecs.html """ resource_type: Optional[str] = "Service" service_name: str = "ecs" # Name for the service. name: str # Name for the service. # Use name if not provided. ecs_service_name: Optional[str] = None # EcsCluster for the service. # Can be # - string: The short name or full Amazon Resource Name (ARN) of the cluster # - EcsCluster # If you do not specify a cluster, the default cluster is assumed. cluster: Optional[Union[EcsCluster, str]] = None # EcsTaskDefinition for the service. # Can be # - string: The family and revision (family:revision ) or full ARN of the task definition. # - EcsTaskDefinition # If a revision isn't specified, the latest ACTIVE revision is used. task_definition: Optional[Union[EcsTaskDefinition, str]] = None # A load balancer object representing the load balancers to use with your service. load_balancers: Optional[List[Dict[str, Any]]] = None # We can generate the load_balancers dict using # the target_group, target_container_name and target_container_port # Target group to attach to a service. target_group: Optional[TargetGroup] = None # Target container name for the service. target_container_name: Optional[str] = None target_container_port: Optional[int] = None # The network configuration for the service. This parameter is required for task definitions that # use the awsvpc network mode to receive their own elastic network interface network_configuration: Optional[Dict[str, Any]] = None subnets: Optional[List[Union[str, Subnet]]] = None security_groups: Optional[List[Union[str, SecurityGroup]]] = None assign_public_ip: Optional[bool] = None # The configuration for this service to discover and connect to services, # and be discovered by, and connected from, other services within a namespace. service_connect_configuration: Optional[Dict[str, Any]] = None # The details of the service discovery registries to assign to this service. service_registries: Optional[List[Dict[str, Any]]] = None # The number of instantiations of the specified task definition to place and keep running on your cluster. # This is required if schedulingStrategy is REPLICA or isn't specified. # If schedulingStrategy is DAEMON then this isn't required. desired_count: Optional[int] = None # An identifier that you provide to ensure the idempotency of the request. It must be unique and is case-sensitive. client_token: Optional[str] = None # The infrastructure that you run your service on. launch_type: Optional[Union[str, Literal["EC2", "FARGATE", "EXTERNAL"]]] = None # The capacity provider strategy to use for the service. capacity_provider_strategy: Optional[List[Dict[str, Any]]] = None platform_version: Optional[str] = None role: Optional[str] = None deployment_configuration: Optional[Dict[str, Any]] = None placement_constraints: Optional[List[Dict[str, Any]]] = None placement_strategy: Optional[List[Dict[str, Any]]] = None health_check_grace_period_seconds: Optional[int] = None scheduling_strategy: Optional[Literal["REPLICA", "DAEMON"]] = None deployment_controller: Optional[Dict[str, Any]] = None tags: Optional[List[Dict[str, Any]]] = None enable_ecsmanaged_tags: Optional[bool] = None propagate_tags: Optional[Literal["TASK_DEFINITION", "SERVICE", "NONE"]] = None enable_execute_command: Optional[bool] = None force_delete: Optional[bool] = None # Force a new deployment of the service on update. # By default, deployments aren't forced. # You can use this option to start a new deployment with no service # definition changes. For example, you can update a service's # tasks to use a newer Docker image with the same # image/tag combination (my_image:latest ) or # to roll Fargate tasks onto a newer platform version. force_new_deployment: Optional[bool] = None wait_for_create: bool = False def get_ecs_service_name(self): return self.ecs_service_name or self.name def get_ecs_cluster_name(self): if self.cluster is not None: if isinstance(self.cluster, EcsCluster): return self.cluster.get_ecs_cluster_name() else: return self.cluster def get_ecs_task_definition(self): if self.task_definition is not None: if isinstance(self.task_definition, EcsTaskDefinition): return self.task_definition.get_task_family() else: return self.task_definition def _create(self, aws_client: AwsApiClient) -> bool: """Create EcsService""" print_info(f"Creating {self.get_resource_type()}: {self.get_resource_name()}") # create a dict of args which are not null, otherwise aws type validation fails not_null_args: Dict[str, Any] = {} cluster_name = self.get_ecs_cluster_name() if cluster_name is not None: not_null_args["cluster"] = cluster_name network_configuration = self.network_configuration if network_configuration is None and (self.subnets is not None or self.security_groups is not None): aws_vpc_config: Dict[str, Any] = {} if self.subnets is not None: subnet_ids = [] for subnet in self.subnets: if isinstance(subnet, Subnet): subnet_ids.append(subnet.name) elif isinstance(subnet, str): subnet_ids.append(subnet) aws_vpc_config["subnets"] = subnet_ids if self.security_groups is not None: security_group_ids = [] for sg in self.security_groups: if isinstance(sg, SecurityGroup): security_group_ids.append(sg.get_security_group_id(aws_client)) else: security_group_ids.append(sg) aws_vpc_config["securityGroups"] = security_group_ids if self.assign_public_ip: aws_vpc_config["assignPublicIp"] = "ENABLED" network_configuration = {"awsvpcConfiguration": aws_vpc_config} if network_configuration is not None: not_null_args["networkConfiguration"] = network_configuration if self.service_connect_configuration is not None: not_null_args["serviceConnectConfiguration"] = self.service_connect_configuration if self.service_registries is not None: not_null_args["serviceRegistries"] = self.service_registries if self.desired_count is not None: not_null_args["desiredCount"] = self.desired_count if self.client_token is not None: not_null_args["clientToken"] = self.client_token if self.launch_type is not None: not_null_args["launchType"] = self.launch_type if self.capacity_provider_strategy is not None: not_null_args["capacityProviderStrategy"] = self.capacity_provider_strategy if self.platform_version is not None: not_null_args["platformVersion"] = self.platform_version if self.role is not None: not_null_args["role"] = self.role if self.deployment_configuration is not None: not_null_args["deploymentConfiguration"] = self.deployment_configuration if self.placement_constraints is not None: not_null_args["placementConstraints"] = self.placement_constraints if self.placement_strategy is not None: not_null_args["placementStrategy"] = self.placement_strategy if self.health_check_grace_period_seconds is not None: not_null_args["healthCheckGracePeriodSeconds"] = self.health_check_grace_period_seconds if self.scheduling_strategy is not None: not_null_args["schedulingStrategy"] = self.scheduling_strategy if self.deployment_controller is not None: not_null_args["deploymentController"] = self.deployment_controller if self.tags is not None: not_null_args["tags"] = self.tags if self.enable_ecsmanaged_tags is not None: not_null_args["enableECSManagedTags"] = self.enable_ecsmanaged_tags if self.propagate_tags is not None: not_null_args["propagateTags"] = self.propagate_tags if self.enable_execute_command is not None: not_null_args["enableExecuteCommand"] = self.enable_execute_command if self.load_balancers is not None: not_null_args["loadBalancers"] = self.load_balancers elif self.target_group is not None and self.target_container_name is not None: not_null_args["loadBalancers"] = [ { "targetGroupArn": self.target_group.get_arn(aws_client), "containerName": self.target_container_name, "containerPort": self.target_container_port, } ] # Register EcsService service_client = self.get_service_client(aws_client) try: create_response = service_client.create_service( serviceName=self.get_ecs_service_name(), taskDefinition=self.get_ecs_task_definition(), **not_null_args, ) logger.debug(f"EcsService: {create_response}") resource_dict = create_response.get("service", {}) # Validate resource creation if resource_dict is not None: self.active_resource = create_response return True except Exception as e: logger.error(f"{self.get_resource_type()} could not be created.") logger.error(e) return False def post_create(self, aws_client: AwsApiClient) -> bool: # Wait for EcsService to be created if self.wait_for_create: try: cluster_name = self.get_ecs_cluster_name() if cluster_name is not None: print_info(f"Waiting for {self.get_resource_type()} to be available.") waiter = self.get_service_client(aws_client).get_waiter("services_stable") waiter.wait( cluster=cluster_name, services=[self.get_ecs_service_name()], WaiterConfig={ "Delay": self.waiter_delay, "MaxAttempts": self.waiter_max_attempts, }, ) else: logger.warning("Skipping waiter, no Service found") except Exception as e: logger.error("Waiter failed.") logger.error(e) return True def _read(self, aws_client: AwsApiClient) -> Optional[Any]: """Read EcsService""" from botocore.exceptions import ClientError logger.debug(f"Reading {self.get_resource_type()}: {self.get_resource_name()}") # create a dict of args which are not null, otherwise aws type validation fails not_null_args: Dict[str, Any] = {} cluster_name = self.get_ecs_cluster_name() if cluster_name is not None: not_null_args["cluster"] = cluster_name service_client = self.get_service_client(aws_client) try: service_name: str = self.get_ecs_service_name() describe_response = service_client.describe_services(services=[service_name], **not_null_args) logger.debug(f"EcsService: {describe_response}") resource_list = describe_response.get("services", None) if resource_list is not None and isinstance(resource_list, list): for resource in resource_list: _service_name: str = resource.get("serviceName", None) if _service_name == service_name: _service_status = resource.get("status", None) if _service_status == "ACTIVE": self.active_resource = resource break except ClientError as ce: logger.debug(f"ClientError: {ce}") except Exception as e: logger.error(f"Error reading {self.get_resource_type()}.") logger.error(e) return self.active_resource def _delete(self, aws_client: AwsApiClient) -> bool: """Delete EcsService""" print_info(f"Deleting {self.get_resource_type()}: {self.get_resource_name()}") # create a dict of args which are not null, otherwise aws type validation fails not_null_args: Dict[str, Any] = {} cluster_name = self.get_ecs_cluster_name() if cluster_name is not None: not_null_args["cluster"] = cluster_name if self.force_delete is not None: not_null_args["force"] = self.force_delete service_client = self.get_service_client(aws_client) self.active_resource = None try: delete_response = service_client.delete_service( service=self.get_ecs_service_name(), **not_null_args, ) logger.debug(f"EcsService: {delete_response}") return True except Exception as e: logger.error(f"{self.get_resource_type()} could not be deleted.") logger.error("Please try again or delete resources manually.") logger.error(e) return False def post_delete(self, aws_client: AwsApiClient) -> bool: # Wait for EcsService to be deleted if self.wait_for_delete: try: cluster_name = self.get_ecs_cluster_name() if cluster_name is not None: print_info(f"Waiting for {self.get_resource_type()} to be deleted.") waiter = self.get_service_client(aws_client).get_waiter("services_inactive") waiter.wait( cluster=cluster_name, services=[self.get_ecs_service_name()], WaiterConfig={ "Delay": self.waiter_delay, "MaxAttempts": self.waiter_max_attempts, }, ) else: logger.warning("Skipping waiter, no Service found") except Exception as e: logger.error("Waiter failed.") logger.error(e) return True def _update(self, aws_client: AwsApiClient) -> bool: """Updates the EcsService Args: aws_client: The AwsApiClient for the current cluster """ print_info(f"Updating {self.get_resource_type()}: {self.get_resource_name()}") # create a dict of args which are not null, otherwise aws type validation fails not_null_args: Dict[str, Any] = {} cluster_name = self.get_ecs_cluster_name() if cluster_name is not None: not_null_args["cluster"] = cluster_name network_configuration = self.network_configuration if network_configuration is None and (self.subnets is not None or self.security_groups is not None): aws_vpc_config: Dict[str, Any] = {} if self.subnets is not None: subnet_ids = [] for subnet in self.subnets: if isinstance(subnet, Subnet): subnet_ids.append(subnet.name) elif isinstance(subnet, str): subnet_ids.append(subnet) aws_vpc_config["subnets"] = subnet_ids if self.security_groups is not None: security_group_ids = [] for sg in self.security_groups: if isinstance(sg, SecurityGroup): security_group_ids.append(sg.get_security_group_id(aws_client)) else: security_group_ids.append(sg) aws_vpc_config["securityGroups"] = security_group_ids if self.assign_public_ip: aws_vpc_config["assignPublicIp"] = "ENABLED" network_configuration = {"awsvpcConfiguration": aws_vpc_config} if self.network_configuration is not None: not_null_args["networkConfiguration"] = network_configuration if self.desired_count is not None: not_null_args["desiredCount"] = self.desired_count if self.capacity_provider_strategy is not None: not_null_args["capacityProviderStrategy"] = self.capacity_provider_strategy if self.deployment_configuration is not None: not_null_args["deploymentConfiguration"] = self.deployment_configuration if self.placement_constraints is not None: not_null_args["placementConstraints"] = self.placement_constraints if self.placement_strategy is not None: not_null_args["placementStrategy"] = self.placement_strategy if self.platform_version is not None: not_null_args["platformVersion"] = self.platform_version if self.force_new_deployment is not None: not_null_args["forceNewDeployment"] = self.force_new_deployment if self.health_check_grace_period_seconds is not None: not_null_args["healthCheckGracePeriodSeconds"] = self.health_check_grace_period_seconds if self.enable_execute_command is not None: not_null_args["enableExecuteCommand"] = self.enable_execute_command if self.enable_ecsmanaged_tags is not None: not_null_args["enableECSManagedTags"] = self.enable_ecsmanaged_tags if self.load_balancers is not None: not_null_args["loadBalancers"] = self.load_balancers if self.propagate_tags is not None: not_null_args["propagateTags"] = self.propagate_tags if self.service_registries is not None: not_null_args["serviceRegistries"] = self.service_registries try: # Update EcsService service_client = self.get_service_client(aws_client) update_response = service_client.update_service( service=self.get_ecs_service_name(), taskDefinition=self.get_ecs_task_definition(), **not_null_args, ) logger.debug(f"update_response: {update_response}") self.active_resource = update_response.get("service", None) if self.active_resource is not None: print_info(f"{self.get_resource_type()}: {self.get_resource_name()} updated") return True except Exception as e: logger.error(f"{self.get_resource_type()} could not be updated.") logger.error("Please try again or update resources manually.") logger.error(e) return False