Skip to content

Commit

Permalink
feat(publish): Add --semantic-version option to sam publish command (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
paoptu023 authored and jfuss committed Mar 8, 2019
1 parent d7ff936 commit 43564aa
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 82 deletions.
21 changes: 14 additions & 7 deletions designs/sam_publish_cmd.rst
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ Create new version of an existing SAR application
Click the link below to view your application in AWS console:
https://console.aws.amazon.com/serverlessrepo/home?region=<region>#/published-applications/<arn>

Alternatively, you can provide the new version number through the --semantic-version option without manually modifying
the template. The command will publish a new application version using the specified value.

>>> sam publish -t ./packaged.yaml --semantic-version 0.0.2

Update the metadata of an existing application without creating new version
Keep SemanticVersion unchanged, then modify metadata fields like Description or ReadmeUrl, and run
``sam publish -t ./packaged.yaml``. SAM CLI prints application metadata updated message, values of updated
Expand Down Expand Up @@ -184,13 +189,15 @@ CLI Changes
$ sam publish -t packaged.yaml --region <region>
Options:
-t, --template PATH AWS SAM template file [default: template.[yaml|yml]]
--profile TEXT Select a specific profile from your credential file to
get AWS credentials.
--region TEXT Set the AWS Region of the service (e.g. us-east-1).
--debug Turn on debug logging to print debug message generated
by SAM CLI.
--help Show this message and exit.
-t, --template PATH AWS SAM template file [default: template.[yaml|yml]]
--semantic-version TEXT Optional. The value provided here overrides SemanticVersion
in the template metadata.
--profile TEXT Select a specific profile from your credential file to
get AWS credentials.
--region TEXT Set the AWS Region of the service (e.g. us-east-1).
--debug Turn on debug logging to print debug message generated
by SAM CLI.
--help Show this message and exit.
2. Update ``sam package`` (``aws cloudformation package``) command to support uploading locally referenced readme and
license files to S3.
Expand Down
2 changes: 1 addition & 1 deletion requirements/base.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ python-dateutil~=2.6
pathlib2~=2.3.2; python_version<"3.4"
requests==2.20.1
aws_lambda_builders==0.2.0
serverlessrepo==0.1.5
serverlessrepo==0.1.8
65 changes: 28 additions & 37 deletions samcli/commands/publish/command.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
"""CLI command for "publish" command."""

import json
import logging

import click
import boto3
from botocore.exceptions import ClientError
from serverlessrepo import publish_application
from serverlessrepo.publish import CREATE_APPLICATION
from serverlessrepo.exceptions import ServerlessRepoError
from serverlessrepo.parser import METADATA, SERVERLESS_REPO_APPLICATION
from serverlessrepo.exceptions import ServerlessRepoError, InvalidS3UriError

from samcli.cli.main import pass_context, common_options as cli_framework_options, aws_creds_options
from samcli.commands._utils.options import template_common_option
from samcli.commands._utils.template import get_template_data
from samcli.commands.exceptions import UserException

LOG = logging.getLogger(__name__)

SAM_PUBLISH_DOC = "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-template-publishing-applications.html" # noqa
SAM_PACKAGE_DOC = "https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-package.html" # noqa
HELP_TEXT = """
Use this command to publish a packaged AWS SAM template to
the AWS Serverless Application Repository to share within your team,
Expand All @@ -22,46 +27,57 @@
This command expects the template's Metadata section to contain an
AWS::ServerlessRepo::Application section with application metadata
for publishing. For more details on this metadata section, see
https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-template-publishing-applications.html
{}
\b
Examples
--------
To publish an application
$ sam publish -t packaged.yaml --region <region>
"""
""".format(SAM_PUBLISH_DOC)
SHORT_HELP = "Publish a packaged AWS SAM template to the AWS Serverless Application Repository."
SERVERLESSREPO_CONSOLE_URL = "https://console.aws.amazon.com/serverlessrepo/home?region={}#/published-applications/{}"
SEMANTIC_VERSION_HELP = "Optional. The value provided here overrides SemanticVersion in the template metadata."
SEMANTIC_VERSION = 'SemanticVersion'


@click.command("publish", help=HELP_TEXT, short_help=SHORT_HELP)
@template_common_option
@click.option('--semantic-version', help=SEMANTIC_VERSION_HELP)
@aws_creds_options
@cli_framework_options
@pass_context
def cli(ctx, template):
def cli(ctx, template, semantic_version):
# All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing

do_cli(ctx, template) # pragma: no cover
do_cli(ctx, template, semantic_version) # pragma: no cover


def do_cli(ctx, template):
def do_cli(ctx, template, semantic_version):
"""Publish the application based on command line inputs."""
try:
template_data = get_template_data(template)
except ValueError as ex:
click.secho("Publish Failed", fg='red')
raise UserException(str(ex))

# Override SemanticVersion in template metadata when provided in command input
if semantic_version and SERVERLESS_REPO_APPLICATION in template_data.get(METADATA, {}):
template_data.get(METADATA).get(SERVERLESS_REPO_APPLICATION)[SEMANTIC_VERSION] = semantic_version

try:
publish_output = publish_application(template_data)
click.secho("Publish Succeeded", fg="green")
click.secho(_gen_success_message(publish_output), fg="yellow")
except ServerlessRepoError as ex:
click.secho(_gen_success_message(publish_output))
except InvalidS3UriError:
click.secho("Publish Failed", fg='red')
raise UserException(str(ex))
except ClientError as ex:
raise UserException(
"Your SAM template contains invalid S3 URIs. Please make sure that you have uploaded application "
"artifacts to S3 by packaging the template. See more details in {}".format(SAM_PACKAGE_DOC))
except ServerlessRepoError as ex:
click.secho("Publish Failed", fg='red')
raise _wrap_s3_uri_exception(ex)
LOG.debug("Failed to publish application to serverlessrepo", exc_info=True)
error_msg = '{}\nPlease follow the instructions in {}'.format(str(ex), SAM_PUBLISH_DOC)
raise UserException(error_msg)

application_id = publish_output.get('application_id')
_print_console_link(ctx.region, application_id)
Expand Down Expand Up @@ -108,28 +124,3 @@ def _print_console_link(region, application_id):
console_link = SERVERLESSREPO_CONSOLE_URL.format(region, application_id.replace('/', '~'))
msg = "Click the link below to view your application in AWS console:\n{}".format(console_link)
click.secho(msg, fg="yellow")


def _wrap_s3_uri_exception(ex):
"""
Wrap invalid S3 URI exception with a better error message.
Parameters
----------
ex : ClientError
boto3 exception
Returns
-------
Exception
UserException if found invalid S3 URI or ClientError
"""
error_code = ex.response.get('Error').get('Code')
message = ex.response.get('Error').get('Message')

if error_code == 'BadRequestException' and "Invalid S3 URI" in message:
return UserException(
"Your SAM template contains invalid S3 URIs. Please make sure that you have uploaded application "
"artifacts to S3 by packaging the template: 'sam package --template-file <file-path>'.")

return ex
5 changes: 4 additions & 1 deletion tests/integration/publish/publish_app_integ_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def base_command(self):

return command

def get_command_list(self, template_path=None, region=None, profile=None):
def get_command_list(self, template_path=None, region=None, profile=None, semantic_version=None):
command_list = [self.base_command(), "publish"]

if template_path:
Expand All @@ -102,4 +102,7 @@ def get_command_list(self, template_path=None, region=None, profile=None):
if profile:
command_list = command_list + ["--profile", profile]

if semantic_version:
command_list = command_list + ["--semantic-version", semantic_version]

return command_list
18 changes: 18 additions & 0 deletions tests/integration/publish/test_command_integ.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from unittest import skipIf

from samcli.commands.publish.command import SEMANTIC_VERSION
from .publish_app_integ_base import PublishAppIntegBase

# Publish tests require credentials and Travis will only add credentials to the env if the PR is from the same repo.
Expand Down Expand Up @@ -59,6 +60,23 @@ def test_create_application_version(self):
app_metadata = json.loads(app_metadata_text)
self.assert_metadata_details(app_metadata, process_stdout.decode('utf-8'))

def test_create_application_version_with_semantic_version_option(self):
template_path = self.temp_dir.joinpath("template_create_app_version.yaml")
command_list = self.get_command_list(
template_path=template_path, region=self.region_name, semantic_version='0.1.0')

process = Popen(command_list, stdout=PIPE)
process.wait()
process_stdout = b"".join(process.stdout.readlines()).strip()

expected_msg = 'The following metadata of application "{}" has been updated:'.format(self.application_id)
self.assertIn(expected_msg, process_stdout.decode('utf-8'))

app_metadata_text = self.temp_dir.joinpath("metadata_create_app_version.json").read_text()
app_metadata = json.loads(app_metadata_text)
app_metadata[SEMANTIC_VERSION] = '0.1.0'
self.assert_metadata_details(app_metadata, process_stdout.decode('utf-8'))


@skipIf(SKIP_PUBLISH_TESTS, "Skip publish tests in Travis only")
class TestPublishNewApp(PublishAppIntegBase):
Expand Down
90 changes: 54 additions & 36 deletions tests/unit/commands/publish/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
from unittest import TestCase
from mock import patch, call, Mock

from botocore.exceptions import ClientError
from serverlessrepo.exceptions import ServerlessRepoError
from serverlessrepo.exceptions import ServerlessRepoError, InvalidS3UriError
from serverlessrepo.publish import CREATE_APPLICATION, UPDATE_APPLICATION
from serverlessrepo.parser import METADATA, SERVERLESS_REPO_APPLICATION

from samcli.commands.publish.command import do_cli as publish_cli
from samcli.commands.publish.command import do_cli as publish_cli, SEMANTIC_VERSION
from samcli.commands.exceptions import UserException


Expand All @@ -26,7 +26,7 @@ def setUp(self):
def test_must_raise_if_value_error(self, click_mock, get_template_data_mock):
get_template_data_mock.side_effect = ValueError("Template not found")
with self.assertRaises(UserException) as context:
publish_cli(self.ctx_mock, self.template)
publish_cli(self.ctx_mock, self.template, None)

message = str(context.exception)
self.assertEqual("Template not found", message)
Expand All @@ -38,42 +38,20 @@ def test_must_raise_if_value_error(self, click_mock, get_template_data_mock):
def test_must_raise_if_serverlessrepo_error(self, click_mock, publish_application_mock):
publish_application_mock.side_effect = ServerlessRepoError()
with self.assertRaises(UserException):
publish_cli(self.ctx_mock, self.template)
publish_cli(self.ctx_mock, self.template, None)

click_mock.secho.assert_called_with("Publish Failed", fg="red")

@patch('samcli.commands.publish.command.get_template_data', Mock(return_value={}))
@patch('samcli.commands.publish.command.publish_application')
@patch('samcli.commands.publish.command.click')
def test_must_raise_if_s3_uri_error(self, click_mock, publish_application_mock):
publish_application_mock.side_effect = ClientError(
{
'Error': {
'Code': 'BadRequestException',
'Message': 'Invalid S3 URI'
}
},
'create_application'
)
def test_must_raise_if_invalid_S3_uri_error(self, click_mock, publish_application_mock):
publish_application_mock.side_effect = InvalidS3UriError(message="")
with self.assertRaises(UserException) as context:
publish_cli(self.ctx_mock, self.template)
publish_cli(self.ctx_mock, self.template, None)

message = str(context.exception)
self.assertIn("Please make sure that you have uploaded application artifacts "
"to S3 by packaging the template", message)
click_mock.secho.assert_called_with("Publish Failed", fg="red")

@patch('samcli.commands.publish.command.get_template_data', Mock(return_value={}))
@patch('samcli.commands.publish.command.publish_application')
@patch('samcli.commands.publish.command.click')
def test_must_raise_if_not_s3_uri_error(self, click_mock, publish_application_mock):
publish_application_mock.side_effect = ClientError(
{'Error': {'Code': 'OtherError', 'Message': 'OtherMessage'}},
'other_operation'
)
with self.assertRaises(ClientError):
publish_cli(self.ctx_mock, self.template)

self.assertTrue("Your SAM template contains invalid S3 URIs" in message)
click_mock.secho.assert_called_with("Publish Failed", fg="red")

@patch('samcli.commands.publish.command.get_template_data', Mock(return_value={}))
Expand All @@ -86,7 +64,7 @@ def test_must_succeed_to_create_application(self, click_mock, publish_applicatio
'actions': [CREATE_APPLICATION]
}

publish_cli(self.ctx_mock, self.template)
publish_cli(self.ctx_mock, self.template, None)
details_str = json.dumps({'attr1': 'value1'}, indent=2)
expected_msg = "Created new application with the following metadata:\n{}"
expected_link = self.console_link.format(
Expand All @@ -95,7 +73,7 @@ def test_must_succeed_to_create_application(self, click_mock, publish_applicatio
)
click_mock.secho.assert_has_calls([
call("Publish Succeeded", fg="green"),
call(expected_msg.format(details_str), fg="yellow"),
call(expected_msg.format(details_str)),
call(expected_link, fg="yellow")
])

Expand All @@ -109,7 +87,7 @@ def test_must_succeed_to_update_application(self, click_mock, publish_applicatio
'actions': [UPDATE_APPLICATION]
}

publish_cli(self.ctx_mock, self.template)
publish_cli(self.ctx_mock, self.template, None)
details_str = json.dumps({'attr1': 'value1'}, indent=2)
expected_msg = 'The following metadata of application "{}" has been updated:\n{}'
expected_link = self.console_link.format(
Expand All @@ -118,7 +96,7 @@ def test_must_succeed_to_update_application(self, click_mock, publish_applicatio
)
click_mock.secho.assert_has_calls([
call("Publish Succeeded", fg="green"),
call(expected_msg.format(self.application_id, details_str), fg="yellow"),
call(expected_msg.format(self.application_id, details_str)),
call(expected_link, fg="yellow")
])

Expand All @@ -139,9 +117,49 @@ def test_print_console_link_if_context_region_not_set(self, click_mock, boto3_mo
session_mock.region_name = "us-west-1"
boto3_mock.Session.return_value = session_mock

publish_cli(self.ctx_mock, self.template)
publish_cli(self.ctx_mock, self.template, None)
expected_link = self.console_link.format(
session_mock.region_name,
self.application_id.replace('/', '~')
)
click_mock.secho.assert_called_with(expected_link, fg="yellow")

@patch('samcli.commands.publish.command.get_template_data')
@patch('samcli.commands.publish.command.publish_application')
def test_must_use_template_semantic_version(self, publish_application_mock,
get_template_data_mock):
template_data = {
METADATA: {
SERVERLESS_REPO_APPLICATION: {SEMANTIC_VERSION: '0.1'}
}
}
get_template_data_mock.return_value = template_data
publish_application_mock.return_value = {
'application_id': self.application_id,
'details': {}, 'actions': {}
}
publish_cli(self.ctx_mock, self.template, None)
publish_application_mock.assert_called_with(template_data)

@patch('samcli.commands.publish.command.get_template_data')
@patch('samcli.commands.publish.command.publish_application')
def test_must_override_template_semantic_version(self, publish_application_mock,
get_template_data_mock):
template_data = {
METADATA: {
SERVERLESS_REPO_APPLICATION: {SEMANTIC_VERSION: '0.1'}
}
}
get_template_data_mock.return_value = template_data
publish_application_mock.return_value = {
'application_id': self.application_id,
'details': {}, 'actions': {}
}

publish_cli(self.ctx_mock, self.template, '0.2')
expected_template_data = {
METADATA: {
SERVERLESS_REPO_APPLICATION: {SEMANTIC_VERSION: '0.2'}
}
}
publish_application_mock.assert_called_with(expected_template_data)

0 comments on commit 43564aa

Please sign in to comment.