import copy import datetime import hashlib import json import pkgutil from datetime import timezone import boto3 import botocore.exceptions import pytest import yaml from botocore.exceptions import ClientError from moto import mock_aws from moto.core import DEFAULT_ACCOUNT_ID as ACCOUNT_ID def _get_yaml_template(): return pkgutil.get_data(__name__, "test_templates/good.yaml") def _validate_document_description( doc_name, doc_description, json_doc, expected_document_version, expected_latest_version, expected_default_version, expected_format, ): if expected_format == "JSON": assert doc_description["Hash"] == ( hashlib.sha256(json.dumps(json_doc).encode("utf-8")).hexdigest() ) else: assert doc_description["Hash"] == ( hashlib.sha256(yaml.dump(json_doc).encode("utf-8")).hexdigest() ) assert doc_description["HashType"] == "Sha256" assert doc_description["Name"] == doc_name assert doc_description["Owner"] == ACCOUNT_ID difference = datetime.datetime.now(tz=timezone.utc) - doc_description["CreatedDate"] if difference.min > datetime.timedelta(minutes=1): assert False assert doc_description["Status"] == "Active" assert doc_description["DocumentVersion"] == expected_document_version assert doc_description["Description"] == json_doc["description"] doc_description["Parameters"] = sorted( doc_description["Parameters"], key=lambda doc: doc["Name"] ) assert doc_description["Parameters"][0]["Name"] == "Parameter1" assert doc_description["Parameters"][0]["Type"] == "Integer" assert doc_description["Parameters"][0]["Description"] == "Command Duration." assert doc_description["Parameters"][0]["DefaultValue"] == "3" assert doc_description["Parameters"][1]["Name"] == "Parameter2" assert doc_description["Parameters"][1]["Type"] == "String" assert doc_description["Parameters"][1]["DefaultValue"] == "def" assert doc_description["Parameters"][2]["Name"] == "Parameter3" assert doc_description["Parameters"][2]["Type"] == "Boolean" assert doc_description["Parameters"][2]["Description"] == "A boolean" assert doc_description["Parameters"][2]["DefaultValue"] == "False" assert doc_description["Parameters"][3]["Name"] == "Parameter4" assert doc_description["Parameters"][3]["Type"] == "StringList" assert doc_description["Parameters"][3]["Description"] == "A string list" assert doc_description["Parameters"][3]["DefaultValue"] == '["abc", "def"]' assert doc_description["Parameters"][4]["Name"] == "Parameter5" assert doc_description["Parameters"][4]["Type"] == "StringMap" assert doc_description["Parameters"][5]["Name"] == "Parameter6" assert doc_description["Parameters"][5]["Type"] == "MapList" if expected_format == "JSON": # We have to replace single quotes from the response to package it back up assert json.loads(doc_description["Parameters"][4]["DefaultValue"]) == { "NotificationArn": "$dependency.topicArn", "NotificationEvents": ["Failed"], "NotificationType": "Command", } assert json.loads(doc_description["Parameters"][5]["DefaultValue"]) == [ {"DeviceName": "/dev/sda1", "Ebs": {"VolumeSize": "50"}}, {"DeviceName": "/dev/sdm", "Ebs": {"VolumeSize": "100"}}, ] else: assert yaml.safe_load(doc_description["Parameters"][4]["DefaultValue"]) == { "NotificationArn": "$dependency.topicArn", "NotificationEvents": ["Failed"], "NotificationType": "Command", } assert yaml.safe_load(doc_description["Parameters"][5]["DefaultValue"]) == [ {"DeviceName": "/dev/sda1", "Ebs": {"VolumeSize": "50"}}, {"DeviceName": "/dev/sdm", "Ebs": {"VolumeSize": "100"}}, ] assert doc_description["DocumentType"] == "Command" assert doc_description["SchemaVersion"] == "2.2" assert doc_description["LatestVersion"] == expected_latest_version assert doc_description["DefaultVersion"] == expected_default_version assert doc_description["DocumentFormat"] == expected_format def _get_doc_validator( response, version_name, doc_version, json_doc_content, document_format ): assert response["Name"] == "TestDocument3" if version_name: assert response["VersionName"] == version_name assert response["DocumentVersion"] == doc_version assert response["Status"] == "Active" if document_format == "JSON": assert json.loads(response["Content"]) == json_doc_content else: assert yaml.safe_load(response["Content"]) == json_doc_content assert response["DocumentType"] == "Command" assert response["DocumentFormat"] == document_format @mock_aws def test_create_document(): template_file = _get_yaml_template() json_doc = yaml.safe_load(template_file) client = boto3.client("ssm", region_name="us-east-1") response = client.create_document( Content=yaml.dump(json_doc), Name="TestDocument", DocumentType="Command", DocumentFormat="YAML", ) doc_description = response["DocumentDescription"] _validate_document_description( "TestDocument", doc_description, json_doc, "1", "1", "1", "YAML" ) response = client.create_document( Content=json.dumps(json_doc), Name="TestDocument2", DocumentType="Command", DocumentFormat="JSON", ) doc_description = response["DocumentDescription"] _validate_document_description( "TestDocument2", doc_description, json_doc, "1", "1", "1", "JSON" ) response = client.create_document( Content=json.dumps(json_doc), Name="TestDocument3", DocumentType="Command", DocumentFormat="JSON", VersionName="Base", TargetType="/AWS::EC2::Instance", Tags=[{"Key": "testing", "Value": "testingValue"}], ) doc_description = response["DocumentDescription"] assert doc_description["VersionName"] == "Base" assert doc_description["TargetType"] == "/AWS::EC2::Instance" assert doc_description["Tags"] == [{"Key": "testing", "Value": "testingValue"}] _validate_document_description( "TestDocument3", doc_description, json_doc, "1", "1", "1", "JSON" ) try: client.create_document( Content=json.dumps(json_doc), Name="TestDocument3", DocumentType="Command", DocumentFormat="JSON", ) raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "CreateDocument" assert ( err.response["Error"]["Message"] == "The specified document already exists." ) try: client.create_document( Content=yaml.dump(json_doc), Name="TestDocument4", DocumentType="Command", DocumentFormat="JSON", ) raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "CreateDocument" assert ( err.response["Error"]["Message"] == "The content for the document is not valid." ) del json_doc["parameters"] response = client.create_document( Content=yaml.dump(json_doc), Name="EmptyParamDoc", DocumentType="Command", DocumentFormat="YAML", ) doc_description = response["DocumentDescription"] assert doc_description["Hash"] == ( hashlib.sha256(yaml.dump(json_doc).encode("utf-8")).hexdigest() ) assert doc_description["HashType"] == "Sha256" assert doc_description["Name"] == "EmptyParamDoc" assert doc_description["Owner"] == ACCOUNT_ID difference = datetime.datetime.now(tz=timezone.utc) - doc_description["CreatedDate"] if difference.min > datetime.timedelta(minutes=1): assert False assert doc_description["Status"] == "Active" assert doc_description["DocumentVersion"] == "1" assert doc_description["Description"] == json_doc["description"] assert doc_description["DocumentType"] == "Command" assert doc_description["SchemaVersion"] == "2.2" assert doc_description["LatestVersion"] == "1" assert doc_description["DefaultVersion"] == "1" assert doc_description["DocumentFormat"] == "YAML" @mock_aws def test_get_document(): template_file = _get_yaml_template() json_doc = yaml.safe_load(template_file) client = boto3.client("ssm", region_name="us-east-1") try: client.get_document(Name="DNE") raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "GetDocument" assert ( err.response["Error"]["Message"] == "The specified document does not exist." ) client.create_document( Content=yaml.dump(json_doc), Name="TestDocument3", DocumentType="Command", DocumentFormat="YAML", VersionName="Base", ) new_json_doc = copy.copy(json_doc) new_json_doc["description"] = "a new description" client.update_document( Content=json.dumps(new_json_doc), Name="TestDocument3", DocumentVersion="$LATEST", VersionName="NewBase", ) response = client.get_document(Name="TestDocument3") _get_doc_validator(response, "Base", "1", json_doc, "JSON") response = client.get_document(Name="TestDocument3", DocumentFormat="YAML") _get_doc_validator(response, "Base", "1", json_doc, "YAML") response = client.get_document(Name="TestDocument3", DocumentFormat="JSON") _get_doc_validator(response, "Base", "1", json_doc, "JSON") response = client.get_document(Name="TestDocument3", VersionName="Base") _get_doc_validator(response, "Base", "1", json_doc, "JSON") response = client.get_document(Name="TestDocument3", DocumentVersion="1") _get_doc_validator(response, "Base", "1", json_doc, "JSON") response = client.get_document(Name="TestDocument3", DocumentVersion="2") _get_doc_validator(response, "NewBase", "2", new_json_doc, "JSON") response = client.get_document(Name="TestDocument3", VersionName="NewBase") _get_doc_validator(response, "NewBase", "2", new_json_doc, "JSON") response = client.get_document( Name="TestDocument3", VersionName="NewBase", DocumentVersion="2" ) _get_doc_validator(response, "NewBase", "2", new_json_doc, "JSON") try: response = client.get_document( Name="TestDocument3", VersionName="BadName", DocumentVersion="2" ) raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "GetDocument" assert ( err.response["Error"]["Message"] == "The specified document does not exist." ) try: response = client.get_document(Name="TestDocument3", DocumentVersion="3") raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "GetDocument" assert ( err.response["Error"]["Message"] == "The specified document does not exist." ) # Updating default should update normal get client.update_document_default_version(Name="TestDocument3", DocumentVersion="2") response = client.get_document(Name="TestDocument3", DocumentFormat="JSON") _get_doc_validator(response, "NewBase", "2", new_json_doc, "JSON") @mock_aws def test_delete_document(): template_file = _get_yaml_template() json_doc = yaml.safe_load(template_file) client = boto3.client("ssm", region_name="us-east-1") try: client.delete_document(Name="DNE") raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "DeleteDocument" assert ( err.response["Error"]["Message"] == "The specified document does not exist." ) # Test simple client.create_document( Content=yaml.dump(json_doc), Name="TestDocument3", DocumentType="Command", DocumentFormat="YAML", VersionName="Base", TargetType="/AWS::EC2::Instance", ) client.delete_document(Name="TestDocument3") try: client.get_document(Name="TestDocument3") raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "GetDocument" assert ( err.response["Error"]["Message"] == "The specified document does not exist." ) # Delete default version with other version is bad client.create_document( Content=yaml.dump(json_doc), Name="TestDocument3", DocumentType="Command", DocumentFormat="YAML", VersionName="Base", TargetType="/AWS::EC2::Instance", ) new_json_doc = copy.copy(json_doc) new_json_doc["description"] = "a new description" client.update_document( Content=json.dumps(new_json_doc), Name="TestDocument3", DocumentVersion="$LATEST", VersionName="NewBase", ) new_json_doc["description"] = "a new description2" client.update_document( Content=json.dumps(new_json_doc), Name="TestDocument3", DocumentVersion="$LATEST", ) new_json_doc["description"] = "a new description3" client.update_document( Content=json.dumps(new_json_doc), Name="TestDocument3", DocumentVersion="$LATEST", ) new_json_doc["description"] = "a new description4" client.update_document( Content=json.dumps(new_json_doc), Name="TestDocument3", DocumentVersion="$LATEST", ) try: client.delete_document(Name="TestDocument3", DocumentVersion="1") raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "DeleteDocument" assert err.response["Error"]["Message"] == ( "Default version of the document can't be deleted." ) try: client.delete_document(Name="TestDocument3", VersionName="Base") raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "DeleteDocument" assert err.response["Error"]["Message"] == ( "Default version of the document can't be deleted." ) # Make sure no ill side effects response = client.get_document(Name="TestDocument3") _get_doc_validator(response, "Base", "1", json_doc, "JSON") client.delete_document(Name="TestDocument3", DocumentVersion="5") # Check that latest version is changed response = client.describe_document(Name="TestDocument3") assert response["Document"]["LatestVersion"] == "4" client.delete_document(Name="TestDocument3", VersionName="NewBase") # Make sure other versions okay client.get_document(Name="TestDocument3", DocumentVersion="1") client.get_document(Name="TestDocument3", DocumentVersion="3") client.get_document(Name="TestDocument3", DocumentVersion="4") client.delete_document(Name="TestDocument3") try: client.get_document(Name="TestDocument3", DocumentVersion="1") raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "GetDocument" assert ( err.response["Error"]["Message"] == "The specified document does not exist." ) try: client.get_document(Name="TestDocument3", DocumentVersion="3") raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "GetDocument" assert ( err.response["Error"]["Message"] == "The specified document does not exist." ) try: client.get_document(Name="TestDocument3", DocumentVersion="4") raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "GetDocument" assert ( err.response["Error"]["Message"] == "The specified document does not exist." ) response = client.list_documents() assert len(response["DocumentIdentifiers"]) == 0 @mock_aws def test_update_document_default_version(): template_file = _get_yaml_template() json_doc = yaml.safe_load(template_file) client = boto3.client("ssm", region_name="us-east-1") try: client.update_document_default_version(Name="DNE", DocumentVersion="1") raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "UpdateDocumentDefaultVersion" assert ( err.response["Error"]["Message"] == "The specified document does not exist." ) client.create_document( Content=json.dumps(json_doc), Name="TestDocument", DocumentType="Command", VersionName="Base", ) json_doc["description"] = "a new description" client.update_document( Content=json.dumps(json_doc), Name="TestDocument", DocumentVersion="$LATEST", DocumentFormat="JSON", ) json_doc["description"] = "a new description2" client.update_document( Content=json.dumps(json_doc), Name="TestDocument", DocumentVersion="$LATEST" ) response = client.update_document_default_version( Name="TestDocument", DocumentVersion="2" ) assert response["Description"]["Name"] == "TestDocument" assert response["Description"]["DefaultVersion"] == "2" json_doc["description"] = "a new description3" client.update_document( Content=json.dumps(json_doc), Name="TestDocument", DocumentVersion="$LATEST", VersionName="NewBase", ) response = client.update_document_default_version( Name="TestDocument", DocumentVersion="4" ) assert response["Description"]["Name"] == "TestDocument" assert response["Description"]["DefaultVersion"] == "4" assert response["Description"]["DefaultVersionName"] == "NewBase" @mock_aws def test_update_document(): template_file = _get_yaml_template() json_doc = yaml.safe_load(template_file) client = boto3.client("ssm", region_name="us-east-1") try: client.update_document( Name="DNE", Content=json.dumps(json_doc), DocumentVersion="1", DocumentFormat="JSON", ) raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "UpdateDocument" assert ( err.response["Error"]["Message"] == "The specified document does not exist." ) client.create_document( Content=json.dumps(json_doc), Name="TestDocument", DocumentType="Command", DocumentFormat="JSON", VersionName="Base", ) try: client.update_document( Name="TestDocument", Content=json.dumps(json_doc), DocumentVersion="2", DocumentFormat="JSON", ) raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "UpdateDocument" assert err.response["Error"]["Message"] == ( "The document version is not valid or does not exist." ) # Duplicate content throws an error try: client.update_document( Content=json.dumps(json_doc), Name="TestDocument", DocumentVersion="1", DocumentFormat="JSON", ) raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "UpdateDocument" assert err.response["Error"]["Message"] == ( "The content of the association document matches another " "document. Change the content of the document and try again." ) json_doc["description"] = "a new description" # Duplicate version name try: client.update_document( Content=json.dumps(json_doc), Name="TestDocument", DocumentVersion="1", DocumentFormat="JSON", VersionName="Base", ) raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "UpdateDocument" assert err.response["Error"]["Message"] == ( "The specified version name is a duplicate." ) response = client.update_document( Content=json.dumps(json_doc), Name="TestDocument", VersionName="Base2", DocumentVersion="1", DocumentFormat="JSON", ) assert response["DocumentDescription"]["Description"] == "a new description" assert response["DocumentDescription"]["DocumentVersion"] == "2" assert response["DocumentDescription"]["LatestVersion"] == "2" assert response["DocumentDescription"]["DefaultVersion"] == "1" json_doc["description"] = "a new description2" response = client.update_document( Content=json.dumps(json_doc), Name="TestDocument", DocumentVersion="$LATEST", DocumentFormat="JSON", VersionName="NewBase", ) assert response["DocumentDescription"]["Description"] == "a new description2" assert response["DocumentDescription"]["DocumentVersion"] == "3" assert response["DocumentDescription"]["LatestVersion"] == "3" assert response["DocumentDescription"]["DefaultVersion"] == "1" assert response["DocumentDescription"]["VersionName"] == "NewBase" @mock_aws def test_describe_document(): template_file = _get_yaml_template() json_doc = yaml.safe_load(template_file) client = boto3.client("ssm", region_name="us-east-1") try: client.describe_document(Name="DNE") raise RuntimeError("Should fail") except botocore.exceptions.ClientError as err: assert err.operation_name == "DescribeDocument" assert ( err.response["Error"]["Message"] == "The specified document does not exist." ) client.create_document( Content=yaml.dump(json_doc), Name="TestDocument", DocumentType="Command", DocumentFormat="YAML", VersionName="Base", TargetType="/AWS::EC2::Instance", Tags=[{"Key": "testing", "Value": "testingValue"}], ) response = client.describe_document(Name="TestDocument") doc_description = response["Document"] _validate_document_description( "TestDocument", doc_description, json_doc, "1", "1", "1", "YAML" ) # Adding update to check for issues new_json_doc = copy.copy(json_doc) new_json_doc["description"] = "a new description2" client.update_document( Content=json.dumps(new_json_doc), Name="TestDocument", DocumentVersion="$LATEST" ) response = client.describe_document(Name="TestDocument") doc_description = response["Document"] _validate_document_description( "TestDocument", doc_description, json_doc, "1", "2", "1", "YAML" ) @mock_aws def test_list_documents(): template_file = _get_yaml_template() json_doc = yaml.safe_load(template_file) client = boto3.client("ssm", region_name="us-east-1") client.create_document( Content=json.dumps(json_doc), Name="TestDocument", DocumentType="Command", DocumentFormat="JSON", ) client.create_document( Content=json.dumps(json_doc), Name="TestDocument2", DocumentType="Command", DocumentFormat="JSON", ) client.create_document( Content=json.dumps(json_doc), Name="TestDocument3", DocumentType="Command", DocumentFormat="JSON", TargetType="/AWS::EC2::Instance", ) response = client.list_documents() assert len(response["DocumentIdentifiers"]) == 3 assert response["DocumentIdentifiers"][0]["Name"] == "TestDocument" assert response["DocumentIdentifiers"][1]["Name"] == "TestDocument2" assert response["DocumentIdentifiers"][2]["Name"] == "TestDocument3" assert response["NextToken"] == "" response = client.list_documents(MaxResults=1) assert len(response["DocumentIdentifiers"]) == 1 assert response["DocumentIdentifiers"][0]["Name"] == "TestDocument" assert response["DocumentIdentifiers"][0]["DocumentVersion"] == "1" assert response["NextToken"] == "1" response = client.list_documents(MaxResults=1, NextToken=response["NextToken"]) assert len(response["DocumentIdentifiers"]) == 1 assert response["DocumentIdentifiers"][0]["Name"] == "TestDocument2" assert response["DocumentIdentifiers"][0]["DocumentVersion"] == "1" assert response["NextToken"] == "2" response = client.list_documents(MaxResults=1, NextToken=response["NextToken"]) assert len(response["DocumentIdentifiers"]) == 1 assert response["DocumentIdentifiers"][0]["Name"] == "TestDocument3" assert response["DocumentIdentifiers"][0]["DocumentVersion"] == "1" assert response["NextToken"] == "" # making sure no bad interactions with update json_doc["description"] = "a new description" client.update_document( Content=json.dumps(json_doc), Name="TestDocument", DocumentVersion="$LATEST", DocumentFormat="JSON", ) client.update_document( Content=json.dumps(json_doc), Name="TestDocument2", DocumentVersion="$LATEST", DocumentFormat="JSON", ) client.update_document_default_version(Name="TestDocument", DocumentVersion="2") response = client.list_documents() assert len(response["DocumentIdentifiers"]) == 3 assert response["DocumentIdentifiers"][0]["Name"] == "TestDocument" assert response["DocumentIdentifiers"][0]["DocumentVersion"] == "2" assert response["DocumentIdentifiers"][1]["Name"] == "TestDocument2" assert response["DocumentIdentifiers"][1]["DocumentVersion"] == "1" assert response["DocumentIdentifiers"][2]["Name"] == "TestDocument3" assert response["DocumentIdentifiers"][2]["DocumentVersion"] == "1" assert response["NextToken"] == "" response = client.list_documents(Filters=[{"Key": "Owner", "Values": ["Self"]}]) assert len(response["DocumentIdentifiers"]) == 3 response = client.list_documents( Filters=[{"Key": "TargetType", "Values": ["/AWS::EC2::Instance"]}] ) assert len(response["DocumentIdentifiers"]) == 1 @mock_aws def test_tags_in_list_tags_from_resource_document(): template_file = _get_yaml_template() json_doc = yaml.safe_load(template_file) client = boto3.client("ssm", region_name="us-east-1") client.create_document( Content=json.dumps(json_doc), Name="TestDocument", DocumentType="Command", DocumentFormat="JSON", Tags=[{"Key": "spam", "Value": "ham"}], ) tags = client.list_tags_for_resource( ResourceId="TestDocument", ResourceType="Document" ) assert tags.get("TagList") == [{"Key": "spam", "Value": "ham"}] client.delete_document(Name="TestDocument") with pytest.raises(ClientError) as ex: client.list_tags_for_resource( ResourceType="Document", ResourceId="TestDocument" ) assert ex.value.response["Error"]["Code"] == "InvalidResourceId"