Search code examples
pythonamazon-web-servicespytestaws-ssmmoto

add to moto default aws ssm parameters


I am trying to run a set of tests where calls to boto3.client('ssm') are mocked using moto.

Moto is providing a set of default aws parameter. https://github.com/spulec/moto/blob/master/moto/ssm/models.py#L59 but is preventing from adding more: https://github.com/spulec/moto/blob/master/moto/ssm/models.py#L858 Trying to actively add any aws prefix parameter will return an error as per the tests in https://github.com/spulec/moto/blob/master/tests/test_ssm/test_ssm_boto3.py#L397

As my lambda is relying on the following to be present my test fails: /aws/service/ecs/optimized-ami/amazon-linux-2/recommended

I was thinking of trying to monkey patch the mocked ssm client, but I have very little understanding of moto's internals.

I have been following this example but modifying it for my needs (calling SSM instead of calling SQS and S3). For ref my code looks like this as I have attempted to monkey patch the put_parameter method without success.

app.py

import boto3
from loguru import logger


@logger.catch()
def lambda_handler(event, context):
    ssm_client = boto3.client("ssm", "eu-west-1")

    ami_param_name = "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-ebs"
    ami_param_value = ssm_client.get_parameter(Name=ami_param_name)

    ecs_param_name = "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended"
    ecs_param_value = ssm_client.get_parameter(Name=ecs_param_name)

    return [ami_param_value, ecs_param_value]

test.py

import os
from unittest import mock

import boto3
import pytest
from moto import mock_ssm

from src.app import lambda_handler

AWS_REGION = 'eu-west-1'


@pytest.fixture(scope="function")
def aws_credentials():
    """Mocked AWS Credentials for moto."""
    os.environ["AWS_ACCESS_KEY_ID"] = "testing"
    os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
    os.environ["AWS_SECURITY_TOKEN"] = "testing"
    os.environ["AWS_SESSION_TOKEN"] = "testing"


@pytest.fixture(scope="function")
def mock_ssm_client(aws_credentials):
    with mock_ssm():
        client = boto3.client("ssm", region_name=AWS_REGION)

        # already present in moto
        # client.put_parameter(
        #     Name='/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-ebs',
        #     Type='String',
        #     Value='ami-stdparam'
        # )

        # What the lambda requires
        # client.put_parameter(
        #     Name='/aws/service/ecs/optimized-ami/amazon-linux-2/recommended',
        #     Type='String',
        #     Value='{"image_id": "ami-ecsparam"}'
        # )

        def side_effect(path):
            if path == "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended":
                return_value = {
                    "Parameter": {
                        "Name": "/aws/service/ecs/optimized-ami/amazon-linux-2/recommended",
                        "Type": "String",
                        "Value": "{\"ecs_agent_version\":\"1.63.1\",\"ecs_runtime_version\":\"Docker version 20.10.13\",\"image_id\":\"ami-002e2fef4b94f8fd0\",\"image_name\":\"amzn2-ami-ecs-hvm-2.0.20220921-x86_64-ebs\",\"image_version\":\"2.0.20220921\",\"os\":\"Amazon Linux 2\",\"schema_version\":1,\"source_image_name\":\"amzn2-ami-minimal-hvm-2.0.20220912.1-x86_64-ebs\"}",
                        "Version": 94,
                        "LastModifiedDate": 1664230158.399,
                        "ARN": "arn:aws:ssm:eu-west-1::parameter/aws/service/ecs/optimized-ami/amazon-linux-2/recommended",
                        "DataType": "text"
                    }
                }

                return return_value
            else:
                return client.get_parameter(path)

        client.get_parameter = mock.patch(
            'boto3.client.get_parameter',
            side_effect=side_effect
        )
        yield client


def test_lambda_handler(mock_ssm_client):
    # Arrange

    # Act
    results = lambda_handler('', 'test')

    # Assert
    assert len(results) == 2

Solution

  • You could use Moto's internal API to store the parameter, as a workaround to mocking/patching Moto.

    See the following code to add a custom parameter called /aws/test:

    @mock_ssm
    def test_default_param():
        client = boto3.client("ssm", region_name="us-east-1")
    
        from moto.ssm.models import ssm_backends, Parameter
        ssm_backends["123456789012"]["us-east-1"]._parameters["/aws/test"].append(Parameter(
            account_id="123456789012",
            name="/aws/test",
            value="val",
            parameter_type="String",
            description="...",
            allowed_pattern=None,
            keyid=None,
            last_modified_date=1664230158.399,
            version=None,
            tags=[],
            data_type="text",
        ))
    
        response = client.get_parameters(Names=["/aws/test"])
        print(response)
    

    Note that this works in the latest version of Moto (4.0.6), but as it's an internal API, it is liable to change.