Skip to content

Commit

Permalink
Merge pull request aws#1160 from kapilt/api-gw-endpoint-config
Browse files Browse the repository at this point in the history
api gateway endpoint configuration support
  • Loading branch information
kyleknap authored Jul 28, 2019
2 parents 32fabb3 + 314ab78 commit c3899ff
Show file tree
Hide file tree
Showing 18 changed files with 416 additions and 73 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ Next Release (TBD)
* Add experimental support for websockets
(`#1017 <https://github.com/aws/chalice/issues/1017>`__)

* API Gateway Endpoint Type Configuration
(`#1160 https://github.com/aws/chalice/pull/1160`__)

* API Gateway Resource Policy Configuration
(`#1160 https://github.com/aws/chalice/pull/1160`__)

1.9.1
=====
Expand Down
18 changes: 10 additions & 8 deletions chalice/awsclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,21 +450,23 @@ def get_rest_api_id(self, name):
return api['id']
return None

def rest_api_exists(self, rest_api_id):
# type: (str) -> bool
def get_rest_api(self, rest_api_id):
# type: (str) -> Dict[str, Any]
"""Check if an an API Gateway REST API exists."""
client = self._client('apigateway')
try:
client.get_rest_api(restApiId=rest_api_id)
return True
result = client.get_rest_api(restApiId=rest_api_id)
result.pop('ResponseMetadata', None)
return result
except client.exceptions.NotFoundException:
return False
return {}

def import_rest_api(self, swagger_document):
# type: (Dict[str, Any]) -> str
def import_rest_api(self, swagger_document, endpoint_type):
# type: (Dict[str, Any], str) -> str
client = self._client('apigateway')
response = client.import_rest_api(
body=json.dumps(swagger_document, indent=2)
body=json.dumps(swagger_document, indent=2),
parameters={'endpointConfigurationTypes': endpoint_type}
)
rest_api_id = response['id']
return rest_api_id
Expand Down
2 changes: 2 additions & 0 deletions chalice/cli/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from chalice.package import AppPackager # noqa
from chalice.constants import DEFAULT_STAGE_NAME
from chalice.constants import DEFAULT_APIGATEWAY_STAGE_NAME
from chalice.constants import DEFAULT_ENDPOINT_TYPE
from chalice.logs import LogRetriever
from chalice import local
from chalice.utils import UI # noqa
Expand Down Expand Up @@ -142,6 +143,7 @@ def create_config_obj(self, chalice_stage_name=DEFAULT_STAGE_NAME,
user_provided_params = {} # type: Dict[str, Any]
default_params = {'project_dir': self.project_dir,
'api_gateway_stage': DEFAULT_APIGATEWAY_STAGE_NAME,
'api_gateway_endpoint_type': DEFAULT_ENDPOINT_TYPE,
'autogen_policy': True}
try:
config_from_disk = self.load_project_config()
Expand Down
20 changes: 19 additions & 1 deletion chalice/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import sys
import json

from typing import Dict, Any, Optional, List # noqa
from typing import Dict, Any, Optional, List, Union # noqa
from chalice import __version__ as current_chalice_version
from chalice.app import Chalice # noqa
from chalice.constants import DEFAULT_STAGE_NAME
Expand Down Expand Up @@ -223,6 +223,24 @@ def api_gateway_stage(self):
return self._chain_lookup('api_gateway_stage',
varies_per_chalice_stage=True)

@property
def api_gateway_endpoint_type(self):
# type: () -> str
return self._chain_lookup('api_gateway_endpoint_type',
varies_per_chalice_stage=True)

@property
def api_gateway_endpoint_vpce(self):
# type: () -> Union[str, List[str]]
return self._chain_lookup('api_gateway_endpoint_vpce',
varies_per_chalice_stage=True)

@property
def api_gateway_policy_file(self):
# type: () -> str
return self._chain_lookup('api_gateway_policy_file',
varies_per_chalice_stage=True)

@property
def minimum_compression_size(self):
# type: () -> int
Expand Down
2 changes: 1 addition & 1 deletion chalice/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def index():

DEFAULT_STAGE_NAME = 'dev'
DEFAULT_APIGATEWAY_STAGE_NAME = 'api'

DEFAULT_ENDPOINT_TYPE = 'EDGE'

DEFAULT_LAMBDA_TIMEOUT = 60
DEFAULT_LAMBDA_MEMORY_SIZE = 128
Expand Down
33 changes: 31 additions & 2 deletions chalice/deploy/deployer.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"""

# pylint: disable=too-many-lines
import json
import os
import textwrap
Expand Down Expand Up @@ -459,15 +459,44 @@ def _create_rest_api_model(self,
handler_name=auth.handler_string, stage_name=stage_name,
)
authorizers.append(auth_lambda)

policy = None
policy_path = config.api_gateway_policy_file
if (config.api_gateway_endpoint_type == 'PRIVATE' and not policy_path):
policy = models.IAMPolicy(
document=self._get_default_private_api_policy(config))
elif policy_path:
policy = models.FileBasedIAMPolicy(
document=models.Placeholder.BUILD_STAGE,
filename=os.path.join(
config.project_dir, '.chalice', policy_path))

return models.RestAPI(
resource_name='rest_api',
swagger_doc=models.Placeholder.BUILD_STAGE,
endpoint_type=config.api_gateway_endpoint_type,
minimum_compression=minimum_compression,
api_gateway_stage=config.api_gateway_stage,
lambda_function=lambda_function,
authorizers=authorizers,
policy=policy
)

def _get_default_private_api_policy(self, config):
# type: (Config) -> Dict[str, Any]
statements = [{
"Effect": "Allow",
"Principal": "*",
"Action": "execute-api:Invoke",
"Resource": "arn:aws:execute-api:*:*:*",
"Condition": {
"StringEquals": {
"aws:SourceVpce": config.api_gateway_endpoint_vpce
}
}
}]
return {"Version": "2012-10-17", "Statement": statements}

def _create_websocket_api_model(
self,
config, # type: Config
Expand Down Expand Up @@ -827,7 +856,7 @@ def __init__(self, swagger_generator):
def handle_restapi(self, config, resource):
# type: (Config, models.RestAPI) -> None
swagger_doc = self._swagger_generator.generate_swagger(
config.chalice_app)
config.chalice_app, resource)
resource.swagger_doc = swagger_doc


Expand Down
3 changes: 3 additions & 0 deletions chalice/deploy/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pylint: disable=line-too-long
import enum
from typing import List, Dict, Optional, Any, TypeVar, Union, Set # noqa
from typing import cast
Expand Down Expand Up @@ -189,7 +190,9 @@ class RestAPI(ManagedModel):
swagger_doc = attrib() # type: DV[Dict[str, Any]]
minimum_compression = attrib() # type: str
api_gateway_stage = attrib() # type: str
endpoint_type = attrib() # type: str
lambda_function = attrib() # type: LambdaFunction
policy = attrib(default=None) # type: Optional[IAMPolicy]
authorizers = attrib(default=Factory(list)) # type: List[LambdaFunction]

def dependencies(self):
Expand Down
63 changes: 36 additions & 27 deletions chalice/deploy/planner.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# pylint: disable=too-many-lines
from collections import OrderedDict

from typing import List, Dict, Any, Optional, Union, Tuple, Set, cast # noqa
Expand Down Expand Up @@ -107,7 +108,7 @@ def _resource_exists_restapi(self, resource):
except ValueError:
return False
rest_api_id = deployed_values['rest_api_id']
return self._client.rest_api_exists(rest_api_id)
return bool(self._client.get_rest_api(rest_api_id))

def _resource_exists_websocketapi(self, resource):
# type: (models.WebsocketAPI) -> bool
Expand Down Expand Up @@ -843,17 +844,19 @@ def _plan_restapi(self, resource):
# There's also a set of instructions that are needed
# at the end of deploying a rest API that apply to both
# the update and create case.
shared_plan_patch_ops = [{
'op': 'replace',
'path': '/minimumCompressionSize',
'value': resource.minimum_compression}
] # type: List[Dict]

shared_plan_epilogue = [
models.APICall(
method_name='update_rest_api',
params={
'rest_api_id': Variable('rest_api_id'),
'patch_operations': [{
'op': 'replace',
'path': '/minimumCompressionSize',
'value': resource.minimum_compression,
}],
},
'patch_operations': shared_plan_patch_ops
}
),
models.APICall(
method_name='add_permission_for_apigateway',
Expand All @@ -862,6 +865,11 @@ def _plan_restapi(self, resource):
'account_id': Variable('account_id'),
'rest_api_id': Variable('rest_api_id')},
),
models.APICall(
method_name='deploy_rest_api',
params={'rest_api_id': Variable('rest_api_id'),
'api_gateway_stage': resource.api_gateway_stage},
),
models.StoreValue(
name='rest_api_url',
value=StringFormat(
Expand Down Expand Up @@ -891,7 +899,8 @@ def _plan_restapi(self, resource):
plan = shared_plan_preamble + [
(models.APICall(
method_name='import_rest_api',
params={'swagger_document': resource.swagger_doc},
params={'swagger_document': resource.swagger_doc,
'endpoint_type': resource.endpoint_type},
output_var='rest_api_id',
), "Creating Rest API\n"),
models.RecordResourceVariable(
Expand All @@ -900,14 +909,24 @@ def _plan_restapi(self, resource):
name='rest_api_id',
variable_name='rest_api_id',
),
models.APICall(
method_name='deploy_rest_api',
params={'rest_api_id': Variable('rest_api_id'),
'api_gateway_stage': resource.api_gateway_stage},
),
] + shared_plan_epilogue
]
else:
deployed = self._remote_state.resource_deployed_values(resource)
shared_plan_epilogue.insert(
0,
models.APICall(
method_name='get_rest_api',
params={'rest_api_id': Variable('rest_api_id')},
output_var='rest_api')
)
shared_plan_patch_ops.append({
'op': 'replace',
'path': StringFormat(
'/endpointConfiguration/types/%s' % (
'{rest_api[endpointConfiguration][types][0]}'),
['rest_api']),
'value': resource.endpoint_type}
)
plan = shared_plan_preamble + [
models.StoreValue(
name='rest_api_id',
Expand All @@ -925,19 +944,9 @@ def _plan_restapi(self, resource):
'swagger_document': resource.swagger_doc,
},
), "Updating rest API\n"),
models.APICall(
method_name='deploy_rest_api',
params={'rest_api_id': Variable('rest_api_id'),
'api_gateway_stage': resource.api_gateway_stage},
),
models.APICall(
method_name='add_permission_for_apigateway',
params={'function_name': function_name,
'region_name': Variable('region_name'),
'account_id': Variable('account_id'),
'rest_api_id': Variable('rest_api_id')},
),
] + shared_plan_epilogue
]

plan.extend(shared_plan_epilogue)
return plan

def _get_role_arn(self, resource):
Expand Down
13 changes: 10 additions & 3 deletions chalice/deploy/swagger.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import copy
import inspect

from typing import Any, List, Dict, Optional # noqa
from typing import Any, List, Dict, Optional, Union # noqa

from chalice.app import Chalice, RouteEntry, Authorizer, CORSConfig # noqa
from chalice.app import ChaliceAuthorizer
from chalice.deploy.planner import StringFormat
from chalice.deploy.models import RestAPI # noqa
from chalice.utils import to_cfn_resource_name


Expand All @@ -32,14 +33,20 @@ def __init__(self, region, deployed_resources):
self._region = region
self._deployed_resources = deployed_resources

def generate_swagger(self, app):
# type: (Chalice) -> Dict[str, Any]
def generate_swagger(self, app, rest_api=None):
# type: (Chalice, Optional[RestAPI]) -> Dict[str, Any]
api = copy.deepcopy(self._BASE_TEMPLATE)
api['info']['title'] = app.app_name
self._add_binary_types(api, app)
self._add_route_paths(api, app)
self._add_resource_policy(api, rest_api)
return api

def _add_resource_policy(self, api, rest_api):
# type: (Dict[str, Any], Optional[RestAPI]) -> None
if rest_api and rest_api.policy:
api['x-amazon-apigateway-policy'] = rest_api.policy.document

def _add_binary_types(self, api, app):
# type: (Dict[str, Any], Chalice) -> None
api['x-amazon-apigateway-binary-media-types'] = app.api.binary_types
Expand Down
34 changes: 34 additions & 0 deletions chalice/deploy/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,40 @@ def validate_configuration(config):
validate_python_version(config)
validate_unique_function_names(config)
validate_feature_flags(config.chalice_app)
validate_endpoint_type(config)
validate_resource_policy(config)


def validate_resource_policy(config):
# type: (Config) -> None
if (config.api_gateway_endpoint_type != 'PRIVATE' and
config.api_gateway_endpoint_vpce):
raise ValueError(
"config.api_gateway_endpoint_vpce should only be "
"specified for PRIVATE api_gateway_endpoint_type")
if config.api_gateway_endpoint_type != 'PRIVATE':
return
if config.api_gateway_policy_file and config.api_gateway_endpoint_vpce:
raise ValueError(
"Can only specify one of api_gateway_policy_file and "
"api_gateway_endpoint_vpce")
if config.api_gateway_policy_file:
return
if not config.api_gateway_endpoint_vpce:
raise ValueError(
("Private Endpoints require api_gateway_policy_file or "
"api_gateway_endpoint_vpce specified"))


def validate_endpoint_type(config):
# type: (Config) -> None
if not config.api_gateway_endpoint_type:
return
valid_types = ('EDGE', 'REGIONAL', 'PRIVATE')
if config.api_gateway_endpoint_type not in valid_types:
raise ValueError(
"api gateway endpoint type must be one of %s" % (
", ".join(valid_types)))


def validate_feature_flags(chalice_app):
Expand Down
1 change: 1 addition & 0 deletions chalice/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ def _generate_restapi(self, resource, template):
resources['RestAPI'] = {
'Type': 'AWS::Serverless::Api',
'Properties': {
'EndpointConfiguration': resource.endpoint_type,
'StageName': resource.api_gateway_stage,
'DefinitionBody': resource.swagger_doc,
}
Expand Down
Loading

0 comments on commit c3899ff

Please sign in to comment.