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)
예제 #4
0
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
예제 #12
0
    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
예제 #13
0
    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
예제 #14
0
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)
예제 #18
0
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)