def reject_change_set(self): """ Delete the named ChangeSet, and if the stack is in the REVIEW_IN_PROGRESS status and there are no remaining ChangeSets, then delete the stack. """ if not self.stack: raise ValidationError(f'Stack {self.config.stack_name} not found') self.print_stack_status() try: self.client.describe_change_set(ChangeSetName=self.change_set_name, StackName=self.config.stack_name) except self.client.exceptions.ChangeSetNotFoundException: raise ValidationError( f'ChangeSet {self.change_set_name} not found') info( f'\nDeleting ChangeSet {self.change_set_name} for {self.config.stack_name}' ) self.client.delete_change_set(ChangeSetName=self.change_set_name, StackName=self.config.stack_name) if StackStatus.is_status(self.stack, StackStatus.REVIEW_IN_PROGRESS): change_sets = self.client.list_change_sets( StackName=self.config.stack_name)['Summaries'] if not change_sets: info( f'\nDeleting REVIEW_IN_PROGRESS Stack {self.config.stack_name} that has no remaining ChangeSets' ) # Delete stack, no need to wait for deletion to complete self.client.delete_stack(StackName=self.config.stack_name)
def build_lambda(source_dir, output_dir, runtime, archive_name): """ Build a Lambda Function zip using a builder from aws-lambda-builders :param source_dir: Source Directory :param output_dir: Output Directory :param runtime: Lambda Runtime :param archive_name: Archive name (optional) :return: Path to generated zip file """ config = get_config(runtime, source_dir) builder = LambdaBuilder(config.language, config.dependency_manager, None) manifest_path = os.path.join(source_dir, config.manifest_name) archive_name = archive_name if archive_name else os.path.basename(os.path.normpath(source_dir)) info(f'\nBuilding {runtime} Lambda function from {source_dir}\n') with tempfile.TemporaryDirectory() as artifacts_dir: with tempfile.TemporaryDirectory() as scratch_dir: try: builder.build(source_dir, artifacts_dir, scratch_dir, manifest_path, runtime) zip_file = make_archive(os.path.join(output_dir, archive_name), 'zip', artifacts_dir) info(f'\nBuilt Lambda Archive {zip_file}') return zip_file except LambdaBuilderError as e: raise PackagingError(e)
def deploy(self): """ Create a new Stack or update an existing Stack using a ChangeSet. This will wait for ChangeSet to be created and print the details, and if Auto-Approve is set, will then execute the ChangeSet and wait for that to complete. If there are no changes in the ChangeSet it will be automatically deleted. :raises StackError: If creating the ChangeSet or executing it fails """ if not StackStatus.is_creatable(self.stack) and not StackStatus.is_updatable(self.stack): stack_status = StackStatus.get_status(self.stack) if stack_status == StackStatus.ROLLBACK_COMPLETE: warn(f'Deleting Stack {self.config.stack_name} in ROLLBACK_COMPLETE status before attempting to create') self.delete() else: raise ValidationError(f'Stack {self.config.stack_name} is not in a deployable status: ' f'{stack_status.name}') info(f'\nCreating ChangeSet {self.change_set_name}\n') try: self.client.create_change_set(**self.build_change_set_args()) if self.wait_for_change_set(): if self.auto_approve: self.execute_change_set() else: self.pending_change_set() except ClientError as ce: raise StackError(ce)
def delete(self, retain_resources=[]): """ Delete a Stack, optionally retraining certain resources. Waits for Stack to delete and prints Events if deletion fails :param list retain_resources: List of LogicalIds to retain :raises StackError: if deletion fails """ if not self.stack: raise ValidationError(f'Stack {self.config.stack_name} not found') if not StackStatus.is_deletable(self.stack): raise ValidationError(f'Stack {self.config.stack_name} is not in a deletable status: ' f'{StackStatus.get_status(self.stack).name}') info(f'\nDeleting Stack {self.config.stack_name}') last_timestamp = self.get_last_timestamp() try: self.client.delete_stack(StackName=self.config.stack_name, RetainResources=retain_resources) self.client.get_waiter('stack_delete_complete').wait( StackName=self.config.stack_name, WaiterConfig={'Delay': 10, 'MaxAttempts': 360}) info(f'\nDeletion of Stack {self.config.stack_name} successfully completed') self.stack = None except ClientError as ce: raise StackError(ce) except WaiterError as we: error(f'\nDeletion of Stack {self.config.stack_name} failed:\n') self.print_events(last_timestamp) raise StackError(we)
def execute_change_set(self): """ Execute a ChangeSet, waiting for execution to complete and printing the details of Stack Events caused by this ChangeSet :raises StackError: If there is an error executing the ChangeSet """ last_timestamp = self.get_last_timestamp() try: info(f'\nExecuting ChangeSet {self.change_set_name} for {self.config.stack_name}') self.client.execute_change_set(ChangeSetName=self.change_set_name, StackName=self.config.stack_name) waiter_name = 'stack_create_complete' if StackStatus.is_creatable(self.stack) else 'stack_update_complete' self.client.get_waiter(waiter_name).wait(StackName=self.config.stack_name, WaiterConfig={'Delay': 10, 'MaxAttempts': 360}) info(f'\nChangeSet {self.change_set_name} for {self.config.stack_name} successfully completed:\n') self.print_events(last_timestamp) except ClientError as ce: raise StackError(ce) except WaiterError as we: error(f'\nChangeSet {self.change_set_name} for {self.config.stack_name} failed:\n') self.print_events(last_timestamp) raise StackError(we)
def update_termination_protection(self, deleting=False): protect = False if deleting else self.config.termination_protection current = False if not self.stack else self.stack.get( 'EnableTerminationProtection', False) if protect is not current: self.client.update_termination_protection( StackName=self.config.stack_name, EnableTerminationProtection=protect) info( f'\n{"Enabled" if protect else "Disabled"} Termination Protection' )
def print_stack_status(self): """ Print the status of the stack if it exists or does not, with last update (or creation) timestamp. """ if self.stack: stack_timestamp = self.stack['LastUpdatedTime'] if 'LastUpdatedTime' in self.stack \ else self.stack['CreationTime'] info( f'\nStack: {self.config.stack_name}, Status: {StackStatus.get_status(self.stack).name} ' f'({self.format_timestamp(stack_timestamp)})') else: info(f'\nStack: {self.config.stack_name}, Status: does not exist')
def status(self, event_days): """ Output the current status of the stack showing pending ChangeSets and recent events. :param int event_days: Number of days to show events for """ self.print_stack_status() self.check_change_sets() if self.stack: last_timestamp = arrow.utcnow().shift(days=-event_days) info(f'\nEvents since {self.format_timestamp(last_timestamp)}:\n') self.print_events(last_timestamp.datetime) self.print_outputs()
def load_stack(self): """ Load Description of stack to determine if it exists and the current status :return: Matching stack of None """ try: stacks = self.client.describe_stacks(StackName=self.config.stack_name)['Stacks'] stack = stacks[0] info(f'\nStack: {self.config.stack_name}, Status: {StackStatus.get_status(stack).name}') return stack except ClientError: info(f'\nStack: {self.config.stack_name}, Status: does not exist') return None
def upload(self, filename, bucket, key, acl='bucket-owner-full-control'): """ Uploads a local file to S3. :param filename: Name of local file :param bucket: S3 Bucket Name :param key: S3 Key :param acl: Object ACL, defaults to bucket-owner-full-control :raises ValidationError: If local file does not exist :raises TransferError: If there is an error uploading the file """ try: self._client.upload_file(Filename=filename, Bucket=bucket, Key=key, ExtraArgs={'ACL': acl}) info(f'\nUploaded {filename} to s3://{bucket}/{key}') except FileNotFoundError: raise ValidationError(f'File {filename} not found') except (Boto3Error, ClientError) as e: raise TransferError(e)
def print_outputs(self): if self.stack and 'Outputs' in self.stack: info('\nOutputs:\n') data = [[o['OutputKey'], o['OutputValue']] for o in self.stack['Outputs']] table(data, ['Key', 'Value'])
def pending_change_set(self, change_set_id): """Subclasses can override this to export the change set information in another format""" info(f'\nChangeSet {self.change_set_name} is ready to run')