diff --git a/src/cfnlint/data/schemas/extensions/aws_ec2_instance/privateipaddress.json b/src/cfnlint/data/schemas/extensions/aws_ec2_instance/privateipaddress.json new file mode 100644 index 0000000000..c115d4a5a4 --- /dev/null +++ b/src/cfnlint/data/schemas/extensions/aws_ec2_instance/privateipaddress.json @@ -0,0 +1,30 @@ +{ + "if": { + "required": [ + "PrivateIpAddress" + ] + }, + "then": { + "properties": { + "NetworkInterfaces": { + "items": { + "properties": { + "PrivateIpAddresses": { + "items": { + "properties": { + "Primary": { + "enum": [ + false + ] + } + } + }, + "type": "array" + } + } + }, + "type": "array" + } + } + } +} diff --git a/src/cfnlint/rules/resources/ectwo/PrivateIpWithNetworkInterface.py b/src/cfnlint/rules/resources/ectwo/PrivateIpWithNetworkInterface.py new file mode 100644 index 0000000000..47cbc92a4c --- /dev/null +++ b/src/cfnlint/rules/resources/ectwo/PrivateIpWithNetworkInterface.py @@ -0,0 +1,33 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from __future__ import annotations + +from typing import Any + +import cfnlint.data.schemas.extensions.aws_ec2_instance +from cfnlint.jsonschema import ValidationError +from cfnlint.rules.jsonschema.CfnLintJsonSchema import CfnLintJsonSchema, SchemaDetails + + +class PrivateIpWithNetworkInterface(CfnLintJsonSchema): + id = "E3674" + shortdesc = "Primary cannoy be True when PrivateIpAddress is specified" + description = "Only specify the private IP address for an instance in one spot" + tags = ["resources", "ec2"] + + def __init__(self) -> None: + super().__init__( + keywords=[ + "Resources/AWS::EC2::Instance/Properties", + ], + schema_details=SchemaDetails( + module=cfnlint.data.schemas.extensions.aws_ec2_instance, + filename="privateipaddress.json", + ), + ) + + def message(self, instance: Any, err: ValidationError) -> str: + return "'Primary' cannot be True when 'PrivateIpAddress' is specified" diff --git a/test/unit/rules/resources/ec2/test_private_ip_with_network_interafce.py b/test/unit/rules/resources/ec2/test_private_ip_with_network_interafce.py new file mode 100644 index 0000000000..33acf75056 --- /dev/null +++ b/test/unit/rules/resources/ec2/test_private_ip_with_network_interafce.py @@ -0,0 +1,119 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from collections import deque + +import pytest + +from cfnlint.jsonschema import ValidationError +from cfnlint.rules.resources.ectwo.PrivateIpWithNetworkInterface import ( + PrivateIpWithNetworkInterface, +) + + +@pytest.fixture(scope="module") +def rule(): + rule = PrivateIpWithNetworkInterface() + yield rule + + +@pytest.mark.parametrize( + "name,instance,path,expected", + [ + ( + "Valid with no Private Ip Address", + { + "NetworkInterfaces": [ + { + "PrivateIpAddresses": [ + {"PrivateIpAddress": "172.31.35.42", "Primary": True} + ] + } + ] + }, + { + "path": ["Resources", "Instance", "Properties"], + }, + [], + ), + ( + "Valid with Private Ip Address with Primary False", + { + "PrivateIpAddress": "172.31.35.42", + "NetworkInterfaces": [ + { + "PrivateIpAddresses": [ + {"PrivateIpAddress": "172.31.35.42", "Primary": False} + ] + } + ], + }, + { + "path": ["Resources", "Instance", "Properties"], + }, + [], + ), + ( + "Valid with Private Ip Address without Primary specified", + { + "PrivateIpAddress": "172.31.35.42", + "NetworkInterfaces": [ + {"PrivateIpAddresses": [{"PrivateIpAddress": "172.31.35.42"}]} + ], + }, + { + "path": ["Resources", "Instance", "Properties"], + }, + [], + ), + ( + "Invalid with a private ip address", + { + "PrivateIpAddress": "172.31.35.42", + "NetworkInterfaces": [ + { + "PrivateIpAddresses": [ + {"PrivateIpAddress": "172.31.35.42", "Primary": True} + ] + } + ], + }, + { + "path": ["Resources", "Instance", "Properties"], + }, + [ + ValidationError( + "'Primary' cannot be True when 'PrivateIpAddress' is specified", + validator="enum", + rule=PrivateIpWithNetworkInterface(), + path=deque( + ["NetworkInterfaces", 0, "PrivateIpAddresses", 0, "Primary"] + ), + schema_path=deque( + [ + "then", + "properties", + "NetworkInterfaces", + "items", + "properties", + "PrivateIpAddresses", + "items", + "properties", + "Primary", + "enum", + ] + ), + ) + ], + ), + ], + indirect=["path"], +) +def test_validate(name, instance, expected, rule, validator): + errs = list(rule.validate(validator, "", instance, {})) + + assert ( + errs == expected + ), f"Expected test {name!r} to have {expected!r} but got {errs!r}"