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

Add support for environment_variables config option #273

Merged
merged 3 commits into from
Apr 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions chalice/awsclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ def lambda_function_exists(self, name):
except client.exceptions.ResourceNotFoundException:
return False

def create_function(self, function_name, role_arn, zip_contents):
# type: (str, str, str) -> str
def create_function(self, function_name, role_arn, zip_contents,
environment_variables=None):
# type: (str, str, str, Optional[Dict[str, str]]) -> str
kwargs = {
'FunctionName': function_name,
'Runtime': 'python2.7',
Expand All @@ -58,6 +59,8 @@ def create_function(self, function_name, role_arn, zip_contents):
'Role': role_arn,
'Timeout': 60,
}
if environment_variables is not None:
kwargs['Environment'] = {"Variables": environment_variables}
client = self._client('lambda')
attempts = 0
while True:
Expand All @@ -75,10 +78,23 @@ def create_function(self, function_name, role_arn, zip_contents):
continue
return response['FunctionArn']

def update_function_code(self, function_name, zip_contents):
# type: (str, str) -> Dict[str, Any]
return self._client('lambda').update_function_code(
def update_function(self, function_name, zip_contents,
environment_variables=None):
# type: (str, str, Optional[Dict[str, str]]) -> Dict[str, Any]
lambda_client = self._client('lambda')
return_value = lambda_client.update_function_code(
FunctionName=function_name, ZipFile=zip_contents)
if environment_variables is None:
environment_variables = {}
# We need to handle the case where the user removes
# all env vars from their config.json file. We'll
# just call update_function_configuration every time.
# We're going to need this moving forward anyways,
# more config options besides env vars will be added.
lambda_client.update_function_configuration(
FunctionName=function_name,
Environment={"Variables": environment_variables})
return return_value

def get_role_arn_for_name(self, name):
# type: (str) -> str
Expand Down
4 changes: 2 additions & 2 deletions chalice/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,12 +286,12 @@ def package(ctx, single_file, stage, out):
if single_file:
dirname = tempfile.mkdtemp()
try:
packager.package_app(dirname)
packager.package_app(config, dirname)
create_zip_file(source_dir=dirname, outfile=out)
finally:
shutil.rmtree(dirname)
else:
packager.package_app(out)
packager.package_app(config, out)


def run_local_server(app_obj, port):
Expand Down
24 changes: 24 additions & 0 deletions chalice/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,25 @@ def _chain_lookup(self, name, varies_per_chalice_stage=False):
if isinstance(cfg_dict, dict) and cfg_dict.get(name) is not None:
return cfg_dict[name]

def _chain_merge(self, name):
# type: (str) -> Dict[str, Any]
# Merge values for all search dicts instead of returning on first
# found.
search_dicts = [
# This is reverse order to _chain_lookup().
self._default_params,
self._config_from_disk,
self._config_from_disk.get('stages', {}).get(
self.chalice_stage, {}),
self._user_provided_params,
]
final = {}
for cfg_dict in search_dicts:
value = cfg_dict.get(name, {})
if isinstance(value, dict):
final.update(value)
return final

@property
def config_file_version(self):
# type: () -> str
Expand Down Expand Up @@ -178,6 +197,11 @@ def autogen_policy(self):
return self._chain_lookup('autogen_policy',
varies_per_chalice_stage=True)

@property
def environment_variables(self):
# type: () -> Dict[str, str]
return self._chain_merge('environment_variables')

def deployed_resources(self, chalice_stage_name):
# type: (str) -> Optional[DeployedResources]
"""Return resources associated with a given stage.
Expand Down
10 changes: 6 additions & 4 deletions chalice/deploy/deployer.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,11 @@ def _first_time_lambda_create(self, config, function_name):
role_arn = self._get_or_create_lambda_role_arn(config, function_name)
zip_filename = self._packager.create_deployment_package(
config.project_dir)
with open(zip_filename, 'rb') as f:
zip_contents = f.read()
zip_contents = self._osutils.get_file_contents(
zip_filename, binary=True)
return self._aws_client.create_function(
function_name, role_arn, zip_contents)
function_name, role_arn, zip_contents,
config.environment_variables)

def _update_lambda_function(self, config, lambda_name):
# type: (Config, str) -> None
Expand All @@ -264,7 +265,8 @@ def _update_lambda_function(self, config, lambda_name):
zip_contents = self._osutils.get_file_contents(
deployment_package_filename, binary=True)
print "Sending changes to lambda."
self._aws_client.update_function_code(lambda_name, zip_contents)
self._aws_client.update_function(lambda_name, zip_contents,
config.environment_variables)

def _write_config_to_disk(self, config):
# type: (Config) -> None
Expand Down
55 changes: 28 additions & 27 deletions chalice/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,8 @@ def create_app_packager(config):
config,
ApplicationPolicyHandler(
osutils, AppPolicyGenerator(osutils)))),
LambdaDeploymentPackager(),
# TODO: remove duplication here.
ApplicationPolicyHandler(osutils, AppPolicyGenerator(osutils)),
config.chalice_app,
config.project_dir,
config.autogen_policy)
LambdaDeploymentPackager()
)


class PreconfiguredPolicyGenerator(object):
Expand Down Expand Up @@ -67,7 +63,9 @@ class SAMTemplateGenerator(object):
'Value': {
'Fn::Sub': (
'https://${RestAPI}.execute-api.${AWS::Region}'
'.amazonaws.com/dev/'
# The api_gateway_stage is filled in when
# the template is built.
'.amazonaws.com/%s/'
)
}
}
Expand All @@ -79,26 +77,37 @@ def __init__(self, swagger_generator, policy_generator):
self._swagger_generator = swagger_generator
self._policy_generator = policy_generator

def generate_sam_template(self, app, code_uri='<placeholder>',
api_gateway_stage='dev'):
# type: (Chalice, str, str) -> Dict[str, Any]
def generate_sam_template(self, config, code_uri='<placeholder>'):
# type: (Config, str) -> Dict[str, Any]
template = copy.deepcopy(self._BASE_TEMPLATE)
resources = {
'APIHandler': self._generate_serverless_function(app, code_uri),
'RestAPI': self._generate_rest_api(app, api_gateway_stage),
'APIHandler': self._generate_serverless_function(config, code_uri),
'RestAPI': self._generate_rest_api(
config.chalice_app, config.api_gateway_stage),
}
template['Resources'] = resources
self._update_endpoint_url_output(template, config)
return template

def _generate_serverless_function(self, app, code_uri):
# type: (Chalice, str) -> Dict[str, Any]
def _update_endpoint_url_output(self, template, config):
# type: (Dict[str, Any], Config) -> None
url = template['Outputs']['EndpointURL']['Value']['Fn::Sub']
template['Outputs']['EndpointURL']['Value']['Fn::Sub'] = (
url % config.api_gateway_stage)

def _generate_serverless_function(self, config, code_uri):
# type: (Config, str) -> Dict[str, Any]
properties = {
'Runtime': 'python2.7',
'Handler': 'app.app',
'CodeUri': code_uri,
'Events': self._generate_function_events(app),
'Events': self._generate_function_events(config.chalice_app),
'Policies': [self._generate_iam_policy()],
}
if config.environment_variables:
properties['Environment'] = {
'Variables': config.environment_variables
}
return {
'Type': 'AWS::Serverless::Function',
'Properties': properties,
Expand Down Expand Up @@ -144,33 +153,25 @@ class AppPackager(object):
def __init__(self,
sam_templater, # type: SAMTemplateGenerator
lambda_packager, # type: LambdaDeploymentPackager
policy_gen, # type: ApplicationPolicyHandler
app, # type: Chalice
project_dir, # type: str
autogen_policy=True # type: bool
):
# type: (...) -> None
self._sam_templater = sam_templater
self._lambda_packaager = lambda_packager
self._app = app
self._policy_gen = policy_gen
self._project_dir = project_dir
self._autogen_policy = autogen_policy

def _to_json(self, doc):
# type: (Any) -> str
return json.dumps(doc, indent=2, separators=(',', ': '))

def package_app(self, outdir):
# type: (str) -> None
def package_app(self, config, outdir):
# type: (Config, str) -> None
# Deployment package
zip_file = os.path.join(outdir, 'deployment.zip')
self._lambda_packaager.create_deployment_package(
self._project_dir, zip_file)
config.project_dir, zip_file)

# SAM template
sam_template = self._sam_templater.generate_sam_template(
self._app, './deployment.zip')
config, './deployment.zip')
if not os.path.isdir(outdir):
os.makedirs(outdir)
with open(os.path.join(outdir, 'sam.json'), 'w') as f:
Expand Down
28 changes: 24 additions & 4 deletions tests/functional/test_awsclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,31 @@ def test_deploy_rest_api(stubbed_session):
stubbed_session.verify_stubs()


def test_update_function_code(stubbed_session):
stubbed_session.stub('lambda').update_function_code(
def test_always_update_function_code(stubbed_session):
lambda_client = stubbed_session.stub('lambda')
lambda_client.update_function_code(
FunctionName='name', ZipFile=b'foo').returns({})
# Even if there's only a code change, we'll always call
# update_function_configuration.
lambda_client.update_function_configuration(
FunctionName='name', Environment={'Variables': {}}).returns({})
stubbed_session.activate_stubs()
awsclient = TypedAWSClient(stubbed_session)
awsclient.update_function_code('name', b'foo')
awsclient.update_function('name', b'foo')
stubbed_session.verify_stubs()


def test_update_function_code_and_deploy(stubbed_session):
lambda_client = stubbed_session.stub('lambda')
lambda_client.update_function_code(
FunctionName='name', ZipFile=b'foo').returns({})
lambda_client.update_function_configuration(
FunctionName='name',
Environment={'Variables': {"FOO": "BAR"}}).returns({})
stubbed_session.activate_stubs()
awsclient = TypedAWSClient(stubbed_session)
awsclient.update_function(
'name', b'foo', {"FOO": "BAR"})
stubbed_session.verify_stubs()


Expand Down Expand Up @@ -215,11 +234,12 @@ def test_create_function_succeeds_first_try(self, stubbed_session):
Handler='app.app',
Role='myarn',
Timeout=60,
Environment={'Variables': {'FOO': 'BAR'}},
).returns({'FunctionArn': 'arn:12345:name'})
stubbed_session.activate_stubs()
awsclient = TypedAWSClient(stubbed_session)
assert awsclient.create_function(
'name', 'myarn', b'foo') == 'arn:12345:name'
'name', 'myarn', b'foo', {'FOO': 'BAR'}) == 'arn:12345:name'
stubbed_session.verify_stubs()

def test_create_function_is_retried_and_succeeds(self, stubbed_session):
Expand Down
4 changes: 2 additions & 2 deletions tests/functional/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def test_can_create_app_packager_with_no_autogen(tmpdir):
config = Config.create(project_dir=str(appdir),
chalice_app=sample_app())
p = package.create_app_packager(config)
p.package_app(str(outdir))
p.package_app(config, str(outdir))
# We're not concerned with the contents of the files
# (those are tested in the unit tests), we just want to make
# sure they're written to disk and look (mostly) right.
Expand All @@ -45,7 +45,7 @@ def test_will_create_outdir_if_needed(tmpdir):
config = Config.create(project_dir=str(appdir),
chalice_app=sample_app())
p = package.create_app_packager(config)
p.package_app(outdir)
p.package_app(config, str(outdir))
contents = os.listdir(str(outdir))
assert 'deployment.zip' in contents
assert 'sam.json' in contents
36 changes: 32 additions & 4 deletions tests/unit/deploy/test_deployer.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ def test_lambda_deployer_repeated_deploy(app_policy, sample_app):
packager.deployment_package_filename.return_value = 'packages.zip'
# Given the lambda function already exists:
aws_client.lambda_function_exists.return_value = True
aws_client.update_function_code.return_value = {"FunctionArn": "myarn"}
aws_client.update_function.return_value = {"FunctionArn": "myarn"}
# And given we don't want chalice to manage our iam role for the lambda
# function:
cfg = Config.create(
Expand All @@ -347,7 +347,8 @@ def test_lambda_deployer_repeated_deploy(app_policy, sample_app):
manage_iam_role=False,
app_name='appname',
iam_role_arn=True,
project_dir='./myproject'
project_dir='./myproject',
environment_variables={"FOO": "BAR"},
)

d = LambdaDeployer(aws_client, packager, None, osutils, app_policy)
Expand All @@ -363,8 +364,35 @@ def test_lambda_deployer_repeated_deploy(app_policy, sample_app):
'./myproject')

# And should result in the lambda function being updated with the API.
aws_client.update_function_code.assert_called_with(
lambda_function_name, 'package contents')
aws_client.update_function.assert_called_with(
lambda_function_name, 'package contents', {"FOO": "BAR"})


def test_lambda_deployer_initial_deploy(app_policy, sample_app):
osutils = InMemoryOSUtils({'packages.zip': b'package contents'})
aws_client = mock.Mock(spec=TypedAWSClient)
aws_client.create_function.return_value = 'lambda-arn'
packager = mock.Mock(LambdaDeploymentPackager)
packager.create_deployment_package.return_value = 'packages.zip'
cfg = Config.create(
chalice_stage='dev',
app_name='myapp',
chalice_app=sample_app,
manage_iam_role=False,
iam_role_arn='role-arn',
project_dir='.',
environment_variables={"FOO": "BAR"},
)

d = LambdaDeployer(aws_client, packager, None, osutils, app_policy)
deployed = d.deploy(cfg, None, 'dev')
assert deployed == {
'api_handler_arn': 'lambda-arn',
'api_handler_name': 'myapp-dev',
}
aws_client.create_function.assert_called_with(
'myapp-dev', 'role-arn', b'package contents',
{"FOO": "BAR"})


def test_cant_have_options_with_cors(sample_app):
Expand Down
Loading