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

fix: Shared Usage Plan scenarios for Resource Level Attribute Support #2040

Merged
merged 7 commits into from
May 20, 2021
Merged
Show file tree
Hide file tree
Changes from 6 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
103 changes: 96 additions & 7 deletions samtranslator/model/api/api_generator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import logging
from collections import namedtuple

from six import string_types
from samtranslator.model.intrinsics import ref, fnGetAtt
from samtranslator.model.intrinsics import ref, fnGetAtt, make_or_condition
from samtranslator.model.apigateway import (
ApiGatewayDeployment,
ApiGatewayRestApi,
Expand All @@ -15,7 +16,7 @@
ApiGatewayApiKey,
)
from samtranslator.model.route53 import Route53RecordSetGroup
from samtranslator.model.exceptions import InvalidResourceException
from samtranslator.model.exceptions import InvalidResourceException, InvalidTemplateException
from samtranslator.model.s3_utils.uri_parser import parse_s3_uri
from samtranslator.region_configuration import RegionConfiguration
from samtranslator.swagger.swagger import SwaggerEditor
Expand Down Expand Up @@ -61,12 +62,92 @@ class SharedApiUsagePlan(object):
so that these information can be used in the shared usage plan
"""

SHARED_USAGE_PLAN_CONDITION_NAME = "SharedUsagePlanCondition"

def __init__(self):
self.usage_plan_shared = False
self.stage_keys_shared = list()
self.api_stages_shared = list()
self.depends_on_shared = list()

# shared resource level attributes
self.conditions = set()
self.any_api_without_condition = False
self.deletion_policy = None
self.update_replace_policy = None

def get_combined_resource_attributes(self, resource_attributes, conditions):
"""
This method returns a dictionary which combines 'DeletionPolicy', 'UpdateReplacePolicy' and 'Condition'
values of API definitions that could be used in Shared Usage Plan resources.

Parameters
----------
resource_attributes: Dict[str]
A dictionary of resource level attributes of the API resource
conditions: Dict[str]
Conditions section of the template
"""
self._set_deletion_policy(resource_attributes.get("DeletionPolicy"))
self._set_update_replace_policy(resource_attributes.get("UpdateReplacePolicy"))
self._set_condition(resource_attributes.get("Condition"), conditions)

combined_resource_attributes = dict()
if self.deletion_policy:
combined_resource_attributes["DeletionPolicy"] = self.deletion_policy
if self.update_replace_policy:
combined_resource_attributes["UpdateReplacePolicy"] = self.update_replace_policy
# do not set Condition if any of the API resource does not have Condition in it
if self.conditions and not self.any_api_without_condition:
combined_resource_attributes["Condition"] = SharedApiUsagePlan.SHARED_USAGE_PLAN_CONDITION_NAME

return combined_resource_attributes

def _set_deletion_policy(self, deletion_policy):
if deletion_policy:
if self.deletion_policy:
# update only if new deletion policy is Retain
if deletion_policy == "Retain":
self.deletion_policy = deletion_policy
else:
self.deletion_policy = deletion_policy

def _set_update_replace_policy(self, update_replace_policy):
if update_replace_policy:
if self.update_replace_policy:
# if new value is Retain or
# new value is retain and current value is Delete then update its value
if (update_replace_policy == "Retain") or (
update_replace_policy == "Snapshot" and self.update_replace_policy == "Delete"
):
self.update_replace_policy = update_replace_policy
else:
self.update_replace_policy = update_replace_policy

def _set_condition(self, condition, template_conditions):
# if there are any API without condition, then skip
if self.any_api_without_condition:
return

if condition and condition not in self.conditions:

if template_conditions is None:
raise InvalidTemplateException(
"Can't have condition without having 'Conditions' section in the template"
)

if self.conditions:
self.conditions.add(condition)
or_condition = make_or_condition(self.conditions)
template_conditions[SharedApiUsagePlan.SHARED_USAGE_PLAN_CONDITION_NAME] = or_condition
else:
self.conditions.add(condition)
template_conditions[SharedApiUsagePlan.SHARED_USAGE_PLAN_CONDITION_NAME] = condition
elif condition is None:
self.any_api_without_condition = True
if template_conditions and SharedApiUsagePlan.SHARED_USAGE_PLAN_CONDITION_NAME in template_conditions:
del template_conditions[SharedApiUsagePlan.SHARED_USAGE_PLAN_CONDITION_NAME]


class ApiGenerator(object):
def __init__(
Expand All @@ -81,6 +162,7 @@ def __init__(
name,
stage_name,
shared_api_usage_plan,
template_conditions,
tags=None,
endpoint_configuration=None,
method_settings=None,
Expand Down Expand Up @@ -147,6 +229,7 @@ def __init__(
self.domain = domain
self.description = description
self.shared_api_usage_plan = shared_api_usage_plan
self.template_conditions = template_conditions

def _construct_rest_api(self):
"""Constructs and returns the ApiGateway RestApi.
Expand Down Expand Up @@ -654,7 +737,9 @@ def _construct_usage_plan(self, rest_api_stage=None):
usage_plan = ApiGatewayUsagePlan(
logical_id=usage_plan_logical_id,
depends_on=self.shared_api_usage_plan.depends_on_shared,
attributes=self.passthrough_resource_attributes,
attributes=self.shared_api_usage_plan.get_combined_resource_attributes(
self.passthrough_resource_attributes, self.template_conditions
),
)
api_stage = dict()
api_stage["ApiId"] = ref(self.logical_id)
Expand Down Expand Up @@ -692,7 +777,9 @@ def _construct_api_key(self, usage_plan_logical_id, create_usage_plan, rest_api_
api_key = ApiGatewayApiKey(
logical_id=api_key_logical_id,
depends_on=[usage_plan_logical_id],
attributes=self.passthrough_resource_attributes,
attributes=self.shared_api_usage_plan.get_combined_resource_attributes(
self.passthrough_resource_attributes, self.template_conditions
),
)
api_key.Enabled = True
stage_key = dict()
Expand All @@ -710,7 +797,6 @@ def _construct_api_key(self, usage_plan_logical_id, create_usage_plan, rest_api_
depends_on=[usage_plan_logical_id],
attributes=self.passthrough_resource_attributes,
)
# api_key = ApiGatewayApiKey(logical_id=api_key_logical_id, depends_on=[usage_plan_logical_id])
api_key.Enabled = True
stage_keys = list()
stage_key = dict()
Expand All @@ -730,17 +816,20 @@ def _construct_usage_plan_key(self, usage_plan_logical_id, create_usage_plan, ap
if create_usage_plan == "SHARED":
# create a mapping between api key and the usage plan
usage_plan_key_logical_id = "ServerlessUsagePlanKey"
resource_attributes = self.shared_api_usage_plan.get_combined_resource_attributes(
self.passthrough_resource_attributes, self.template_conditions
)
# for create_usage_plan = "PER_API"
else:
# create a mapping between api key and the usage plan
usage_plan_key_logical_id = self.logical_id + "UsagePlanKey"
resource_attributes = self.passthrough_resource_attributes

usage_plan_key = ApiGatewayUsagePlanKey(
logical_id=usage_plan_key_logical_id,
depends_on=[api_key.logical_id],
attributes=self.passthrough_resource_attributes,
attributes=resource_attributes,
)
# usage_plan_key = ApiGatewayUsagePlanKey(logical_id=usage_plan_key_logical_id, depends_on=[api_key.logical_id])
usage_plan_key.KeyId = ref(api_key.logical_id)
usage_plan_key.KeyType = "API_KEY"
usage_plan_key.UsagePlanId = ref(usage_plan_logical_id)
Expand Down
3 changes: 0 additions & 3 deletions samtranslator/model/eventsources/push.py
Original file line number Diff line number Diff line change
Expand Up @@ -960,9 +960,6 @@ def to_cloudformation(self, **kwargs):
resources.append(lambda_permission)

self._inject_lambda_config(function, userpool)
userpool_resource = CognitoUserPool.from_dict(userpool_id, userpool)
for attribute, value in function.get_passthrough_resource_attributes().items():
userpool_resource.set_resource_attribute(attribute, value)
resources.append(CognitoUserPool.from_dict(userpool_id, userpool))
return resources

Expand Down
2 changes: 2 additions & 0 deletions samtranslator/model/sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,7 @@ def to_cloudformation(self, **kwargs):
self.Auth = intrinsics_resolver.resolve_parameter_refs(self.Auth)
redeploy_restapi_parameters = kwargs.get("redeploy_restapi_parameters")
shared_api_usage_plan = kwargs.get("shared_api_usage_plan")
template_conditions = kwargs.get("conditions")

api_generator = ApiGenerator(
self.logical_id,
Expand All @@ -874,6 +875,7 @@ def to_cloudformation(self, **kwargs):
self.Name,
self.StageName,
shared_api_usage_plan,
template_conditions,
tags=self.Tags,
endpoint_configuration=self.EndpointConfiguration,
method_settings=self.MethodSettings,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
Globals:
Api:
Auth:
ApiKeyRequired: true
UsagePlan:
CreateUsagePlan: SHARED

Conditions:
C1:
Fn::Equals:
- test
- test
C2:
Fn::Equals:
- test
- test

Resources:
MyApiOne:
Type: AWS::Serverless::Api
Condition: C1
UpdateReplacePolicy: Delete
Properties:
StageName: Prod

MyApiTwo:
Type: AWS::Serverless::Api
Condition: C2
UpdateReplacePolicy: Snapshot
Properties:
StageName: Prod

MyApiThree:
Type: AWS::Serverless::Api
Properties:
StageName: Prod

MyFunctionOne:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs12.x
InlineCode: |
exports.handler = async (event) => {
return {
statusCode: 200,
body: JSON.stringify(event),
headers: {}
}
}
Events:
ApiKey:
Type: Api
Properties:
RestApiId:
Ref: MyApiOne
Method: get
Path: /path/one

MyFunctionTwo:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs12.x
InlineCode: |
exports.handler = async (event) => {
return {
statusCode: 200,
body: JSON.stringify(event),
headers: {}
}
}
Events:
ApiKey:
Type: Api
Properties:
RestApiId:
Ref: MyApiTwo
Method: get
Path: /path/two

MyFunctionThree:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs12.x
InlineCode: |
exports.handler = async (event) => {
return {
statusCode: 200,
body: JSON.stringify(event),
headers: {}
}
}
Events:
ApiKey:
Type: Api
Properties:
RestApiId:
Ref: MyApiThree
Method: get
Path: /path/three
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
Globals:
Api:
Auth:
ApiKeyRequired: true
UsagePlan:
CreateUsagePlan: SHARED

Conditions:
C1:
Fn::Equals:
- test
- test
C2:
Fn::Equals:
- test
- test

Resources:
MyApiOne:
Type: AWS::Serverless::Api
DeletionPolicy: Delete
Condition: C1
Properties:
StageName: Prod

MyApiTwo:
Type: AWS::Serverless::Api
DeletionPolicy: Retain
Condition: C2
Properties:
StageName: Prod

MyFunctionOne:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs12.x
InlineCode: |
exports.handler = async (event) => {
return {
statusCode: 200,
body: JSON.stringify(event),
headers: {}
}
}
Events:
ApiKey:
Type: Api
Properties:
RestApiId:
Ref: MyApiOne
Method: get
Path: /path/one

MyFunctionTwo:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
Runtime: nodejs12.x
InlineCode: |
exports.handler = async (event) => {
return {
statusCode: 200,
body: JSON.stringify(event),
headers: {}
}
}
Events:
ApiKey:
Type: Api
Properties:
RestApiId:
Ref: MyApiTwo
Method: get
Path: /path/two
Loading