AmmarFahmy
adding all files
105b369
from textwrap import dedent
from typing import Optional, Any, Dict, List
from typing_extensions import Literal
from phi.aws.api_client import AwsApiClient
from phi.aws.resource.base import AwsResource
from phi.aws.resource.ecs.container import EcsContainer
from phi.aws.resource.ecs.volume import EcsVolume
from phi.aws.resource.iam.role import IamRole
from phi.aws.resource.iam.policy import IamPolicy
from phi.cli.console import print_info
from phi.utils.log import logger
class EcsTaskDefinition(AwsResource):
"""
Reference:
- https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ecs/client/register_task_definition.html
"""
resource_type: Optional[str] = "TaskDefinition"
service_name: str = "ecs"
# Name of the task definition.
# Used as task definition family.
name: str
# The family for a task definition.
# Use name as family if not provided
# You can use it track multiple versions of the same task definition.
# The family is used as a name for your task definition.
family: Optional[str] = None
# Networking mode to use for the containers in the task.
# The valid values are none, bridge, awsvpc, and host.
# If no network mode is specified, the default is bridge.
network_mode: Optional[Literal["bridge", "host", "awsvpc", "none"]] = None
# A list of container definitions that describe the different containers that make up the task.
containers: Optional[List[EcsContainer]] = None
volumes: Optional[List[EcsVolume]] = None
placement_constraints: Optional[List[Dict[str, Any]]] = None
requires_compatibilities: Optional[List[str]] = None
cpu: Optional[str] = None
memory: Optional[str] = None
tags: Optional[List[Dict[str, str]]] = None
pid_mode: Optional[Literal["host", "task"]] = None
ipc_mode: Optional[Literal["host", "task", "none"]] = None
proxy_configuration: Optional[Dict[str, Any]] = None
inference_accelerators: Optional[List[Dict[str, Any]]] = None
ephemeral_storage: Optional[Dict[str, Any]] = None
runtime_platform: Optional[Dict[str, Any]] = None
# Amazon ECS IAM roles
# The short name or full Amazon Resource Name (ARN) of the IAM role that containers in this task can assume.
# The permissions granted in this IAM role are assumed by the containers running in the task.
# For permissions that Amazon ECS needs to pull container images, see execution_role_arn
# If your containerized applications need to call AWS APIs, they must sign their
# AWS API requests with AWS credentials, and a task IAM role provides a strategy for managing credentials
# for your applications to use
task_role_arn: Optional[str] = None
# If task_role_arn is None, a default role is created if create_task_role is True
create_task_role: bool = True
# Name for the default role when task_role_arn is None, use "name-task-role" if not provided
task_role_name: Optional[str] = None
# Provide a list of policy ARNs to attach to the role
add_policy_arns_to_task_role: Optional[List[str]] = None
# Provide a list of IamPolicy to attach to the task role
add_policies_to_task_role: Optional[List[IamPolicy]] = None
# Add bedrock access to task role
add_bedrock_access_to_task: bool = False
# Add ecs_exec_policy to task role
add_exec_access_to_task: bool = False
# Add secret access to task role
add_secret_access_to_task: bool = False
# Add s3 access to task role
add_s3_access_to_task: bool = False
# The Amazon Resource Name (ARN) of the task execution role that grants the Amazon ECS container agent permission
# to make Amazon Web Services API calls on your behalf. The task execution IAM role is required depending on the
# requirements of your task.
execution_role_arn: Optional[str] = None
# If execution_role_arn is None, a default role is created if create_execution_role is True
create_execution_role: bool = True
# Name for the default role when execution_role_arn is None, use "name-execution-role" if not provided
execution_role_name: Optional[str] = None
# Provide a list of policy ARNs to attach to the role
add_policy_arns_to_execution_role: Optional[List[str]] = None
# Provide a list of IamPolicy to attach to the execution role
add_policies_to_execution_role: Optional[List[IamPolicy]] = None
# Add policy to read secrets to execution role
add_secret_access_to_ecs: bool = False
def get_task_family(self):
return self.family or self.name
def _create(self, aws_client: AwsApiClient) -> bool:
"""Create EcsTaskDefinition"""
print_info(f"Creating {self.get_resource_type()}: {self.get_resource_name()}")
# Step 1: Get task role arn
task_role_arn = self.task_role_arn
if task_role_arn is None and self.create_task_role:
# Create the IamRole and get task_role_arn
task_role = self.get_task_role()
try:
task_role.create(aws_client)
task_role_arn = task_role.read(aws_client).arn
print_info(f"ARN for {task_role.name}: {task_role_arn}")
except Exception as e:
logger.error("IamRole creation failed, please fix and try again")
logger.error(e)
return False
# Step 2: Get execution role arn
execution_role_arn = self.execution_role_arn
if execution_role_arn is None and self.create_execution_role:
# Create the IamRole and get execution_role_arn
execution_role = self.get_execution_role()
try:
execution_role.create(aws_client)
execution_role_arn = execution_role.read(aws_client).arn
print_info(f"ARN for {execution_role.name}: {execution_role_arn}")
except Exception as e:
logger.error("IamRole creation failed, please fix and try again")
logger.error(e)
return False
# create a dict of args which are not null, otherwise aws type validation fails
not_null_args: Dict[str, Any] = {}
if task_role_arn is not None:
not_null_args["taskRoleArn"] = task_role_arn
if execution_role_arn is not None:
not_null_args["executionRoleArn"] = execution_role_arn
if self.network_mode is not None:
not_null_args["networkMode"] = self.network_mode
if self.containers is not None:
container_definitions = [c.get_container_definition(aws_client=aws_client) for c in self.containers]
not_null_args["containerDefinitions"] = container_definitions
if self.volumes is not None:
volume_definitions = [v.get_volume_definition() for v in self.volumes]
not_null_args["volumes"] = volume_definitions
if self.placement_constraints is not None:
not_null_args["placementConstraints"] = self.placement_constraints
if self.requires_compatibilities is not None:
not_null_args["requiresCompatibilities"] = self.requires_compatibilities
if self.cpu is not None:
not_null_args["cpu"] = self.cpu
if self.memory is not None:
not_null_args["memory"] = self.memory
if self.tags is not None:
not_null_args["tags"] = self.tags
if self.pid_mode is not None:
not_null_args["pidMode"] = self.pid_mode
if self.ipc_mode is not None:
not_null_args["ipcMode"] = self.ipc_mode
if self.proxy_configuration is not None:
not_null_args["proxyConfiguration"] = self.proxy_configuration
if self.inference_accelerators is not None:
not_null_args["inferenceAccelerators"] = self.inference_accelerators
if self.ephemeral_storage is not None:
not_null_args["ephemeralStorage"] = self.ephemeral_storage
if self.runtime_platform is not None:
not_null_args["runtimePlatform"] = self.runtime_platform
# Register EcsTaskDefinition
service_client = self.get_service_client(aws_client)
try:
create_response = service_client.register_task_definition(
family=self.get_task_family(),
**not_null_args,
)
logger.debug(f"EcsTaskDefinition: {create_response}")
resource_dict = create_response.get("taskDefinition", {})
# 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 _read(self, aws_client: AwsApiClient) -> Optional[Any]:
"""Read EcsTaskDefinition"""
from botocore.exceptions import ClientError
logger.debug(f"Reading {self.get_resource_type()}: {self.get_resource_name()}")
service_client = self.get_service_client(aws_client)
try:
describe_response = service_client.describe_task_definition(taskDefinition=self.get_task_family())
logger.debug(f"EcsTaskDefinition: {describe_response}")
resource = describe_response.get("taskDefinition", None)
if resource is not None:
# compare the task definition with the current state
# if there is a difference, create a new task definition
# TODO: fix the task_definition_up_to_date function
# if self.task_definition_up_to_date(task_definition=resource):
self.active_resource = resource
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 EcsTaskDefinition"""
print_info(f"Deleting {self.get_resource_type()}: {self.get_resource_name()}")
# Step 1: Delete the task role
if self.task_role_arn is None and self.create_task_role:
task_role = self.get_task_role()
try:
task_role.delete(aws_client)
except Exception as e:
logger.error("IamRole deletion failed, please try again or delete manually")
logger.error(e)
# Step 2: Delete the execution role
if self.execution_role_arn is None and self.create_execution_role:
execution_role = self.get_execution_role()
try:
execution_role.delete(aws_client)
except Exception as e:
logger.error("IamRole deletion failed, please try again or delete manually")
logger.error(e)
service_client = self.get_service_client(aws_client)
self.active_resource = None
try:
# Get the task definition revisions
list_response = service_client.list_task_definitions(familyPrefix=self.get_task_family(), sort="DESC")
logger.debug(f"EcsTaskDefinition: {list_response}")
task_definition_arns = list_response.get("taskDefinitionArns", [])
if task_definition_arns:
# Delete all revisions
for task_definition_arn in task_definition_arns:
service_client.deregister_task_definition(taskDefinition=task_definition_arn)
print_info(f"EcsTaskDefinition deleted: {self.get_resource_name()}")
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 _update(self, aws_client: AwsApiClient) -> bool:
"""Update EcsTaskDefinition"""
print_info(f"Updating {self.get_resource_type()}: {self.get_resource_name()}")
return self._create(aws_client)
def get_task_role(self) -> IamRole:
policy_arns = [
"arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
"arn:aws:iam::aws:policy/CloudWatchFullAccess",
]
if self.add_policy_arns_to_task_role is not None and isinstance(self.add_policy_arns_to_task_role, list):
policy_arns.extend(self.add_policy_arns_to_task_role)
policies = []
if self.add_bedrock_access_to_task:
bedrock_access_policy = IamPolicy(
name=f"{self.name}-bedrock-access-policy",
policy_document=dedent(
"""\
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "bedrock:*",
"Resource": "*"
}
]
}
"""
),
)
policies.append(bedrock_access_policy)
if self.add_exec_access_to_task:
ecs_exec_policy = IamPolicy(
name=f"{self.name}-task-exec-policy",
policy_document=dedent(
"""\
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
],
"Resource": "*"
}
]
}
"""
),
)
policies.append(ecs_exec_policy)
if self.add_secret_access_to_task:
policy_arns.append("arn:aws:iam::aws:policy/SecretsManagerReadWrite")
if self.add_s3_access_to_task:
policy_arns.append("arn:aws:iam::aws:policy/AmazonS3FullAccess")
if self.add_policies_to_task_role:
policies.extend(self.add_policies_to_task_role)
return IamRole(
name=self.task_role_name or f"{self.name}-task-role",
assume_role_policy_document=dedent(
"""\
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
"""
),
policies=policies,
policy_arns=policy_arns,
)
def get_execution_role(self) -> IamRole:
policy_arns = [
"arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy",
"arn:aws:iam::aws:policy/CloudWatchFullAccess",
]
if self.add_policy_arns_to_execution_role is not None and isinstance(
self.add_policy_arns_to_execution_role, list
):
policy_arns.extend(self.add_policy_arns_to_execution_role)
policies = []
if self.add_secret_access_to_ecs:
ecs_secret_policy = IamPolicy(
name=f"{self.name}-ecs-secret-policy",
policy_document=dedent(
"""\
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
"secretsmanager:ListSecretVersionIds"
],
"Resource": "*"
}
]
}
"""
),
)
policies.append(ecs_secret_policy)
if self.add_policies_to_execution_role:
policies.extend(self.add_policies_to_execution_role)
return IamRole(
name=self.execution_role_name or f"{self.name}-execution-role",
assume_role_policy_document=dedent(
"""\
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
"""
),
policies=policies,
policy_arns=policy_arns,
)
# def task_definition_up_to_date(self, task_definition: Dict[str, Any]) -> bool:
# """Return True if task_definition from the cluster matches the current state"""
#
# # Validate container definitions
# if self.containers is not None:
# container_definitions_from_api = task_definition.get("containerDefinitions")
# # Compare the container definitions from the api with the current containers
# # The order of the container definitions should also match
# if container_definitions_from_api is not None and len(container_definitions_from_api) == len(
# self.containers
# ):
# for i, container in enumerate(self.containers):
# if not container.container_definition_up_to_date(
# container_definition=container_definitions_from_api[i]
# ):
# logger.debug("Container definitions not up to date")
# return False
# else:
# logger.debug("Container definitions not up to date")
# return False
#
# # Validate volumes
# if self.volumes is not None:
# volume_definitions_from_api = task_definition.get("volumes")
# # Compare the volume definitions from the api with the current volumes
# # The order of the volume definitions should also match
# if volume_definitions_from_api is not None and len(volume_definitions_from_api) == len(
# self.volumes
# ):
# for i, volume in enumerate(self.volumes):
# if not volume.volume_definition_up_to_date(
# volume_definition=volume_definitions_from_api[i]
# ):
# logger.debug("Volume definitions not up to date")
# return False
# else:
# logger.debug("Volume definitions not up to date")
# return False
#
# # Validate other properties
# if self.task_role_arn is not None:
# if self.task_role_arn != task_definition.get("taskRoleArn"):
# logger.debug("{} != {}".format(self.task_role_arn, task_definition.get("taskRoleArn")))
# return False
# if self.execution_role_arn is not None:
# if self.execution_role_arn != task_definition.get("executionRoleArn"):
# logger.debug(
# "{} != {}".format(self.execution_role_arn, task_definition.get("executionRoleArn"))
# )
# return False
# if self.network_mode is not None:
# if self.network_mode != task_definition.get("networkMode"):
# logger.debug("{} != {}".format(self.network_mode, task_definition.get("networkMode")))
# return False
# if self.placement_constraints is not None:
# if self.placement_constraints != task_definition.get("placementConstraints"):
# logger.debug(
# "{} != {}".format(
# self.placement_constraints,
# task_definition.get("placementConstraints"),
# )
# )
# return False
# if self.requires_compatibilities is not None:
# if self.requires_compatibilities != task_definition.get("requiresCompatibilities"):
# logger.debug(
# "{} != {}".format(
# self.requires_compatibilities,
# task_definition.get("requiresCompatibilities"),
# )
# )
# return False
# if self.cpu is not None:
# if self.cpu != task_definition.get("cpu"):
# logger.debug("{} != {}".format(self.cpu, task_definition.get("cpu")))
# return False
# if self.memory is not None:
# if self.memory != task_definition.get("memory"):
# logger.debug("{} != {}".format(self.memory, task_definition.get("memory")))
# return False
# if self.tags is not None:
# if self.tags != task_definition.get("tags"):
# logger.debug("{} != {}".format(self.tags, task_definition.get("tags")))
# return False
# if self.pid_mode is not None:
# if self.pid_mode != task_definition.get("pidMode"):
# logger.debug("{} != {}".format(self.pid_mode, task_definition.get("pidMode")))
# return False
# if self.ipc_mode is not None:
# if self.ipc_mode != task_definition.get("ipcMode"):
# logger.debug("{} != {}".format(self.ipc_mode, task_definition.get("ipcMode")))
# return False
# if self.proxy_configuration is not None:
# if self.proxy_configuration != task_definition.get("proxyConfiguration"):
# logger.debug(
# "{} != {}".format(
# self.proxy_configuration,
# task_definition.get("proxyConfiguration"),
# )
# )
# return False
# if self.inference_accelerators is not None:
# if self.inference_accelerators != task_definition.get("inferenceAccelerators"):
# logger.debug(
# "{} != {}".format(
# self.inference_accelerators,
# task_definition.get("inferenceAccelerators"),
# )
# )
# return False
# if self.ephemeral_storage is not None:
# if self.ephemeral_storage != task_definition.get("ephemeralStorage"):
# logger.debug(
# "{} != {}".format(self.ephemeral_storage, task_definition.get("ephemeralStorage"))
# )
# return False
# if self.runtime_platform is not None:
# if self.runtime_platform != task_definition.get("runtimePlatform"):
# logger.debug("{} != {}".format(self.runtime_platform, task_definition.get("runtimePlatform")))
# return False
#
# return True