From eb97e5626b7e84b49686c3a6f2cd36e657bd594b Mon Sep 17 00:00:00 2001 From: Kevin DeJong Date: Sat, 7 Sep 2024 09:00:49 -0700 Subject: [PATCH] Create rule E3056 to validate HealthCheck --- .../healthcheckgraceperiodseconds.json | 35 ++++++ .../ServiceHealthCheckGracePeriodSeconds.py | 58 +++++++++ ...rvice_health_check_grace_period_seconds.py | 115 ++++++++++++++++++ 3 files changed, 208 insertions(+) create mode 100644 src/cfnlint/data/schemas/extensions/aws_ecs_service/healthcheckgraceperiodseconds.json create mode 100644 src/cfnlint/rules/resources/ecs/ServiceHealthCheckGracePeriodSeconds.py create mode 100644 test/unit/rules/resources/ecs/test_service_health_check_grace_period_seconds.py diff --git a/src/cfnlint/data/schemas/extensions/aws_ecs_service/healthcheckgraceperiodseconds.json b/src/cfnlint/data/schemas/extensions/aws_ecs_service/healthcheckgraceperiodseconds.json new file mode 100644 index 0000000000..666f84b3e0 --- /dev/null +++ b/src/cfnlint/data/schemas/extensions/aws_ecs_service/healthcheckgraceperiodseconds.json @@ -0,0 +1,35 @@ +{ + "if": { + "properties": { + "HealthCheckGracePeriodSeconds": { + "type": [ + "string", + "integer" + ] + } + }, + "required": [ + "HealthCheckGracePeriodSeconds" + ], + "type": "object" + }, + "then": { + "if": { + "properties": { + "LoadBalancers": { + "type": "array" + } + } + }, + "required": [ + "LoadBalancers" + ], + "then": { + "properties": { + "LoadBalancers": { + "minItems": 1 + } + } + } + } +} diff --git a/src/cfnlint/rules/resources/ecs/ServiceHealthCheckGracePeriodSeconds.py b/src/cfnlint/rules/resources/ecs/ServiceHealthCheckGracePeriodSeconds.py new file mode 100644 index 0000000000..6510a9c846 --- /dev/null +++ b/src/cfnlint/rules/resources/ecs/ServiceHealthCheckGracePeriodSeconds.py @@ -0,0 +1,58 @@ +""" +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_service +from cfnlint.jsonschema import ValidationResult +from cfnlint.jsonschema.protocols import Validator +from cfnlint.rules.jsonschema.CfnLintJsonSchema import CfnLintJsonSchema, SchemaDetails + + +class ServiceHealthCheckGracePeriodSeconds(CfnLintJsonSchema): + id = "E3056" + shortdesc = ( + "ECS service using HealthCheckGracePeriodSeconds " + "must also have LoadBalancers specified" + ) + description = ( + "When using a HealthCheckGracePeriodSeconds on an ECS " + "service, the service must also have a LoadBalancers specified " + "with at least one LoadBalancer in the array." + ) + source_url = "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-service.html#cfn-ecs-service-healthcheckgraceperiodseconds" + tags = ["properties", "ecs", "service", "container"] + + def __init__(self) -> None: + super().__init__( + keywords=["Resources/AWS::ECS::Service/Properties"], + schema_details=SchemaDetails( + module=cfnlint.data.schemas.extensions.aws_ecs_service, + filename="healthcheckgraceperiodseconds.json", + ), + all_matches=True, + ) + + def validate( + self, validator: Validator, keywords: Any, instance: Any, schema: dict[str, Any] + ) -> ValidationResult: + + cfn_validator = self.extend_validator( + validator=validator.evolve( + function_filter=validator.function_filter.evolve( + validate_dynamic_references=False, + add_cfn_lint_keyword=False, + ) + ), + schema=self._schema, + context=validator.context.evolve( + functions=["Fn::If", "Ref"], + strict_types=True, + ), + ) + + yield from self._iter_errors(cfn_validator, instance) diff --git a/test/unit/rules/resources/ecs/test_service_health_check_grace_period_seconds.py b/test/unit/rules/resources/ecs/test_service_health_check_grace_period_seconds.py new file mode 100644 index 0000000000..dcabd42182 --- /dev/null +++ b/test/unit/rules/resources/ecs/test_service_health_check_grace_period_seconds.py @@ -0,0 +1,115 @@ +""" +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.ServiceHealthCheckGracePeriodSeconds import ( + ServiceHealthCheckGracePeriodSeconds, +) + + +@pytest.fixture(scope="module") +def rule(): + rule = ServiceHealthCheckGracePeriodSeconds() + yield rule + + +@pytest.fixture +def template(): + return { + "Conditions": { + "IsUsEast1": {"Fn::Equals": [{"Ref": "AWS::Region"}, "us-east-1"]} + }, + } + + +@pytest.mark.parametrize( + "instance,expected", + [ + ( + {"HealthCheckGracePeriodSeconds": "Foo", "LoadBalancers": ["Bar"]}, + [], + ), + ( + {"LoadBalancers": []}, + [], + ), + ( + [], # wrong type + [], + ), + ( + {"HealthCheckGracePeriodSeconds": "Foo", "LoadBalancers": []}, + [ + ValidationError( + "[] is too short (1)", + rule=ServiceHealthCheckGracePeriodSeconds(), + path=deque(["LoadBalancers"]), + validator="minItems", + schema_path=deque( + ["then", "then", "properties", "LoadBalancers", "minItems"] + ), + ) + ], + ), + ( + { + "HealthCheckGracePeriodSeconds": "Foo", + }, + [ + ValidationError( + "'LoadBalancers' is a required property", + rule=ServiceHealthCheckGracePeriodSeconds(), + path=deque([]), + validator="required", + schema_path=deque(["then", "required"]), + ) + ], + ), + ( + { + "HealthCheckGracePeriodSeconds": "Foo", + "LoadBalancers": [ + {"Fn::If": ["IsUsEast1", "Bar", {"Ref": "AWS::NoValue"}]} + ], + }, + [ + ValidationError( + "[] is too short (1)", + rule=ServiceHealthCheckGracePeriodSeconds(), + path=deque(["LoadBalancers"]), + validator="minItems", + schema_path=deque( + ["then", "then", "properties", "LoadBalancers", "minItems"] + ), + ) + ], + ), + ( + { + "HealthCheckGracePeriodSeconds": "Foo", + "LoadBalancers": { + "Fn::If": ["IsUsEast1", ["Bar"], {"Ref": "AWS::NoValue"}] + }, + }, + [ + ValidationError( + "'LoadBalancers' is a required property", + rule=ServiceHealthCheckGracePeriodSeconds(), + path=deque([]), + validator="required", + schema_path=deque(["then", "required"]), + ) + ], + ), + ], +) +def test_validate(instance, expected, rule, validator): + errs = list(rule.validate(validator, "", instance, {})) + + assert errs == expected, f"Expected {expected} got {errs}"