Skip to content

Commit

Permalink
fix: Shared Usage Plan scenarios for Resource Level Attribute Support (
Browse files Browse the repository at this point in the history
…#2040)

* do not propagate attributes to CognitoUserPool

* shared usage plan with propagated resource level attributes

* add unit tests

* Added test templates for shared usage plan with resource attributes

* Black reformatting

* Removing unused import

* fix python2 hashes

Co-authored-by: Qingchuan Ma <[email protected]>
  • Loading branch information
mndeveci and qingchm authored May 20, 2021
1 parent ed3c283 commit be9b9d9
Show file tree
Hide file tree
Showing 13 changed files with 3,203 additions and 10 deletions.
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

0 comments on commit be9b9d9

Please sign in to comment.