Skip to content

Commit

Permalink
Add chalice stage option and deprecate existing arg
Browse files Browse the repository at this point in the history
The "chalice deploy <stage>" is deprecated.  Instead, I've
added two options in its place:

* --api-gateway-stage, for users that want the existing behavior
* --stage, for the new stage behavior which creates an entirely separate
  set of AWS resources.

I've also added a deprecation warning if you specify a positional stage
as well as param validation if you specify both the api gateway stage
as well as the positional stage param.

As part of this commit I've also updated the remaining commands that
need to be stage aware:

* package
* url
* generate-sdk
* logs

For each of these commands I've added a "--stage" option that uses the
deployed values JSON file to look up the appropriate stage values.
  • Loading branch information
jamesls committed Mar 31, 2017
1 parent c690afe commit 0783ff2
Show file tree
Hide file tree
Showing 10 changed files with 467 additions and 195 deletions.
8 changes: 6 additions & 2 deletions chalice/awsclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import botocore.session # noqa
from typing import Any, Optional, Dict, Callable, List # noqa

from chalice.constants import DEFAULT_STAGE_NAME


class TypedAWSClient(object):

Expand Down Expand Up @@ -244,7 +246,8 @@ def get_function_policy(self, function_name):
policy = client.get_policy(FunctionName=function_name)
return json.loads(policy['Policy'])

def download_sdk(self, rest_api_id, output_dir, api_gateway_stage='dev',
def download_sdk(self, rest_api_id, output_dir,
api_gateway_stage=DEFAULT_STAGE_NAME,
sdk_type='javascript'):
# type: (str, str, str, str) -> None
"""Download an SDK to a directory.
Expand Down Expand Up @@ -279,7 +282,8 @@ def download_sdk(self, rest_api_id, output_dir, api_gateway_stage='dev',
"The downloaded SDK had an unexpected directory structure: %s" %
(', '.join(dirnames)))

def get_sdk_download_stream(self, rest_api_id, api_gateway_stage='dev',
def get_sdk_download_stream(self, rest_api_id,
api_gateway_stage=DEFAULT_STAGE_NAME,
sdk_type='javascript'):
# type: (str, str, str) -> file
"""Generate an SDK for a given SDK.
Expand Down
266 changes: 140 additions & 126 deletions chalice/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import botocore.exceptions
import click
from botocore.session import Session # noqa
from typing import Dict, Any # noqa
from typing import Dict, Any, Optional # noqa

from chalice import __version__ as chalice_version
from chalice import prompts
Expand All @@ -23,58 +23,39 @@
from chalice.config import Config # noqa
from chalice.logs import LogRetriever
from chalice.utils import create_zip_file, record_deployed_values
from chalice.constants import CONFIG_VERSION, TEMPLATE_APP, GITIGNORE
from chalice.constants import DEFAULT_STAGE_NAME


# This is the version that's written to the config file
# on a `chalice new-project`. It's also how chalice is able
# to know when to warn you when changing behavior is introduced.
CONFIG_VERSION = '2.0'


TEMPLATE_APP = """\
from chalice import Chalice
app = Chalice(app_name='%s')
@app.route('/')
def index():
return {'hello': 'world'}
# The view function above will return {"hello": "world"}
# whenever you make an HTTP GET request to '/'.
#
# Here are a few more examples:
#
# @app.route('/hello/{name}')
# def hello_name(name):
# # '/hello/james' -> {"hello": "james"}
# return {'hello': name}
#
# @app.route('/users', methods=['POST'])
# def create_user():
# # This is the JSON body the user sent in their POST request.
# user_as_json = app.json_body
# # Suppose we had some 'db' object that we used to
# # read/write from our database.
# # user_id = db.create_user(user_as_json)
# return {'user_id': user_id}
#
# See the README documentation for more examples.
#
"""


GITIGNORE = """\
.chalice/deployments/
.chalice/venv/
"""
def create_new_project_skeleton(project_name, profile=None):
# type: (str, Optional[str]) -> None
chalice_dir = os.path.join(project_name, '.chalice')
os.makedirs(chalice_dir)
config = os.path.join(project_name, '.chalice', 'config.json')
cfg = {
'version': CONFIG_VERSION,
'app_name': project_name,
'stages': {
DEFAULT_STAGE_NAME: {
'api_gateway_stage': DEFAULT_STAGE_NAME,
}
}
}
if profile is not None:
cfg['profile'] = profile
with open(config, 'w') as f:
f.write(json.dumps(cfg, indent=2))
with open(os.path.join(project_name, 'requirements.txt'), 'w'):
pass
with open(os.path.join(project_name, 'app.py'), 'w') as f:
f.write(TEMPLATE_APP % project_name)
with open(os.path.join(project_name, '.gitignore'), 'w') as f:
f.write(GITIGNORE)


def show_lambda_logs(session, config, max_entries, include_lambda_messages):
# type: (Session, Config, int, bool) -> None
lambda_arn = config.lambda_arn
def show_lambda_logs(session, lambda_arn, max_entries,
include_lambda_messages):
# type: (Session, str, int, bool) -> None
client = session.create_client('logs')
retriever = LogRetriever.create_from_arn(client, lambda_arn)
events = retriever.retrieve_logs(
Expand Down Expand Up @@ -123,32 +104,68 @@ def local(ctx, port=8000):
default=True,
help='Automatically generate IAM policy for app code.')
@click.option('--profile', help='Override profile at deploy time.')
@click.argument('stage', nargs=1, required=False)
@click.option('--api-gateway-stage',
help='Name of the API gateway stage to deploy to.')
@click.option('--stage', default=DEFAULT_STAGE_NAME,
help=('Name of the Chalice stage to deploy to. '
'Specifying a new chalice stage will create '
'an entirely new set of AWS resources.'))
@click.argument('deprecated-api-gateway-stage', nargs=1, required=False)
@click.pass_context
def deploy(ctx, autogen_policy, profile, stage):
# type: (click.Context, bool, str, str) -> None
def deploy(ctx, autogen_policy, profile, api_gateway_stage, stage,
deprecated_api_gateway_stage):
# type: (click.Context, bool, str, str, str, str) -> None
if api_gateway_stage is not None and \
deprecated_api_gateway_stage is not None:
raise _create_deprecated_stage_error(api_gateway_stage,
deprecated_api_gateway_stage)
if deprecated_api_gateway_stage is not None:
# The "chalice deploy <stage>" is deprecated and will be removed
# in future versions. We'll support it for now, but let the
# user know to stop using this.
_warn_pending_removal(deprecated_api_gateway_stage)
api_gateway_stage = deprecated_api_gateway_stage
factory = ctx.obj['factory'] # type: CLIFactory
factory.profile = profile
config = factory.create_config_obj(
# Note: stage_name is not the same thing as the chalice stage.
# This is a legacy artifact that just means "API gateway stage",
# or for our purposes, the URL prefix.
chalice_stage_name='dev', autogen_policy=autogen_policy)
if stage is None:
stage = 'dev'
chalice_stage_name=stage, autogen_policy=autogen_policy)
session = factory.create_botocore_session()
d = factory.create_default_deployer(session=session, prompter=click)
try:
deployed_values = d.deploy(config, chalice_stage_name=stage)
record_deployed_values(deployed_values, os.path.join(
config.project_dir, '.chalice', 'deployed.json'))
except botocore.exceptions.NoRegionError:
e = click.ClickException("No region configured. "
"Either export the AWS_DEFAULT_REGION "
"environment variable or set the "
"region value in our ~/.aws/config file.")
e.exit_code = 2
raise e
deployed_values = d.deploy(config, chalice_stage_name=stage)
record_deployed_values(deployed_values, os.path.join(
config.project_dir, '.chalice', 'deployed.json'))


def _create_deprecated_stage_error(option, positional_arg):
# type: (str, str) -> click.ClickException
message = (
"You've specified both an '--api-gateway-stage' value of "
"'%s' as well as the positional API Gateway stage argument "
"'chalice deploy \"%s\"'.\n\n"
"The positional argument for API gateway stage ('chalice deploy "
"<api-gateway-stage>') is deprecated and support will be "
"removed in a future version of chalice.\nIf you want to "
"specify an API Gateway stage, just specify the "
"--api-gateway-stage option and remove the positional "
"stage argument.\n"
"If you want a completely separate set of AWS resources, "
"consider using the '--stage' argument."
) % (option, positional_arg)
exception = click.ClickException(message)
exception.exit_code = 2
return exception


def _warn_pending_removal(deprecated_stage):
# type: (str) -> None
click.echo("You've specified a deploy command of the form "
"'chalice deploy <stage>'\n"
"This form is deprecated and will be removed in a "
"future version of chalice.\n"
"You can use the --api-gateway-stage to achieve the "
"same functionality, or the newer '--stage' argument "
"if you want an entirely set of separate resources.",
err=True)


@cli.command()
Expand All @@ -157,13 +174,17 @@ def deploy(ctx, autogen_policy, profile, stage):
@click.option('--include-lambda-messages/--no-include-lambda-messages',
default=False,
help='Controls whether or not lambda log messages are included.')
@click.option('--stage', default=DEFAULT_STAGE_NAME)
@click.pass_context
def logs(ctx, num_entries, include_lambda_messages):
# type: (click.Context, int, bool) -> None
def logs(ctx, num_entries, include_lambda_messages, stage):
# type: (click.Context, int, bool, str) -> None
factory = ctx.obj['factory'] # type: CLIFactory
config = factory.create_config_obj('dev', False)
session = factory.create_botocore_session()
show_lambda_logs(session, config, num_entries, include_lambda_messages)
config = factory.create_config_obj(stage, False)
deployed = config.deployed_resources(stage)
if deployed is not None:
session = factory.create_botocore_session()
show_lambda_logs(session, deployed.api_handler_arn, num_entries,
include_lambda_messages)


@cli.command('gen-policy')
Expand All @@ -176,7 +197,7 @@ def gen_policy(ctx, filename):
if filename is None:
filename = os.path.join(ctx.obj['project_dir'], 'app.py')
if not os.path.isfile(filename):
click.echo("App file does not exist: %s" % filename)
click.echo("App file does not exist: %s" % filename, err=True)
raise click.Abort()
with open(filename) as f:
contents = f.read()
Expand All @@ -192,71 +213,57 @@ def new_project(project_name, profile):
if project_name is None:
project_name = prompts.getting_started_prompt(click)
if os.path.isdir(project_name):
click.echo("Directory already exists: %s" % project_name)
click.echo("Directory already exists: %s" % project_name, err=True)
raise click.Abort()
chalice_dir = os.path.join(project_name, '.chalice')
os.makedirs(chalice_dir)
config = os.path.join(project_name, '.chalice', 'config.json')
cfg = {
'version': CONFIG_VERSION,
'app_name': project_name,
'stages': {
'dev': {
'api_gateway_stage': 'dev'
}
}
}
if profile:
cfg['profile'] = profile
with open(config, 'w') as f:
f.write(json.dumps(cfg, indent=2))
with open(os.path.join(project_name, 'requirements.txt'), 'w'):
pass
with open(os.path.join(project_name, 'app.py'), 'w') as f:
f.write(TEMPLATE_APP % project_name)
with open(os.path.join(project_name, '.gitignore'), 'w') as f:
f.write(GITIGNORE)
create_new_project_skeleton(project_name, profile)


@cli.command('url')
@click.option('--stage', default=DEFAULT_STAGE_NAME)
@click.pass_context
def url(ctx):
# type: (click.Context) -> None
def url(ctx, stage):
# type: (click.Context, str) -> None
factory = ctx.obj['factory'] # type: CLIFactory
# TODO: Command should be stage aware!
config = factory.create_config_obj()
session = factory.create_botocore_session()
c = TypedAWSClient(session)
rest_api_id = c.get_rest_api_id(config.app_name)
api_gateway_stage = config.api_gateway_stage
region_name = c.region_name
click.echo(
"https://{api_id}.execute-api.{region}.amazonaws.com/{stage}/"
.format(api_id=rest_api_id, region=region_name,
stage=api_gateway_stage)
)
config = factory.create_config_obj(stage)
deployed = config.deployed_resources(stage)
if deployed is not None:
click.echo(
"https://{api_id}.execute-api.{region}.amazonaws.com/{stage}/"
.format(api_id=deployed.rest_api_id,
region=deployed.region,
stage=deployed.api_gateway_stage)
)
else:
e = click.ClickException(
"Could not find a record of deployed values to chalice stage: '%s'"
% stage)
e.exit_code = 2
raise e


@cli.command('generate-sdk')
@click.option('--sdk-type', default='javascript',
type=click.Choice(['javascript']))
@click.option('--stage', default=DEFAULT_STAGE_NAME)
@click.argument('outdir')
@click.pass_context
def generate_sdk(ctx, sdk_type, outdir):
# type: (click.Context, str, str) -> None
def generate_sdk(ctx, sdk_type, stage, outdir):
# type: (click.Context, str, str, str) -> None
factory = ctx.obj['factory'] # type: CLIFactory
config = factory.create_config_obj()
config = factory.create_config_obj(stage)
session = factory.create_botocore_session()
client = TypedAWSClient(session)
rest_api_id = client.get_rest_api_id(config.app_name)
api_gateway_stage = config.api_gateway_stage
if rest_api_id is None:
deployed = config.deployed_resources(stage)
if deployed is None:
click.echo("Could not find API ID, has this application "
"been deployed?")
"been deployed?", err=True)
raise click.Abort()
client.download_sdk(rest_api_id, outdir,
api_gateway_stage=api_gateway_stage,
sdk_type=sdk_type)
else:
rest_api_id = deployed.rest_api_id
api_gateway_stage = deployed.api_gateway_stage
client.download_sdk(rest_api_id, outdir,
api_gateway_stage=api_gateway_stage,
sdk_type=sdk_type)


@cli.command('package')
Expand All @@ -268,12 +275,13 @@ def generate_sdk(ctx, sdk_type, outdir):
"package assets will be placed. If "
"this argument is specified, a single "
"zip file will be created instead."))
@click.option('--stage', default=DEFAULT_STAGE_NAME)
@click.argument('out')
@click.pass_context
def package(ctx, single_file, out):
# type: (click.Context, bool, str) -> None
def package(ctx, single_file, stage, out):
# type: (click.Context, bool, str, str) -> None
factory = ctx.obj['factory'] # type: CLIFactory
config = factory.create_config_obj()
config = factory.create_config_obj(stage)
packager = factory.create_app_packager(config)
if single_file:
dirname = tempfile.mkdtemp()
Expand Down Expand Up @@ -301,6 +309,12 @@ def main():
# pylint: disable=unexpected-keyword-arg,no-value-for-parameter
try:
return cli(obj={})
except botocore.exceptions.NoRegionError:
click.echo("No region configured. "
"Either export the AWS_DEFAULT_REGION "
"environment variable or set the "
"region value in our ~/.aws/config file.", err=True)
return 2
except Exception as e:
click.echo(str(e))
click.echo(str(e), err=True)
return 2
Loading

0 comments on commit 0783ff2

Please sign in to comment.