Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sam package without awscli pre-installed #1437

Merged
merged 25 commits into from
Nov 5, 2019

Conversation

sriram-mv
Copy link
Contributor

@sriram-mv sriram-mv commented Sep 30, 2019

Will be making additional changes and adding tests.

  • rely on boto3 instead
  • port over code from aws cli

Issue #, if available:

Description of changes:

Checklist:

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Copy link
Contributor

@jfuss jfuss left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we planning on refactoring this more later? I know this is only the start but we will be incurring some tech debt with this if we keep package and deploy isolated from the rest of what we have built in this code base.

CLI command for "package" command
"""

from functools import partial
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the command.py classes, we should move all imports we can into functions. We did this in other commands to help with sam --help load time.

"""


@click.command("package", short_help=SHORT_HELP, context_settings={"ignore_unknown_options": True}, help=HELP_TEXT)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need the context_settings here?

profile,
):

with PackageCommandContext(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I don't think you need Command in the name. For invoke and build we have InvokeContext and BuildContext.

self.kwargs = kwargs


class InvalidTemplatePathError(CloudFormationCommandError):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be combining these with our existing exceptions? We have an InvalidSamTemplateException in samcli/commands/local/cli_common/user_exceptions.py

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totally, needs refactoring.

@@ -0,0 +1,67 @@
"""
Exceptions that are raised by sam package and sam deploy
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: These exceptions are under package but also apply to deploy. Should we have a higher level exception class that includes all user exceptions or ones shared across commands instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, needs refactoring.

msg = self.MSG_PACKAGED_TEMPLATE_WRITTEN.format(
output_file_name=output_file, output_file_path=os.path.abspath(output_file)
)
sys.stdout.write(msg)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move to click.echo

)

template_path = self.template_file
if not os.path.isfile(template_path):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check isn't needed if you expand the click.Path() or change it to click.File() in the --template-file option on the command.

https://click.palletsprojects.com/en/7.x/api/#click.Path


from samcli.commands.package import exceptions
from samcli.yamlhelper import yaml_dump, yaml_parse
import jmespath
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New dependency? We should be adding this to our base.txt file


if isinstance(url, str) and url.startswith("s3://"):

# Python < 2.7.10 don't parse query parameters from URI with custom
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there needed anymore since we have stopped support for 2.7

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, its from the port, will remove this.



class ServerlessFunctionResource(Resource):
RESOURCE_TYPE = "AWS::Serverless::Function"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the help text of the command, should we be adding what resources are supported?

How do we keep this in sync with https://github.com/awslabs/aws-sam-cli/blob/develop/samcli/commands/_utils/template.py#L19

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can do that.

@sriram-mv
Copy link
Contributor Author

Did some refactor and added the first integration test for sam package

- unit tests for package context class
- removal old `test_package.py`
- add unit tests for s3 uploader
- refactor of the port from `aws-cli` for artifact exporter
- add unit tests for artifact exporter
- added unit tests for the click wiring
- fix for smoke tests
- remove unneeded variables
- remove unneeded exceptions, inherit package specific exceptions from
  UserException
- refactor resources and their respective properties enums to its own
  file. This file is cross-referenced across package and other commands.
- include jmespath as explicit dependency
- refactor unit tests as required based on code structure changes.
Usage: samdev package [OPTIONS]

  The SAM package command creates a zip of your code and dependencies
and uploads it to S3. The command
  returns a copy of your template, replacing references to local
artifacts with the S3 location where the
  command uploaded the artifacts.

  The following resources and their property locations are supported.

  Resource : AWS::ServerlessRepo::Application | Location : LicenseUrl

  Resource : AWS::ServerlessRepo::Application | Location : ReadmeUrl

  Resource : AWS::Serverless::Function | Location : CodeUri

  Resource : AWS::Serverless::Api | Location : DefinitionUri

  Resource : AWS::AppSync::GraphQLSchema | Location :DefinitionS3Location

  Resource : AWS::AppSync::Resolver | Location : RequestMappingTemplateS3Location

  Resource : AWS::AppSync::Resolver | Location : ResponseMappingTemplateS3Location

  Resource : AWS::AppSync::FunctionConfiguration | Location : RequestMappingTemplateS3Location

  Resource : AWS::AppSync::FunctionConfiguration | Location : ResponseMappingTemplateS3Location

  Resource : AWS::Lambda::Function | Location : Code

  Resource : AWS::ApiGatewayRestApi | Location : BodyS3Location

  Resource : AWS::ElasticBeanstalk::ApplicationVersion | Location : SourceBundle

  Resource : AWS::CloudFormation::Stack | Location : TemplateURL

  Resource : AWS::Serverless::Application | Location : Location

  Resource : AWS::Lambda::LayerVersion | Location : Content

  Resource : AWS::Serverless::LayerVersion | Location : ContentUri

  Resource : AWS::Glue::Job | Location : Command.ScriptLocation
- add one barebones test to check `sam package` works
- With AWS::Serverless::Function Resource
@sriram-mv sriram-mv changed the title [WIP] feat: sam package without awscli pre-installed feat: sam package without awscli pre-installed Oct 24, 2019
@sriram-mv
Copy link
Contributor Author

Rebased to latest from develop

Copy link
Contributor

@jfuss jfuss left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still haven't gone through the tests. But looks really good over all.

Question:
Will this support PR add support for Globals in sam package?

@@ -221,7 +221,7 @@ notes=FIXME,XXX
[SIMILARITIES]

# Minimum lines number of a similarity.
min-similarity-lines=6
min-similarity-lines=12
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code smell alert?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its just number of arguments being similair.

==samcli.commands.package.command:110
==samcli.commands.package.package_context:47
        template_file,
        s3_bucket,
        s3_prefix,
        kms_key_id,
        output_template_file,
        use_json,
        force_upload,
        metadata,
        region,
        profile,
    ): (duplicate-code)
samcli/settings/__init__.py:1:0: R0801: Similar lines in 2 files
==samcli.commands.package.command:96
==samcli.commands.package.package_context:47
        template_file,
        s3_bucket,
        s3_prefix,
        kms_key_id,
        output_template_file,
        use_json,
        force_upload,
        metadata, (duplicate-code)

}


def resources_generator():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add doc strings for this? I having a hard time parsing what this is doing/what we would use it for

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, will add.

"AWS::Lambda::LayerVersion": ["Content"],
"AWS::Serverless::LayerVersion": ["ContentUri"],
}
from samcli.commands._utils.resources import METADATA_WITH_LOCAL_PATHS, RESOURCES_WITH_LOCAL_PATHS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NICE!!!

Exception.__init__(self, msg)
self.kwargs = kwargs

fmt = "S3 Bucket does not exist. " "Execute the command to create a new bucket" "\n" "aws s3 mb s3://{bucket_name}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will \n work on windows?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it should.

return result


class ProgressPercentage:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to investigate if click's progress bar has a callback function. Click's own Progressbar class seems to be in a private py file _term_ui_impl.py and it also takes an iterable upfront to determine progress, which we dont know in this case. So, might need retro-fitting to use it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with self._lock:
self._seen_so_far += bytes_transferred
percentage = (self._seen_so_far / self._size) * 100
sys.stdout.write(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be stdout or stderr?
#970

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thought, will change to stderr

@sriram-mv
Copy link
Contributor Author

Copy link
Contributor Author

@sriram-mv sriram-mv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still haven't gone through the tests. But looks really good over all.

Question:
Will this support PR add support for Globals in sam package?

In another PR. not this one.

- Additional metadata click parameter type
- Added unit, integration tests for `--metadata`
@sriram-mv sriram-mv force-pushed the package_off_awscli branch 2 times, most recently from dbdcf79 to 5b023d4 Compare October 25, 2019 18:49
- Dont allow list or dict in values portion of a metadata dict
- added tests for `AWS::ServerlessRepo::Application` Metadata field
- compare output template-file from `sam package` and `aws
  cloudformation package`
@sriram-mv
Copy link
Contributor Author

@jfuss
Copy link
Contributor

jfuss commented Oct 26, 2019

> I still haven't gone through the tests. But looks really good over all.

   Question:
   Will this support PR add support for Globals in sam package?

In another PR. not this one.

That is what I figured but wanted to ask anyways.

@@ -54,6 +54,8 @@ install:
- "venv\\Scripts\\activate"
- "python --version"

# Install AWS CLI
- "pip install awscli"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this temporary until we have things ported and confident in the changes?

This will install AWS CLI into the same venv as SAM CLI. Is there any issue with that (version conflicts potentials?)? If this is not temporary, we should be moving this to its own venv and putting the cli onto the path.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, my thinking was to keep regression suite running till we have to make an explicit change in behavior in our cli wrt to the commands, installation in a separate venv does make sense though. will address that! 👍


_pattern = r"([A-Za-z0-9\"]+)=([A-Za-z0-9\"]+)"

name = ""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is name used for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It isnt used for anything, but a custom type requires a name attribute, otherwise the help text crashes.


name = ""

def convert(self, value, param, ctx):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some comments above key sections here to make this method easier to understand. Looks like you are trying to parse the json way first and if that fails try the KeyName1=string, KeyName2=string2.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely, can add a docstring.

"""
for resource, locations in dict({**METADATA_WITH_LOCAL_PATHS, **RESOURCES_WITH_LOCAL_PATHS}).items():
for location in locations:
yield "\nResource : {resource} | Location : {location}\n".format(resource=resource, location=location)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"\nResource : {resource} | Location : {location}\n".format(resource=resource, location=location) could be f"\nResource : {resource} | Location : {location}\n" 🐍3!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, will change it.


def resources_generator():
"""
Generator to yield set of resources and their locations that are supported for package operations
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used for help text generation? Reading this I thought this was going to yield {(resource, location)} but instead its a string. Just making sure I understand this correctly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, its a string, but I can make it yield (resource, location) tuple instead and leave the construction of the string to a different function.

SHORT_HELP = "Package an AWS SAM application."


def resources_and_properties_help_string():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just have resources_generator return the string or move the logic within resources_generator here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having a generator of resource, location would be useful elsewhere too, is my opinion.


@click.command("package", short_help=SHORT_HELP, help=HELP_TEXT, context_settings=dict(max_content_width=120))
@click.option(
"--template-file",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just be expanding the --template option to include --template-file as well? This would allow customers to be consistent across SAM CLI?

click.option(
        "--template", "--template-file", "-t",
        default=_TEMPLATE_OPTION_DEFAULT_VALUE,
        type=click.Path(),
        envvar="SAM_TEMPLATE_FILE",
        callback=partial(get_or_default_template_file_name, include_build=include_build),
        show_default=True,
        help="AWS SAM template file",
    )

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense.

try:
package_context.run()
except OSError as ex:
raise UserException(str(ex))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is OSError a UserException? Should we have a more specific UserException Error here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its primarily to do with Permissions on template file and output template file.

Copy link
Contributor Author

@sriram-mv sriram-mv left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review!

@sriram-mv sriram-mv force-pushed the package_off_awscli branch 2 times, most recently from 0bca997 to 1d83aa7 Compare October 28, 2019 22:30
- template can now be specified as `-t`, `--template-file` and
  `--template`. Future PR will standardize it across the codebase.
  Current commit aims to fix it only in `package` space.

- exception handling on passing an unknown profile name

- refactor catching of OSError within package_context.py

- New exception class for package failures.
Copy link
Contributor

@jfuss jfuss left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing major just a couple small comments. I did notice that all of the files are under sam/commands/package/. We try and keep all command specific code under the sam/commands and move all 'library' related modules to sam/lib. artifact_exported and s3_uploader seem more like libs. Shouldn't we move them to follow the rest of the codebase's patterns?

@@ -49,6 +49,9 @@ install:
# Upgrade setuptools, wheel and virtualenv
- "python -m pip install --upgrade setuptools wheel virtualenv"

# Install AWS CLI Globally via pip3
- "pip3 install awscli"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pip3 in not on windows (according to build output)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moving back to pip.

appveyor.yml Outdated
@@ -56,7 +59,7 @@ for:
- "pylint --rcfile .pylintrc samcli"

# Runs only in Linux
- sh: "pytest -vv tests/integration"
- sh: "pytest -vv -n 4 tests/integration"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this work? Are all our tests able to do this? Will this cause further issues with timeouts and other things we have been seeing?

sh: won't run in windows. These lines can all be removed from the windows matrix.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should have added this for the regression suite, not here. will move.

try:
boto3.setup_default_session(region_name=self._aws_region, profile_name=self._aws_profile)
except botocore.exceptions.ProfileNotFound as ex:
raise UserException(str(ex))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have more explicit exceptions? All we will see in Telemetry is UserException is thrown (which happens in a ton of places).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, can add an explicit exception.



@click.command("package", short_help=SHORT_HELP, help=HELP_TEXT, context_settings=dict(max_content_width=120))
# TODO(TheSriram): Move to template_common_option across aws-sam-cli
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just do it now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will touch the full surface of commands across build, local etc. I'd rather have a separate PR for that.


process = Popen(command_list, stdout=PIPE)
process.wait()
self.assertTrue(process.returncode, 1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought assertTrue only takes in one expression? Unless the 1 is getting set to the message. Might be better to either remove 1 or make an explicit message.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to assertNotEqual to 0.

process_stdout = b"".join(process.stdout.readlines()).strip()

self.assertIn(
bytes(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can pass encoding into bytes builtin.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

command = [base]
if os.getenv("SAM_CLI_DEV") and base == "sam":
command = ["samdev"]
elif base == "aws":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does aws work on windows?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we are installing via pip, so the executable is aws.exe and should work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 47 to 55
with tempfile.NamedTemporaryFile(delete=False) as output_template_file_sam:
template_path = self.test_data_path.joinpath(template_file)
sam_command_list = self.get_command_list(
s3_bucket=self.s3_bucket.name,
template_file=template_path,
output_template_file=output_template_file_sam.name,
)
process = Popen(sam_command_list, stdout=PIPE)
process.wait()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be nice to have this abstracted. I know the regression suite is more short term, if you don't feel like it will really help in creating new tests until its removed than just ignore this comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

- move regression checking logic to base class
- address comments on exceptions, appveyor file, encoding
Copy link
Contributor

@jfuss jfuss left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work!!!

@sriram-mv sriram-mv mentioned this pull request Oct 31, 2019
6 tasks
@sriram-mv sriram-mv merged commit 7cbcfff into aws:develop Nov 5, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants