diff --git a/cloudlift/__init__.py b/cloudlift/__init__.py index 3c7e4adf..dae4d6e6 100644 --- a/cloudlift/__init__.py +++ b/cloudlift/__init__.py @@ -9,6 +9,7 @@ from cloudlift.deployment import EnvironmentCreator, editor from cloudlift.config.logging import log_err from cloudlift.deployment.service_creator import ServiceCreator +from cloudlift.deployment.service_deletion import ServiceDeletion from cloudlift.deployment.service_information_fetcher import ServiceInformationFetcher from cloudlift.deployment.service_updater import ServiceUpdater from cloudlift.deployment.task_definition_creator import TaskDefinitionCreator @@ -170,5 +171,13 @@ def start_session(name, environment, mfa, component): SessionCreator(name, environment).start_session(mfa, component) +@cli.command(help="Delete a service. This will delete all nested \ +ECS services") +@_require_environment +@_require_name +def delete_service(name, environment): + ServiceDeletion(name, environment).delete_stack() + + if __name__ == '__main__': cli() diff --git a/cloudlift/deployment/service_deletion.py b/cloudlift/deployment/service_deletion.py new file mode 100644 index 00000000..560266e4 --- /dev/null +++ b/cloudlift/deployment/service_deletion.py @@ -0,0 +1,96 @@ +from time import sleep + +from cloudlift.config import get_client_for, get_service_stack_name +from cloudlift.config import ParameterStore +from cloudlift.config.logging import log, log_err, log_bold +from cloudlift.exceptions import UnrecoverableException +from cloudlift.deployment.progress import get_stack_events, print_new_events +from botocore.exceptions import ClientError + +class ServiceDeletion(object): + ''' + Delete CloudFormation stack for ECS service and related dependencies + ''' + def __init__(self, name, environment): + self.name = name + self.environment = environment + self.stack_name = get_service_stack_name(environment, name) + self.client = get_client_for('cloudformation', self.environment) + self.client_iam = get_client_for('iam', self.environment) + self.client_ssm = get_client_for('ssm', environment) + self.existing_events = get_stack_events(self.client, self.stack_name) + self.init_stack_info() + + def init_stack_info(self): + self.stack_name = get_service_stack_name(self.environment, self.name) + try: + self.stack_resource = self.client.describe_stack_resources(StackName=self.stack_name)['StackResources'] + except ClientError as boto_client_error: + if boto_client_error.response['Error']['Code'] == 'ValidationError' and 'does not exist' in boto_client_error.response['Error']['Message']: + raise UnrecoverableException( + "Stack " + self.stack_name + " does not exist") + raise UnrecoverableException(boto_client_error) + + def delete_stack(self): + self.delete_iam_role_policy() + try: + log("Deleting CloudFormation stack " + self.stack_name + "...") + self.client.delete_stack(StackName=self.stack_name) + self._print_progress() + self.delete_ssm_parameter() + return True + except ClientError as boto_client_error: + if boto_client_error.response['Error']['Code'] == 'ValidationError' and 'does not exist' in boto_client_error.response['Error']['Message']: + raise UnrecoverableException( + "Stack " + self.stack_name + " does not exist") + raise UnrecoverableException(boto_client_error) + + def delete_iam_role_policy(self): + + for resource in self.stack_resource: + if resource['LogicalResourceId'].endswith("Role") and resource['LogicalResourceId'] != "ECSServiceRole": + log("Deleting IAM Role Policy for "+ resource['LogicalResourceId'] + " resource") + try: + policies = self.client_iam.list_attached_role_policies(RoleName=resource['PhysicalResourceId']) + for policy in policies['AttachedPolicies']: + self.client_iam.detach_role_policy( + RoleName=resource['PhysicalResourceId'], PolicyArn=policy['PolicyArn']) + print(f"All the attached policies of {resource['PhysicalResourceId']} has been removed from IAM role") + except ClientError as boto_client_error: + if boto_client_error.response['Error']['Code'] == 'LimitExceededException': + log_bold("Limit Exceeded Exception, waiting for 5 sec..") + sleep(5) + continue + raise UnrecoverableException(boto_client_error) + + def delete_ssm_parameter(self): + log("Deleting SSM Parameters..") + parameter_store = ParameterStore(self.name, self.environment) + environment_configs, environment_configs_path = parameter_store.get_existing_config() + for k, v in environment_configs_path.items(): + try: + self.client_ssm.delete_parameter( + Name=v + ) + except ClientError as boto_client_error: + if boto_client_error.response['Error']['Code'] == 'ParameterNotFound': + log_err(f"Parameter {v} does not exist") + continue + else: + raise UnrecoverableException(boto_client_error) + + def _print_progress(self): + while True: + try: + response = self.client.describe_stacks(StackName=self.stack_name) + if "DELETE_IN_PROGRESS" not in response['Stacks'][0]['StackStatus']: + break + all_events = get_stack_events(self.client, self.stack_name) + print_new_events(all_events, self.existing_events) + self.existing_events = all_events + sleep(5) + except ClientError as boto_client_error: + if boto_client_error.response['Error']['Code'] == 'ValidationError' and 'does not exist' in boto_client_error.response['Error']['Message']: + log_bold("Stack " + self.stack_name + " deleted") + break + raise UnrecoverableException(boto_client_error) \ No newline at end of file diff --git a/cloudlift/version/__init__.py b/cloudlift/version/__init__.py index 99d56a09..47f23af8 100644 --- a/cloudlift/version/__init__.py +++ b/cloudlift/version/__init__.py @@ -1 +1 @@ -VERSION = '1.5.7' \ No newline at end of file +VERSION = '2.0.0' \ No newline at end of file