def test_getatt_on_similar_resources_is_not_equal(self): # Setup func1 = GetAtt(SingleAttributeObject(one=42), "Attrib") func2 = GetAtt(SingleAttributeObject(one=42), "Attrib") # Verify assert func1 != func2 assert not (func1 == func2) assert hash(func1) != hash(func2)
def test_getatt_on_same_resource_is_equal(self): # Setup data = SingleAttributeObject(one=42) func1 = GetAtt(data, "Attrib") func2 = GetAtt(data, "Attrib") # Verify assert func1 == func2 assert not (func1 != func2) assert hash(func1) == hash(func2)
def test_getatt_on_different_attributes_is_not_equal(self): # Setup data = SingleAttributeObject(one=42) func1 = GetAtt(data, "Attrib1", "Attrib2") func2 = GetAtt(data, "Attrib2", "Attrib1") # Verify assert func1 != func2 assert not (func1 == func2) assert hash(func1) != hash(func2)
def create_lambda_invalidation_stack(function: str, dependencies: List[SSMParameter], role) -> Stack: """Create CloudFormation resources to invalidate a single AWS Lambda Function. This is accomplished by adding a meaningless environment variable to the Function, which will force it to re-deploy into a new execution context (but without altering any behaviour). Parameters: dependencies: SSM Parameters that this Function uses function: CloudFormation reference to the Lambda Function (eg. an unversioned ARN, or the name) role: CloudFormation reference (eg. an ARN) to an IAM role that will be used to modify the Function. """ # TODO make role optional, and create it on-the-fly if not provided # TODO find a way to share role and lambda between multiple calls in the same stack? can de-dupe/cache based on identity in the final stack # TODO get Lambda handler to have an internal timeout as well? stack = Stack( Description="Invalidate Lambda Function after parameter update") # Create an inline Lambda that can restart an ECS service, since this # isn't built-in CloudFormation functionality. stack.Resources[ "ReplacementLambda"] = replace_lambda_context_lambda = Function.create_from_python_function( handler=replace_lambda_context_resource_handler, Role=role) # Set the Lambda Replacer's timeout to a fixed value. This should be # universal, so we don't let callers specify it. replace_lambda_context_lambda.Properties.Timeout = 20 # Create a custom resource to replace the Lambda's execution context. # # We don't want this to happen until the parameters have # all been created, so we need to have the Restarter resource depend on # the parameters (either implicitly or via DependsOn). We also want the # restart to only happen if the parameters have actually changed - this # can be done if we make the SSM Parameters be part of the resource # specification (both the key and the value). # TODO pull out common code here stack.Resources["Replacer"] = dict( Type="Custom::ReplaceLambdaContext", Properties=dict( ServiceToken=GetAtt(replace_lambda_context_lambda, "Arn"), FunctionName=function, IgnoredParameterNames=[Ref(p) for p in dependencies], IgnoredParameterKeys=[GetAtt(p, "Value") for p in dependencies], ), ) # TODO consider creating a waiter anyway, so that the timeout is strictly reliable return stack
def test_getatt_with_same_ref_as_attribute_is_equal(self): # Setup data = SingleAttributeObject(one=42) referent = ZeroAttributeObject() func1 = GetAtt(data, Ref(referent)) func2 = GetAtt(data, Ref(referent)) # Verify assert func1 == func2 assert not (func1 != func2) assert hash(func1) == hash(func2)
def test_referred_object_can_be_a_plain_dict(self): # Setup data = {"one": 42} stack = Stack(Resources={"SomeResource": data}) func = GetAtt(data, "Attrib") dumper = create_refsafe_dumper(stack) # Exercise node = func.as_yaml_node(dumper) # Verify assert node.value == "SomeResource.Attrib"
def test_non_resource_stack_objects_cannot_be_referenced( self, object_type): # Setup data = {"one": 42} stack = Stack() setattr(stack, object_type, {"SomeResource": data}) func = GetAtt(data, "Attrib") dumper = create_refsafe_dumper(stack) # Exercise with pytest.raises(Exception): func.as_yaml_node(dumper)
def test_hash_is_different_from_hash_of_resource(self): # Setup data = SingleAttributeObject(one=42) func = GetAtt(data, "Attrib") # Verify assert hash(data) != hash(func)
def test_referred_object_that_is_a_string_is_rejected_immediately(self): # Setup name = "SomeResource" data = SingleAttributeObject(one=42) stack = Stack(Resources={name: data}) # Exercise with pytest.raises(TypeError) as excinfo: _ = GetAtt(name, "AttribName") assert "directly create a GetAtt on a logical name" in str( excinfo.value)
def test_getatt_is_not_equal_to_arbitrary_object(self, constructor): # Setup data = SingleAttributeObject(one=42) func = GetAtt(data, "Attrib") other = constructor(data, "Attrib") # Verify assert func != other assert other != func assert not (func == other) assert not (other == func) assert hash(func) != hash(other)
def _create_getatt_function(resource_name, *attrib_name): """Create a GetAtt function object that refers to a valid resource. Returns: A tuple of (dumper, function_object) """ data = SingleAttributeObject(one=42) stack = Stack(Resources={resource_name: data}) func = GetAtt(data, *attrib_name) dumper = create_refsafe_dumper(stack) return dumper, func
def test_creates_stack_with_supplied_parameters(self): # Setup ssm_parameter = SSMParameter( Properties=SSMParameterProperties(Name="test-parameter-name", Type="String", Value="test-param-value")) cluster_name = "cluster-name" service = "service-name" dependencies = [ssm_parameter] role = "role-arn" # Exercise stack = create_ecs_service_invalidation_stack( cluster=cluster_name, service=service, dependencies=dependencies, restart_role=role, timeout=30, ) # Verify Lambda Function functions = [ r for r in stack.Resources.values() if isinstance(r, Function) ] assert len( functions) == 1, "There should be a Lambda Function resource" func = functions[0] assert re.search(r"restart.*ecs.*service", func.Properties.Handler, re.I), "Lambda should restart an ECS service" assert func.Properties.Role == role # Verify Custom Resource custom_resources = [ r for r in stack.Resources.values() if r["Type"].startswith("Custom::") ] assert ( len(custom_resources) == 1 ), "There should be a custom resource to restart the ECS service" restarter = custom_resources[0] assert restarter["Properties"]["ClusterArn"] == cluster_name assert restarter["Properties"]["ServiceArn"] == service # Verify dependencies for service restart dependent_values = _get_flattened_attributes(restarter) assert Ref(ssm_parameter) in dependent_values assert GetAtt(ssm_parameter, "Value") in dependent_values
def test_creates_stack_for_single_lambda(self): # TODO pull out some common helpers # Setup ssm_parameter = SSMParameter( Properties=SSMParameterProperties(Name="test-parameter-name", Type="String", Value="test-param-value")) function_name = "some-function-name" dependencies = [ssm_parameter] role = "role-arn" # Exercise stack = create_lambda_invalidation_stack(function=function_name, dependencies=dependencies, role=role) # Verify Lambda Function functions = [ r for r in stack.Resources.values() if isinstance(r, Function) ] assert ( len(functions) == 1 ), "There should be a Lambda Function resource to perform the invalidation" func = functions[0] assert re.search(r"replace.*lambda.*context", func.Properties.Handler, re.I), "Lambda should update an existing Lambda" assert func.Properties.Role == role # Verify Custom Resource custom_resources = [ r for r in stack.Resources.values() if r["Type"].startswith("Custom::") ] assert ( len(custom_resources) == 1 ), "There should be a custom resource to update the target Lambda" updater = custom_resources[0] assert updater["Properties"]["FunctionName"] == function_name # Verify dependencies for Lambda update dependent_values = _get_flattened_attributes(updater) assert Ref(ssm_parameter) in dependent_values assert GetAtt(ssm_parameter, "Value") in dependent_values
def generate_stack_template(): stack = Stack() stack.Resources["WebServer"] = create_ec2_instance("webserver") stack.Resources["DatabaseServer"] = dbserver = create_ec2_instance( "dbserver", "t2.medium") dbserver.DeletionPolicy = "Retain" stack.Outputs["DatabaseServerIp"] = Output( Description=f"Internal IP address for the database server", Value=GetAtt(dbserver, "PrivateIp"), ) stack.tag(application="api-service", environment="test", owner=os.environ.get("USER")) return stack.export("yaml")
def test_stack_yaml_output(self): """An integration test, yay!""" # Setup data = SingleAttributeObject(one=42) stack = Stack( Resources=dict(Foo=data, Bar=GetAtt(data, "ResourceAttrib1"))) del stack.Metadata # Exercise output = stack.export("yaml") # Verify assert output == dedent(""" --- AWSTemplateFormatVersion: '2010-09-09' Resources: Bar: !GetAtt Foo.ResourceAttrib1 Foo: one: 42 """)
def test_yaml_output_with_nested_function(self): # Setup data = SingleAttributeObject(one=42) stack = Stack(Resources=dict( Foo=data, Bar=GetAtt(data, "ResourceAttrib1", Ref(AWS_Region)))) del stack.Metadata # Exercise output = stack.export("yaml") # Verify assert output == dedent(""" --- AWSTemplateFormatVersion: '2010-09-09' Resources: Bar: Fn::GetAtt: - Foo - ResourceAttrib1 - !Ref AWS::Region Foo: one: 42 """)
def test_attribute_name_list_cannot_be_empty(self): # Exercise with pytest.raises(ValueError) as excinfo: _ = GetAtt({"one": 42}) assert "AWS attribute name is required" in str(excinfo.value)
def create_ecs_service_invalidation_stack( cluster, service, dependencies: List[SSMParameter], restart_role, timeout: int = 8 * 60, ) -> Stack: """Create CloudFormation resources to invalidate a single ECS service. This is accomplished by restarting the ECS service, which will force it to use the new parameters. Parameters: cluster: CloudFormation reference (eg. an ARN) to the cluster the service is in dependencies: SSM Parameters that this service uses service: CloudFormation reference (eg. an ARN) to the ECS service to invalidate restart_role: CloudFormation reference (eg. an ARN) to an IAM role that will be used to restart the ECS service. timeout: Number of seconds to wait for the ECS service to detect the new parameters successfully after restart. If we exceed this timeout it is presumed that the updated parameters are broken, and the changes will be rolled back. The default is 5 minutes, which should be enough for most services. """ # TODO make restart_role optional, and create it on-the-fly if not provided # TODO find a way to share role and lambda between multiple calls in the same stack? can de-dupe/cache based on identity in the final stack # TODO get Lambda handler to have an internal timeout as well? stack = Stack(Description="Invalidate ECS service after parameter update") # Create an inline Lambda that can restart an ECS service, since this # isn't built-in CloudFormation functionality. stack.Resources[ "RestartLambda"] = restart_service_lambda = Function.create_from_python_function( handler=restart_ecs_service_resource_handler, Role=restart_role) # The Lambda timeout should be a bit longer than the restart timeout, # to give some leeway. restart_service_lambda.Properties.Timeout = timeout + 15 # Create a custom resource to restart the ECS service. # # We don't want the service restart to happen until the parameters have # all been created, so we need to have the Restarter resource depend on # the parameters (either implicitly or via DependsOn). We also want the # restart to only happen if the parameters have actually changed - this # can be done if we make the SSM Parameters be part of the resource # specification (both the key and the value). stack.Resources["Restarter"] = dict( Type="Custom::RestartEcsService", Properties=dict( ServiceToken=GetAtt(restart_service_lambda, "Arn"), ClusterArn=cluster, ServiceArn=service, IgnoredParameterNames=[Ref(p) for p in dependencies], IgnoredParameterKeys=[GetAtt(p, "Value") for p in dependencies], ), ) # TODO consider creating a waiter anyway, so that the timeout is strictly reliable return stack
def test_attribute_name_contains_an_unsupported_function(self): # Exercise with pytest.raises(ValueError) as excinfo: _ = GetAtt({"one": 42}, "Attrib", GetAZs()) assert "GetAZs" in str(excinfo.value)