Skip to content

Commit 4ef6093

Browse files
c2tarunmgrandisChris Rehnhawflaujfuss
authored
Release/v1.38.0 (#2082) (#2095)
* test: New test group to test for side effects (#2046) * test: New test group to test for side effects * refactor: Updated to use _compare_transform and test CN and GOV partitions * docs: fix dead link (#2045) * Percentage-based Enablement for Feature Toggle (#1952) * Percentage-based Enablement for Feature Toggle * Update Feature Toggle to accept stage, account_id and region during instanciation * remove unnecessary uses of dict.get method * Refactor feature toggle methods * Update test names * black reformat * Update FeatureToggle to require stage, region and account_id to instanciate * Update log message * Implement calculating account percentile based on hash of account_id and feature_name * Refactor _is_feature_enabled_for_region_config * Refactor dialup logic into its own classes * Add comments for dialup classes * Rename NeverEnabledDialup to DisabledDialup * chore(tests): Adding any tests (#2053) * Adding api_request_model any tests * Add any to api_request_model_openapi_3 cases * Add rest of relevant any test cases * Fix hashing to match python2 * add api_with_swagger_authorizer_none to be run * fix py2 hashes in api_with_swagger_authorizer_none tests Co-authored-by: Jacob Fuss <[email protected]> * Add modes support for RestApi (#2055) * Adding Mode passthrough property to RestApi with unit tests. * Adding integration test for Mode * Fixing sam-translate for manual translation. * running black formatting * Running black formatting, again. * Clearing pip-wheel-metadata. * Clearing tmp folder created by integ test. Co-authored-by: Tarun Mall <[email protected]> * chore: bump version to 1.38.0 (#2081) Co-authored-by: Mathieu Grandis <[email protected]> Co-authored-by: Chris Rehn <[email protected]> Co-authored-by: Wing Fung Lau <[email protected]> Co-authored-by: Jacob Fuss <[email protected]> Co-authored-by: Jacob Fuss <[email protected]> Co-authored-by: Tarun Mall <[email protected]> Co-authored-by: Raymond Wang <[email protected]> Co-authored-by: Mathieu Grandis <[email protected]> Co-authored-by: Chris Rehn <[email protected]> Co-authored-by: Wing Fung Lau <[email protected]> Co-authored-by: Jacob Fuss <[email protected]> Co-authored-by: Jacob Fuss <[email protected]> Co-authored-by: Tarun Mall <[email protected]> Co-authored-by: Raymond Wang <[email protected]>
1 parent 25fa8a8 commit 4ef6093

File tree

147 files changed

+22958
-13156
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

147 files changed

+22958
-13156
lines changed

bin/sam-translate.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,10 @@ def transform_template(input_file_path, output_file_path):
9999
feature_toggle = FeatureToggle(
100100
FeatureToggleLocalConfigProvider(
101101
os.path.join(my_path, "..", "tests", "feature_toggle", "input", "feature_toggle_config.json")
102-
)
102+
),
103+
stage=None,
104+
account_id=None,
105+
region=None,
103106
)
104107
cloud_formation_template = transform(sam_template, {}, ManagedPolicyLoader(iam_client), feature_toggle)
105108
cloud_formation_template_prettified = json.dumps(cloud_formation_template, indent=2)

docs/safe_lambda_deployments.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ NOTE: Verify that your AWS SDK version supports PutLifecycleEventHookExecutionSt
190190

191191
.. _PutLifecycleEventHookExecutionStatus: https://docs.aws.amazon.com/codedeploy/latest/APIReference/API_PutLifecycleEventHookExecutionStatus.html
192192

193-
.. _Here: https://github.com/awslabs/serverless-application-model/blob/master/examples/2016-10-31/lambda_safe_deployments/src/preTrafficHook.js
193+
.. _Here: https://github.com/aws/serverless-application-model/blob/d168f371f494196a57032313075db9faae5587e4/examples/2016-10-31/lambda_safe_deployments/src/preTrafficHook.js
194194

195195
Traffic Shifting Configurations
196196
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

integration/helpers/base_test.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import logging
23
import os
34

@@ -146,6 +147,28 @@ def create_and_verify_stack(self, file_name, parameters=None):
146147
self.deploy_stack(parameters)
147148
self.verify_stack()
148149

150+
def update_and_verify_stack(self, file_name, parameters=None):
151+
"""
152+
Updates the Cloud Formation stack and verifies it against the expected
153+
result
154+
155+
Parameters
156+
----------
157+
file_name : string
158+
Template file name
159+
parameters : list
160+
List of parameters
161+
"""
162+
if not self.stack_name:
163+
raise Exception("Stack not created.")
164+
self.output_file_path = str(Path(self.output_dir, "cfn_" + file_name + ".yaml"))
165+
self.expected_resource_path = str(Path(self.expected_dir, file_name + ".json"))
166+
167+
self._fill_template(file_name)
168+
self.transform_template()
169+
self.deploy_stack(parameters)
170+
self.verify_stack(end_state="UPDATE_COMPLETE")
171+
149172
def transform_template(self):
150173
transform_template(self.sub_input_file_path, self.output_file_path)
151174

@@ -342,12 +365,12 @@ def deploy_stack(self, parameters=None):
342365
self.stack_description = self.client_provider.cfn_client.describe_stacks(StackName=self.stack_name)
343366
self.stack_resources = self.client_provider.cfn_client.list_stack_resources(StackName=self.stack_name)
344367

345-
def verify_stack(self):
368+
def verify_stack(self, end_state="CREATE_COMPLETE"):
346369
"""
347370
Gets and compares the Cloud Formation stack against the expect result file
348371
"""
349372
# verify if the stack was successfully created
350-
self.assertEqual(self.stack_description["Stacks"][0]["StackStatus"], "CREATE_COMPLETE")
373+
self.assertEqual(self.stack_description["Stacks"][0]["StackStatus"], end_state)
351374
# verify if the stack contains the expected resources
352375
error = verify_stack_resources(self.expected_resource_path, self.stack_resources)
353376
if error:
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[
2+
{"LogicalResourceId": "MyApi", "ResourceType": "AWS::ApiGateway::RestApi"},
3+
{"LogicalResourceId": "MyApiDeploymenta808f15210", "ResourceType": "AWS::ApiGateway::Deployment"},
4+
{"LogicalResourceId": "MyApiMyNewStageNameStage", "ResourceType": "AWS::ApiGateway::Stage"},
5+
{"LogicalResourceId": "TestFunction", "ResourceType": "AWS::Lambda::Function"},
6+
{"LogicalResourceId": "TestFunctionAliaslive", "ResourceType": "AWS::Lambda::Alias"},
7+
{"LogicalResourceId": "TestFunctionGetPermissionMyNewStageName", "ResourceType": "AWS::Lambda::Permission"},
8+
{"LogicalResourceId": "TestFunctionPutPermissionMyNewStageName", "ResourceType": "AWS::Lambda::Permission"},
9+
{"LogicalResourceId": "TestFunctionRole", "ResourceType": "AWS::IAM::Role"},
10+
{"LogicalResourceId": "TestFunctionVersione9898fd501", "ResourceType": "AWS::Lambda::Version"}
11+
]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
{"LogicalResourceId": "MyApi", "ResourceType": "AWS::ApiGateway::RestApi"},
3+
{"LogicalResourceId": "MyApiDeploymentada889e3ac", "ResourceType": "AWS::ApiGateway::Deployment"},
4+
{"LogicalResourceId": "MyApiMyNewStageNameStage", "ResourceType": "AWS::ApiGateway::Stage"},
5+
{"LogicalResourceId": "TestFunction", "ResourceType": "AWS::Lambda::Function"},
6+
{"LogicalResourceId": "TestFunctionAliaslive", "ResourceType": "AWS::Lambda::Alias"},
7+
{"LogicalResourceId": "TestFunctionPutPermissionMyNewStageName", "ResourceType": "AWS::Lambda::Permission"},
8+
{"LogicalResourceId": "TestFunctionRole", "ResourceType": "AWS::IAM::Role"},
9+
{"LogicalResourceId": "TestFunctionVersion847aaa5fc1", "ResourceType": "AWS::Lambda::Version"}
10+
]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
Resources:
2+
MyApi:
3+
Type: AWS::Serverless::Api
4+
Properties:
5+
StageName: MyNewStageName
6+
Mode: overwrite
7+
8+
TestFunction:
9+
Type: 'AWS::Serverless::Function'
10+
Properties:
11+
Handler: index.handler
12+
Runtime: python3.6
13+
AutoPublishAlias: live
14+
InlineCode: |
15+
import json
16+
def handler(event, context):
17+
return {'statusCode': 200, 'body': json.dumps('Hello World!')}
18+
Events:
19+
Get:
20+
Type: Api
21+
Properties:
22+
Path: /get
23+
Method: get
24+
RestApiId: !Ref MyApi
25+
Put:
26+
Type: Api
27+
Properties:
28+
Path: /put
29+
Method: put
30+
RestApiId: !Ref MyApi
31+
32+
Outputs:
33+
ApiEndpoint:
34+
Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/MyNewStageName"
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Resources:
2+
MyApi:
3+
Type: AWS::Serverless::Api
4+
Properties:
5+
StageName: MyNewStageName
6+
Mode: overwrite
7+
8+
TestFunction:
9+
Type: 'AWS::Serverless::Function'
10+
Properties:
11+
Handler: index.handler
12+
Runtime: python3.6
13+
AutoPublishAlias: live
14+
InlineCode: |
15+
def handler(event, context):
16+
print("Hello, world!")
17+
Events:
18+
Put:
19+
Type: Api
20+
Properties:
21+
Path: /put
22+
Method: put
23+
RestApiId: !Ref MyApi
24+

integration/single/test_basic_api.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from integration.helpers.base_test import BaseTest
2+
import requests
23

34

45
class TestBasicApi(BaseTest):
@@ -24,6 +25,24 @@ def test_basic_api(self):
2425

2526
self.assertEqual(len(set(first_dep_ids).intersection(second_dep_ids)), 0)
2627

28+
def test_basic_api_with_mode(self):
29+
"""
30+
Creates an API and updates its DefinitionUri
31+
"""
32+
# Create an API with get and put
33+
self.create_and_verify_stack("basic_api_with_mode")
34+
35+
stack_output = self.get_stack_outputs()
36+
api_endpoint = stack_output.get("ApiEndpoint")
37+
response = requests.get(f"{api_endpoint}/get")
38+
self.assertEqual(response.status_code, 200)
39+
40+
# Removes get from the API
41+
self.update_and_verify_stack("basic_api_with_mode_update")
42+
response = requests.get(f"{api_endpoint}/get")
43+
# API Gateway by default returns 403 if a path do not exist
44+
self.assertEqual(response.status_code, 403)
45+
2746
def test_basic_api_inline_openapi(self):
2847
"""
2948
Creates an API with and inline OpenAPI and updates its DefinitionBody basePath

samtranslator/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.37.0"
1+
__version__ = "1.38.0"
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import hashlib
2+
3+
4+
class BaseDialup(object):
5+
"""BaseDialup class to provide an interface for all dialup classes"""
6+
7+
def __init__(self, region_config, **kwargs):
8+
self.region_config = region_config
9+
10+
def is_enabled(self):
11+
"""
12+
Returns a bool on whether this dialup is enabled or not
13+
"""
14+
raise NotImplementedError
15+
16+
def __str__(self):
17+
return self.__class__.__name__
18+
19+
20+
class DisabledDialup(BaseDialup):
21+
"""
22+
A dialup that is never enabled
23+
"""
24+
25+
def __init__(self, region_config, **kwargs):
26+
super(DisabledDialup, self).__init__(region_config)
27+
28+
def is_enabled(self):
29+
return False
30+
31+
32+
class ToggleDialup(BaseDialup):
33+
"""
34+
A simple toggle Dialup
35+
Example of region_config: { "type": "toggle", "enabled": True }
36+
"""
37+
38+
def __init__(self, region_config, **kwargs):
39+
super(ToggleDialup, self).__init__(region_config)
40+
self.region_config = region_config
41+
42+
def is_enabled(self):
43+
return self.region_config.get("enabled", False)
44+
45+
46+
class SimpleAccountPercentileDialup(BaseDialup):
47+
"""
48+
Simple account percentile dialup, enabling X% of
49+
Example of region_config: { "type": "account-percentile", "enabled-%": 20 }
50+
"""
51+
52+
def __init__(self, region_config, account_id, feature_name, **kwargs):
53+
super(SimpleAccountPercentileDialup, self).__init__(region_config)
54+
self.account_id = account_id
55+
self.feature_name = feature_name
56+
57+
def _get_account_percentile(self):
58+
"""
59+
Get account percentile based on sha256 hash of account ID and feature_name
60+
61+
:returns: integer n, where 0 <= n < 100
62+
"""
63+
m = hashlib.sha256()
64+
m.update(self.account_id.encode())
65+
m.update(self.feature_name.encode())
66+
return int(m.hexdigest(), 16) % 100
67+
68+
def is_enabled(self):
69+
"""
70+
Enable when account_percentile falls within target_percentile
71+
Meaning only (target_percentile)% of accounts will be enabled
72+
"""
73+
target_percentile = self.region_config.get("enabled-%", 0)
74+
return self._get_account_percentile() < target_percentile

0 commit comments

Comments
 (0)