Spaces:
Runtime error
Runtime error
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 | |