Esempio n. 1
0
def simple_scaling_policy(alarm, asg_name, downscale=False):
    """Create a simple scaling policy using the supplied alarm."""
    stack = Stack(Description="Resources for a single scaling policy.")

    scaling_policy = ScalingPolicy(
        Properties=dict(
            AdjustmentType="ChangeInCapacity",  # TODO consider making this a lookup value
            AutoScalingGroupName=asg_name,
            Cooldown=1,
            ScalingAdjustment=-1 if downscale else 1,
        ),
    )
    stack.Resources["ScalingPolicy"] = scaling_policy

    # TODO need properties to be a real object (not a dict), and to auto-create empty lists.
    alarm.Properties.setdefault("AlarmActions", []).append(fn.Ref(scaling_policy))
    alarm.Properties.setdefault("Dimensions", []).append(
        # TODO logical class that wraps this up instead, and allows you to express in a mroe convenient way
        dict(
            Name="AutoScalingGroupName",
            Value=asg_name,
        )
    )
    stack.Resources["ScalingAlarm"] = alarm

    return stack
Esempio n. 2
0
def _create_embedded_invalidations(appconfig: dict, stack: Stack):
    """Invalidate the cache in applications that use some of these parameters
    (by restarting the application), as specified by configuration embedded
    inline in the input file.
    """
    invalidatable_services = appconfig.get(".ssmash-config",
                                           {}).get("invalidations")
    if not invalidatable_services:
        return

    clean_config = dict(appconfig)
    clean_config.pop(".ssmash-config", None)
    invalidated_resources = _get_invalidated_resources(clean_config)

    for appname, appresources in invalidated_resources.items():
        invalidator = invalidatable_services.get(appname)
        if not invalidator:
            # TODO this error message is a bit fragile
            raise ValueError(
                f"Parameter {appresources[0].Properties.Name} invalidates service {appname}, but that service is not defined."
            )

        stack.merge_stack(
            invalidator.create_resources(appresources).with_prefixed_names(
                "Invalidate" + clean_logical_name(appname)))
Esempio n. 3
0
    def test_find_a_pseudo_parameter(self):
        # Setup
        data = AWS_Region
        stack = Stack()

        # Exercise & Verify
        assert stack.get_logical_name(data) == "AWS::Region"
Esempio n. 4
0
def create_params_from_dict(stack: Stack,
                            appconfig: dict,
                            path_prefix: str = "/") -> None:
    for key, value in appconfig.items():
        _check_path_component_is_valid(key)
        item_path = path_prefix + key

        # Nested dictionaries form a parameter hierarchy
        if isinstance(value, dict):
            create_params_from_dict(stack, value, item_path + "/")
            continue

        # Store this value as a parameter
        logical_name = _clean_logical_name(item_path)
        logical_name = _dedupe_logical_name(stack, logical_name)

        if isinstance(value, list):
            # Store lists of plain values as a StringList
            stack.Resources[logical_name] = SSMParameter(
                Properties=SSMParameterProperties(
                    Name=item_path,
                    Type="StringList",
                    Value=_get_list_parameter_value(value),
                ))
        else:
            # Plain values should be stored as a string parameter
            stack.Resources[logical_name] = SSMParameter(
                Properties=SSMParameterProperties(
                    Name=item_path,
                    Type="String",
                    Value=_get_plain_parameter_value(value),
                ))
Esempio n. 5
0
    def test_find_a_resource_when_only_searching_resources(self):
        # Setup
        name = "Foo"
        stack = Stack()
        data = ZeroAttributeObject()
        stack.Resources[name] = data

        # Exercise & Verify
        assert stack.get_logical_name(data, resources_only=True) == name
Esempio n. 6
0
    def test_prefix_cannot_be_empty(self):
        # Setup
        stack = Stack(Resources={"SomeName": SimpleResource()})

        # Exercise & Verify
        with pytest.raises(ValueError) as excinfo:
            _ = stack.with_prefixed_names("")

        assert "empty" in str(excinfo.value).lower()
Esempio n. 7
0
    def test_prefix_must_be_string(self, prefix):
        # Setup
        stack = Stack(Resources={"SomeName": SimpleResource()})

        # Exercise & Verify
        with pytest.raises(TypeError) as excinfo:
            _ = stack.with_prefixed_names(prefix)

        assert "string" in str(excinfo.value).lower()
Esempio n. 8
0
    def test_prefix_cannot_contain_special_characters(self, prefix):
        # Setup
        stack = Stack(Resources={"SomeName": SimpleResource()})

        # Exercise & Verify
        with pytest.raises(ValueError) as excinfo:
            _ = stack.with_prefixed_names(prefix)

        assert "alphanumeric" in str(excinfo.value).lower()
Esempio n. 9
0
    def test_find_a_parameter(self):
        # Setup
        name = "Foo"
        stack = Stack()
        data = Parameter(Type="String")
        stack.Parameters[name] = data

        # Exercise & Verify
        assert stack.get_logical_name(data) == name
Esempio n. 10
0
    def test_find_a_resource(self):
        # Setup
        name = "Foo"
        stack = Stack()
        data = ZeroAttributeObject()
        stack.Resources[name] = data

        # Exercise & Verify
        assert stack.get_logical_name(data) == name
Esempio n. 11
0
    def test_find_a_resource_which_is_a_plain_dict(self):
        # Setup
        name = "Foo"
        stack = Stack()
        data = dict()
        stack.Resources[name] = data

        # Exercise & Verify
        assert stack.get_logical_name(data) == name
Esempio n. 12
0
    def test_cannot_merge_if_sam_transform_version_is_different(self):
        source = Stack(Transform="123")
        target = Stack(Transform="456")

        # Exercise & Verify
        with pytest.raises(StackMergeError) as excinfo:
            target.merge_stack(source)

        assert "transform version" in str(excinfo.value).lower()
Esempio n. 13
0
    def test_cannot_merge_if_template_version_is_different(self):
        source = Stack(AWSTemplateFormatVersion="123")
        target = Stack(AWSTemplateFormatVersion="456")

        # Exercise & Verify
        with pytest.raises(StackMergeError) as excinfo:
            target.merge_stack(source)

        assert "template version" in str(excinfo.value).lower()
Esempio n. 14
0
    def test_merge_returns_target_stack(self):
        # Setup
        source = Stack(Resources={"SomeResource": SimpleResource()})
        target = Stack()

        # Exercise
        result = target.merge_stack(source)

        # Verify
        assert target is result
Esempio n. 15
0
    def test_fail_if_object_doesnt_exist(self):
        # Setup
        stack = Stack()
        data = ZeroAttributeObject()

        # Exercise & Verify
        with pytest.raises(ValueError) as excinfo:
            stack.get_logical_name(data)

        assert "not part of this stack" in str(excinfo.value)
Esempio n. 16
0
    def test_return_value_is_a_new_stack(self):
        # Setup
        stack = Stack(Resources={"SomeName": SimpleResource()})

        # Exercise
        new_stack = stack.with_prefixed_names(self.STACK_PREFIX)

        # Verify
        assert isinstance(new_stack, Stack)
        assert new_stack is not stack
Esempio n. 17
0
    def test_prefix_must_have_leading_capital(self):
        # Setup
        stack = Stack(Resources={"SomeName": SimpleResource()})

        # Exercise & Verify
        with pytest.raises(ValueError) as excinfo:
            _ = stack.with_prefixed_names(
                "lowercasedCamelsAreBactrianButInvalid")

        assert "uppercase" in str(excinfo.value).lower()
Esempio n. 18
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
Esempio n. 19
0
    def test_only_search_resources_when_requested(self, object_type):
        # Setup
        stack = Stack()
        data = ZeroAttributeObject()
        setattr(stack, object_type, {"Foo": data})

        # Exercise & Verify
        with pytest.raises(ValueError) as excinfo:
            stack.get_logical_name(data, resources_only=True)

        assert "not part of this stack" in str(excinfo.value)
Esempio n. 20
0
    def test_dont_create_export_name_for_output_when_it_is_not_set(self):
        # Setup
        name = "SomeItemName"
        stack = Stack(Outputs={name: (Output(Value="HelloWorld"))})

        # Exercise
        new_stack = stack.with_prefixed_names(self.STACK_PREFIX)

        # Verify
        new_output = new_stack.Outputs[self.STACK_PREFIX + name]
        assert getattr(new_output, "Export", None) is None
Esempio n. 21
0
    def test_modify_content_of_description(self):
        # Setup
        stack = Stack(Description=LOREM_IPSUM)

        # Exercise
        new_stack = stack.with_prefixed_names(self.STACK_PREFIX)

        # Verify
        assert self.STACK_PREFIX in new_stack.Description
        assert LOREM_IPSUM in new_stack.Description
        assert stack.Description == LOREM_IPSUM, "Old stack should not be modified"
Esempio n. 22
0
    def test_fail_if_object_is_pseudo_parameter_when_only_searching_resources(
            self):
        # Setup
        data = AWS_Region
        stack = Stack()

        # Exercise & Verify
        with pytest.raises(ValueError) as excinfo:
            stack.get_logical_name(data, resources_only=True)

        assert "not part of this stack" in str(excinfo.value)
Esempio n. 23
0
    def test_does_not_copy_description(self):
        # Setup
        source = Stack(Description="Source Description")
        original_description = "Target Description"
        target = Stack(Description=original_description)

        # Exercise
        target.merge_stack(source)

        # Verify
        assert target.Description == original_description
Esempio n. 24
0
def _initialise_stack(description: str) -> Stack:
    """Create a basic Flying Circus stack, customised for ssmash"""
    stack = Stack(Description=description)

    from ssmash import __version__

    stack.Metadata["ssmash"] = {
        "generated_timestamp": datetime.now(tz=timezone.utc).isoformat(),
        "version": __version__,
    }
    return stack
Esempio n. 25
0
    def test_create_description_when_it_is_not_set(self):
        # Setup
        stack = Stack()

        # Exercise
        new_stack = stack.with_prefixed_names(self.STACK_PREFIX)

        # Verify
        assert self.STACK_PREFIX == new_stack.Description
        assert (not hasattr(stack, "Description") or
                stack.Description is None), "Old stack should not be modified"
Esempio n. 26
0
def create_ec2_stack():
    stack = Stack()
    stack.Resources["WebServer"] = get_standard_ec2_instance("Web Server")
    stack.Resources["DBServer"] = dbserver = get_standard_ec2_instance("Database Server", instance_type="t2.nano")

    stack.Resources["DBServerAlarm"] = alarm = Alarms.high_cpu(85)
    alarm.Properties.setdefault("Dimensions", []).append({
        "Name": "InstanceId",
        "Value": fn.Ref(dbserver),
    })

    return stack
Esempio n. 27
0
    def test_copy_cfn_template_version(self):
        """AWSTemplateFormatVersion should be a string which we copy across unchanged"""
        # Setup
        version_string = "WhatIfThisWaSemanticallyVersioned.1.0"
        stack = Stack(AWSTemplateFormatVersion=version_string)

        # Exercise
        new_stack = stack.with_prefixed_names(self.STACK_PREFIX)

        # Verify
        assert new_stack.AWSTemplateFormatVersion == version_string
        assert stack.AWSTemplateFormatVersion == version_string, "Old stack should not be modified"
Esempio n. 28
0
    def test_stack_cannot_be_set_when_it_is_already_set(self):
        # Setup
        dumper = AmazonCFNDumper(None)
        stack1 = Stack()
        stack2 = Stack()

        dumper.cfn_stack = stack1

        # Exercise & Verify
        with pytest.raises(RuntimeError) as excinfo:
            dumper.cfn_stack = stack2

        assert "already set" in str(excinfo.value)
Esempio n. 29
0
    def test_item_is_added_to_the_target_stack(self, stack_attribute, item):
        # Setup
        item_name = "SomeChildProperty"
        source = Stack()
        source[stack_attribute] = {item_name: item}
        target = Stack()

        # Exercise
        target.merge_stack(source)

        # Verify
        assert len(target[stack_attribute]) == 1
        assert target[stack_attribute][item_name] is item
Esempio n. 30
0
    def test_export_basic_stack(self):
        """Should be able to create and export a simple stack example."""
        stack = Stack()
        stack.Resources["SomeName"] = SimpleResource()
        output = stack.export("yaml")

        assert output == dedent("""
        ---
        AWSTemplateFormatVersion: '2010-09-09'
        Resources:
          SomeName:
            Type: NameSpace::Service::Resource
        """)