Skip to content

Commit

Permalink
Introduce completely separate chalice stages
Browse files Browse the repository at this point in the history
This introduces a change to how stages work in chalice.
Previously a "stage" in chalice corresponded to an API gateway
stage.  While this could theoretically work for lambda functions
as well (via aliasing and function versions) it would not necessarily
scale with other AWS resources.  Now, chalice "stages" are
entirely new sets of resources.  For example, if I run:

$ chalice deploy dev  # Note 'dev' can be omitted, it's the default
$ chalice deploy prod

I will have two completely separate sets of API gateway rest APIs,
Lambda functions, and IAM roles.
To help track this, a ".chalice/deployed.json" is written on deploys
that contains the deployed values per-stage.

There's still some additional work remaining, primarily against
upgrading from old versions to this new version.  We should be warning
or erroring out in that scenario, but that will be addressed as a
separate commit.

Additionally, the "chalice url/logs" command can be updated to use
the ".chalice/deployed.json" file as well, but that will be added as
a separate commit.
  • Loading branch information
jamesls committed Mar 28, 2017
1 parent 6a5ca14 commit 93aa095
Show file tree
Hide file tree
Showing 7 changed files with 297 additions and 136 deletions.
15 changes: 13 additions & 2 deletions chalice/awsclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ def create_function(self, function_name, role_arn, zip_contents):
return response['FunctionArn']

def update_function_code(self, function_name, zip_contents):
# type: (str, str) -> None
self._client('lambda').update_function_code(
# type: (str, str) -> Dict[str, Any]
return self._client('lambda').update_function_code(
FunctionName=function_name, ZipFile=zip_contents)

def get_role_arn_for_name(self, name):
Expand Down Expand Up @@ -138,6 +138,17 @@ def get_rest_api_id(self, name):
return api['id']
return None

def rest_api_exists(self, rest_api_id):
# type: (str) -> bool
"""Check if an an API Gateway REST API exists."""
try:
self._client('apigateway').get_rest_api(restApiId=rest_api_id)
return True
except botocore.exceptions.ClientError as e:
if e['Code'] == 'NotFoundException':
return False
raise

def import_rest_api(self, swagger_document):
# type: (Dict[str, Any]) -> str
client = self._client('apigateway')
Expand Down
13 changes: 11 additions & 2 deletions chalice/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
from chalice.deploy import deployer
from chalice.logs import LogRetriever
from chalice.package import create_app_packager
from chalice.utils import record_deployed_values


TEMPLATE_APP = """\
from chalice import Chalice
Expand Down Expand Up @@ -168,13 +170,20 @@ def local(ctx, port=8000):
def deploy(ctx, autogen_policy, profile, stage):
# type: (click.Context, bool, str, str) -> None
config = create_config_obj(
ctx, stage_name=stage, autogen_policy=autogen_policy,
# 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.
ctx, stage_name='dev', autogen_policy=autogen_policy,
profile=profile)
if stage is None:
stage = 'dev'
session = create_botocore_session(profile=config.profile,
debug=ctx.obj['debug'])
d = deployer.create_default_deployer(session=session, prompter=click)
try:
d.deploy(config)
deployed_values = d.deploy(config, 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 "
Expand Down
62 changes: 56 additions & 6 deletions chalice/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from typing import Dict, Any # noqa
import os
import json

from typing import Dict, Any, Optional # noqa
from chalice.app import Chalice # noqa

StrMap = Dict[str, Any]
Expand Down Expand Up @@ -29,11 +32,6 @@ def create(cls, **kwargs):
# type: (**Any) -> Config
return cls(user_provided_params=kwargs.copy())

@property
def lambda_arn(self):
# type: () -> str
return self._chain_lookup('lambda_arn')

@property
def profile(self):
# type: () -> str
Expand Down Expand Up @@ -97,3 +95,55 @@ def _chain_lookup(self, name):
for cfg_dict in all_dicts:
if isinstance(cfg_dict, dict) and cfg_dict.get(name) is not None:
return cfg_dict[name]

@property
def lambda_arn(self):
# type: () -> str
return self._chain_lookup('lambda_arn')

def deployed_resources(self, stage_name):
# type: (str) -> Optional[DeployedResources]
"""Return resources associated with a given stage.
If a deployment to a given stage has never happened,
this method will return a value of None.
"""
# This is arguably the wrong level of abstraction.
# We might be able to move this elsewhere.
deployed_file = os.path.join(self.project_dir, '.chalice',
'deployed.json')
if not os.path.isfile(deployed_file):
return None
with open(deployed_file, 'r') as f:
data = json.load(f)
if stage_name not in data:
return None
return DeployedResources.from_dict(data[stage_name])


class DeployedResources(object):
def __init__(self, backend, api_handler_arn,
api_handler_name, rest_api_id, api_gateway_stage,
region, chalice_version):
# type: (str, str, str, str, str, str, str) -> None
self.backend = backend
self.api_handler_arn = api_handler_arn
self.api_handler_name = api_handler_name
self.rest_api_id = rest_api_id
self.api_gateway_stage = api_gateway_stage
self.region = region
self.chalice_version = chalice_version

@classmethod
def from_dict(cls, data):
# type: (Dict[str, str]) -> DeployedResources
return cls(
data['backend'],
data['api_handler_arn'],
data['api_handler_name'],
data['rest_api_id'],
data['api_gateway_stage'],
data['region'],
data['chalice_version'],
)
Loading

0 comments on commit 93aa095

Please sign in to comment.