From 83b0cdbd0ada73b6a75cc52cc10778a3ef8cbfa6 Mon Sep 17 00:00:00 2001 From: Kevin DeJong Date: Mon, 15 Jul 2024 11:52:47 -0700 Subject: [PATCH] Add two new rules to validate fargate tasks (#3464) * Add rule E3047 to validate ECS Fargate cpu/memory * Add two new rules to validate fargate tasks --- .../fargate_cpu_memory.json | 136 ++++++++++++++++++ .../fargate_properties.json | 57 ++++++++ .../rules/resources/ecs/FargateCpuMemory.py | 41 ++++++ .../resources/ecs/TaskFargateProperties.py | 46 ++++++ .../ecs/test_ecs_fargate_cpu_memory.py | 132 +++++++++++++++++ .../ecs/test_ecs_task_fargate_properties.py | 109 ++++++++++++++ 6 files changed, 521 insertions(+) create mode 100644 src/cfnlint/data/schemas/extensions/aws_ecs_taskdefinition/fargate_cpu_memory.json create mode 100644 src/cfnlint/data/schemas/extensions/aws_ecs_taskdefinition/fargate_properties.json create mode 100644 src/cfnlint/rules/resources/ecs/FargateCpuMemory.py create mode 100644 src/cfnlint/rules/resources/ecs/TaskFargateProperties.py create mode 100644 test/unit/rules/resources/ecs/test_ecs_fargate_cpu_memory.py create mode 100644 test/unit/rules/resources/ecs/test_ecs_task_fargate_properties.py diff --git a/src/cfnlint/data/schemas/extensions/aws_ecs_taskdefinition/fargate_cpu_memory.json b/src/cfnlint/data/schemas/extensions/aws_ecs_taskdefinition/fargate_cpu_memory.json new file mode 100644 index 0000000000..3fc18efad7 --- /dev/null +++ b/src/cfnlint/data/schemas/extensions/aws_ecs_taskdefinition/fargate_cpu_memory.json @@ -0,0 +1,136 @@ +{ + "if": { + "properties": { + "Cpu": { + "type": [ + "string", + "integer" + ] + }, + "Memory": { + "type": [ + "string", + "integer" + ] + }, + "RequiresCompatibilities": { + "contains": { + "enum": [ + "FARGATE" + ], + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "RequiresCompatibilities", + "Cpu", + "Memory" + ] + }, + "then": { + "anyOf": [ + { + "properties": { + "Cpu": { + "enum": [ + "256" + ] + }, + "Memory": { + "enum": [ + "512", + "1024", + "2048" + ] + } + } + }, + { + "properties": { + "Cpu": { + "enum": [ + "512" + ] + }, + "Memory": { + "maximum": 4096, + "minimum": 1024, + "multipleOf": 1024 + } + } + }, + { + "properties": { + "Cpu": { + "enum": [ + "1024" + ] + }, + "Memory": { + "maximum": 8192, + "minimum": 2048, + "multipleOf": 1024 + } + } + }, + { + "properties": { + "Cpu": { + "enum": [ + "2048" + ] + }, + "Memory": { + "maximum": 16384, + "minimum": 4096, + "multipleOf": 1024 + } + } + }, + { + "properties": { + "Cpu": { + "enum": [ + "4096" + ] + }, + "Memory": { + "maximum": 30720, + "minimum": 8192, + "multipleOf": 1024 + } + } + }, + { + "properties": { + "Cpu": { + "enum": [ + "8192" + ] + }, + "Memory": { + "maximum": 61440, + "minimum": 16384, + "multipleOf": 4096 + } + } + }, + { + "properties": { + "Cpu": { + "enum": [ + "16384" + ] + }, + "Memory": { + "maximum": 122880, + "minimum": 32768, + "multipleOf": 8192 + } + } + } + ] + } +} diff --git a/src/cfnlint/data/schemas/extensions/aws_ecs_taskdefinition/fargate_properties.json b/src/cfnlint/data/schemas/extensions/aws_ecs_taskdefinition/fargate_properties.json new file mode 100644 index 0000000000..c590df4986 --- /dev/null +++ b/src/cfnlint/data/schemas/extensions/aws_ecs_taskdefinition/fargate_properties.json @@ -0,0 +1,57 @@ +{ + "if": { + "properties": { + "RequiresCompatibilities": { + "contains": { + "enum": [ + "FARGATE" + ], + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "RequiresCompatibilities" + ] + }, + "then": { + "if": { + "properties": { + "Cpu": { + "type": [ + "string", + "integer" + ] + } + }, + "required": [ + "Cpu" + ] + }, + "not": { + "required": [ + "PlacementConstraints" + ] + }, + "required": [ + "Cpu", + "Memory" + ], + "then": { + "properties": { + "Cpu": { + "enum": [ + "256", + "512", + "1024", + "2048", + "4096", + "8192", + "16384" + ] + } + } + } + } +} diff --git a/src/cfnlint/rules/resources/ecs/FargateCpuMemory.py b/src/cfnlint/rules/resources/ecs/FargateCpuMemory.py new file mode 100644 index 0000000000..af5c7626c6 --- /dev/null +++ b/src/cfnlint/rules/resources/ecs/FargateCpuMemory.py @@ -0,0 +1,41 @@ +""" +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_ecs_taskdefinition +from cfnlint.jsonschema import ValidationError +from cfnlint.rules.jsonschema.CfnLintJsonSchema import CfnLintJsonSchema, SchemaDetails + + +class FargateCpuMemory(CfnLintJsonSchema): + id = "E3047" + shortdesc = ( + "Validate ECS Fargate tasks have the right combination of CPU and memory" + ) + description = ( + "When using a ECS Fargate task there is a specfic combination " + "of memory and cpu that can be used" + ) + source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html#cfn-ecs-taskdefinition-memory" + tags = ["properties", "ecs", "service", "container", "fargate"] + + def __init__(self) -> None: + super().__init__( + keywords=["Resources/AWS::ECS::TaskDefinition/Properties"], + schema_details=SchemaDetails( + module=cfnlint.data.schemas.extensions.aws_ecs_taskdefinition, + filename="fargate_cpu_memory.json", + ), + ) + + def message(self, instance: Any, err: ValidationError) -> str: + return ( + f"Cpu {instance.get('Cpu')!r} is not " + "compatible with memory " + f"{instance.get('Memory')!r}" + ) diff --git a/src/cfnlint/rules/resources/ecs/TaskFargateProperties.py b/src/cfnlint/rules/resources/ecs/TaskFargateProperties.py new file mode 100644 index 0000000000..37cba5b5d7 --- /dev/null +++ b/src/cfnlint/rules/resources/ecs/TaskFargateProperties.py @@ -0,0 +1,46 @@ +""" +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: MIT-0 +""" + +from __future__ import annotations + +from collections import deque +from typing import Any + +import cfnlint.data.schemas.extensions.aws_ecs_taskdefinition +from cfnlint.jsonschema import ValidationResult +from cfnlint.jsonschema.protocols import Validator +from cfnlint.rules.jsonschema.CfnLintJsonSchema import CfnLintJsonSchema, SchemaDetails + + +class TaskFargateProperties(CfnLintJsonSchema): + id = "E3048" + shortdesc = "Validate ECS Fargate tasks have required properties and values" + description = ( + "When using a ECS Fargate task there is a specfic combination " + "of required properties and values" + ) + source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-taskdefinition.html#cfn-ecs-taskdefinition-memory" + tags = ["properties", "ecs", "service", "container", "fargate"] + + def __init__(self) -> None: + super().__init__( + keywords=["Resources/AWS::ECS::TaskDefinition/Properties"], + schema_details=SchemaDetails( + module=cfnlint.data.schemas.extensions.aws_ecs_taskdefinition, + filename="fargate_properties.json", + ), + all_matches=True, + ) + + def validate( + self, validator: Validator, keywords: Any, instance: Any, schema: dict[str, Any] + ) -> ValidationResult: + for err in super().validate(validator, keywords, instance, schema): + if err.validator == "not": + err.message = "'PlacementConstraints' isn't supported for Fargate tasks" + err.path = deque(["PlacementConstraints"]) + yield err + continue + yield err diff --git a/test/unit/rules/resources/ecs/test_ecs_fargate_cpu_memory.py b/test/unit/rules/resources/ecs/test_ecs_fargate_cpu_memory.py new file mode 100644 index 0000000000..7aacac0af5 --- /dev/null +++ b/test/unit/rules/resources/ecs/test_ecs_fargate_cpu_memory.py @@ -0,0 +1,132 @@ +""" +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.ecs.FargateCpuMemory import FargateCpuMemory + + +@pytest.fixture(scope="module") +def rule(): + rule = FargateCpuMemory() + yield rule + + +@pytest.mark.parametrize( + "instance,expected", + [ + ( + { + "RequiresCompatibilities": ["FARGATE"], + "Cpu": 256, + "Memory": "512", + }, + [], + ), + ( + { + "RequiresCompatibilities": ["FARGATE"], + "Cpu": "512", + "Memory": 1024, + }, + [], + ), + ( + { + "RequiresCompatibilities": ["FARGATE"], + "Cpu": "1024", + "Memory": "2048", + }, + [], + ), + ( + { + "RequiresCompatibilities": ["FARGATE"], + "Cpu": 2048, + "Memory": 4096, + }, + [], + ), + ( + { + "RequiresCompatibilities": ["FARGATE"], + "Cpu": 4096, + "Memory": 30720, + }, + [], + ), + ( + { + "RequiresCompatibilities": ["FARGATE"], + "Cpu": 8192, + "Memory": 16384, + }, + [], + ), + ( + { + "RequiresCompatibilities": ["FARGATE"], + "Cpu": 16384, + "Memory": 122880, + }, + [], + ), + ( + { + "RequiresCompatibilities": ["FARGATE"], + "Cpu": 16384, + "Memory": 123904, + }, + [ + ValidationError( + "Cpu 16384 is not compatible with memory 123904", + rule=FargateCpuMemory(), + path=deque([]), + validator="anyOf", + schema_path=deque(["then", "anyOf"]), + ) + ], + ), + ( + { + "RequiresCompatibilities": ["FARGATE", "Foo"], + "Cpu": 4096, + "Memory": 512, + }, + [ + ValidationError( + "Cpu 4096 is not compatible with memory 512", + rule=FargateCpuMemory(), + path=deque([]), + validator="anyOf", + schema_path=deque(["then", "anyOf"]), + ) + ], + ), + ( + { + "RequiresCompatibilities": ["FARGATE", "Foo"], + "Cpu": 4096, + "Memory": 16385, + }, + [ + ValidationError( + "Cpu 4096 is not compatible with memory 16385", + rule=FargateCpuMemory(), + path=deque([]), + validator="anyOf", + schema_path=deque(["then", "anyOf"]), + ) + ], + ), + ], +) +def test_validate(instance, expected, rule, validator): + errs = list(rule.validate(validator, "", instance, {})) + + assert errs == expected, f"Expected {expected} got {errs}" diff --git a/test/unit/rules/resources/ecs/test_ecs_task_fargate_properties.py b/test/unit/rules/resources/ecs/test_ecs_task_fargate_properties.py new file mode 100644 index 0000000000..846bd2c023 --- /dev/null +++ b/test/unit/rules/resources/ecs/test_ecs_task_fargate_properties.py @@ -0,0 +1,109 @@ +""" +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.ecs.TaskFargateProperties import TaskFargateProperties + + +@pytest.fixture(scope="module") +def rule(): + rule = TaskFargateProperties() + yield rule + + +@pytest.mark.parametrize( + "instance,expected", + [ + ( + { + "RequiresCompatibilities": ["FARGATE"], + "Cpu": 256, + "Memory": "512", + }, + [], + ), + ( + { + "RequiresCompatibilities": ["FARGATE"], + "Cpu": 16384, + }, + [ + ValidationError( + "'Memory' is a required property", + rule=TaskFargateProperties(), + path=deque([]), + validator="required", + schema_path=deque(["then", "required"]), + ) + ], + ), + ( + { + "RequiresCompatibilities": ["FARGATE"], + "Memory": "512", + }, + [ + ValidationError( + "'Cpu' is a required property", + rule=TaskFargateProperties(), + path=deque([]), + validator="required", + schema_path=deque(["then", "required"]), + ) + ], + ), + ( + { + "RequiresCompatibilities": ["FARGATE"], + "Memory": "512", + "Cpu": 256, + "PlacementConstraints": "foo", + }, + [ + ValidationError( + ("'PlacementConstraints' isn't supported for Fargate tasks"), + rule=TaskFargateProperties(), + path=deque(["PlacementConstraints"]), + validator="not", + schema_path=deque(["then", "not"]), + ) + ], + ), + ( + { + "RequiresCompatibilities": ["FARGATE"], + "Cpu": {"Ref": "MyParameter"}, + "Memory": "512", + }, + [], + ), + ( + { + "RequiresCompatibilities": ["FARGATE"], + "Cpu": 128, + "Memory": "512", + }, + [ + ValidationError( + ( + "128 is not one of ['256', '512', '1024', " + "'2048', '4096', '8192', '16384']" + ), + rule=TaskFargateProperties(), + path=deque(["Cpu"]), + validator="enum", + schema_path=deque(["then", "then", "properties", "Cpu", "enum"]), + ) + ], + ), + ], +) +def test_validate(instance, expected, rule, validator): + errs = list(rule.validate(validator, "", instance, {})) + assert errs == expected, f"Expected {expected} got {errs}"