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
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)))
def test_find_a_pseudo_parameter(self): # Setup data = AWS_Region stack = Stack() # Exercise & Verify assert stack.get_logical_name(data) == "AWS::Region"
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), ))
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
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()
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()
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()
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
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
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
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()
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()
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
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)
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
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()
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_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)
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
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"
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)
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
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
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"
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
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"
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)
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
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 """)