From 24d59ee57f93c7f0af28f30a5bea54efe1471dae Mon Sep 17 00:00:00 2001 From: Andrew May Date: Sun, 5 Jul 2020 14:07:34 -0400 Subject: [PATCH] #12 - Add upload utility method --- README.md | 20 ++++++++++++++++++ setup.py | 2 +- stackmanager/cli.py | 25 ++++++++++++++++++++-- stackmanager/exceptions.py | 5 +++++ stackmanager/uploader.py | 43 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 stackmanager/uploader.py diff --git a/README.md b/README.md index d0acc55..093ca6a 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ Stackmanager has the following commands: * `deploy` - Create or update a CloudFormation stack for a specific environment/region using a ChangeSet. By default exits after creating the changeset, but can `--auto-apply`. * `apply` - Apply a previously created ChangeSet * `delete` - Delete an existing CloudFormation stack +* `upload` - Uploads a local file to S3. Utility method to prevent the need to use the AWS CLI or other tools. ### deploy @@ -160,6 +161,25 @@ Options: --help Show this message and exit. ``` +### Upload + +``` +Usage: stackmanager upload [OPTIONS] + + Uploads a File to S3. This might be a large CloudFormation template, or a + Lambda zip file + +Options: + -p, --profile TEXT AWS Profile, will use default or environment variables + if not specified + + -r, --region TEXT AWS Region to upload to [required] + -f, --filename TEXT File to upload [required] + -b, --bucket TEXT Bucket to upload to [required] + -k, --key TEXT Key to upload to [required] + --help Show this message and exit. +``` + ## CI/CD Pipeline support ### Azure DevOps diff --git a/setup.py b/setup.py index de741ce..ee227d9 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='stackmanager', - version='0.4', + version='0.5', description='Utility to manage CloudFormation stacks using YAML configuration file', long_description=README, long_description_content_type='text/markdown', diff --git a/stackmanager/cli.py b/stackmanager/cli.py index 0f3142b..c04e38a 100644 --- a/stackmanager/cli.py +++ b/stackmanager/cli.py @@ -1,9 +1,10 @@ #!/usr/bin/env python import click -from stackmanager.exceptions import StackError, ValidationError +from stackmanager.exceptions import StackError, TransferError, ValidationError from stackmanager.loader import load_config from stackmanager.messages import error from stackmanager.runner import create_runner +from stackmanager.uploader import create_uploader @click.group(chain=True) @@ -78,5 +79,25 @@ def delete(ctx, profile, config, environment, region, retain_resources): exit(1) +@cli.command() +@click.pass_context +@click.option('-p', '--profile', help='AWS Profile, will use default or environment variables if not specified') +@click.option('-r', '--region', required=True, help='AWS Region to upload to') +@click.option('-f', '--filename', required=True, help='File to upload') +@click.option('-b', '--bucket', required=True, help='Bucket to upload to') +@click.option('-k', '--key', required=True, help='Key to upload to') +def upload(ctx, profile, region, filename, bucket, key): + """ + Uploads a File to S3. + This might be a large CloudFormation template, or a Lambda zip file. + """ + try: + uploader = create_uploader(profile, region) + uploader.upload(filename, bucket, key) + except (TransferError, ValidationError) as e: + error(f'\nError: {e}') + exit(1) + + if __name__ == '__main__': - cli() + cli(prog_name='stackmanager') diff --git a/stackmanager/exceptions.py b/stackmanager/exceptions.py index 74cdb5f..bad3759 100644 --- a/stackmanager/exceptions.py +++ b/stackmanager/exceptions.py @@ -6,3 +6,8 @@ class ValidationError(Exception): class StackError(Exception): """Exception thrown when there is an AWS Error managing a CloudFormation stack""" pass + + +class TransferError(Exception): + """Exception thrown when there is an AWS Error transferring files to S3""" + pass diff --git a/stackmanager/uploader.py b/stackmanager/uploader.py new file mode 100644 index 0000000..167fbbb --- /dev/null +++ b/stackmanager/uploader.py @@ -0,0 +1,43 @@ +import boto3 +from boto3.exceptions import Boto3Error +from botocore.exceptions import ClientError +from stackmanager.exceptions import TransferError, ValidationError +from stackmanager.messages import info + + +class Uploader: + """Utility for uploading files to S3""" + + def __init__(self, client): + self._client = client + + 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 create_uploader(profile, region): + """ + Create a new Uploader + :param profile: AWS Profile + :param region: AWS Region + :return: Configured Uploader + """ + session = boto3.Session(profile_name=profile, region_name=region) + client = session.client('s3') + return Uploader(client)