diff --git a/localstack-core/localstack/services/scheduler/provider.py b/localstack-core/localstack/services/scheduler/provider.py index c587e59133e94..e797fe1fb229c 100644 --- a/localstack-core/localstack/services/scheduler/provider.py +++ b/localstack-core/localstack/services/scheduler/provider.py @@ -1,10 +1,34 @@ import logging +import re -from localstack.aws.api.scheduler import SchedulerApi +from moto.scheduler.models import EventBridgeSchedulerBackend + +from localstack.aws.api.scheduler import SchedulerApi, ValidationException +from localstack.services.events.rule import RULE_SCHEDULE_CRON_REGEX, RULE_SCHEDULE_RATE_REGEX from localstack.services.plugins import ServiceLifecycleHook +from localstack.utils.patch import patch LOG = logging.getLogger(__name__) +AT_REGEX = r"^at[(](0[1-9]|1\d|2[0-8]|29(?=-\d\d-(?!1[01345789]00|2[1235679]00)\d\d(?:[02468][048]|[13579][26]))|30(?!-02)|31(?=-0[13578]|-1[02]))-(0[1-9]|1[0-2])-([12]\d{3}) ([01]\d|2[0-3]):([0-5]\d):([0-5]\d)[)]$" +RULE_SCHEDULE_AT_REGEX = re.compile(AT_REGEX) + class SchedulerProvider(SchedulerApi, ServiceLifecycleHook): pass + + +def _validate_schedule_expression(schedule_expression: str) -> None: + if not ( + RULE_SCHEDULE_CRON_REGEX.match(schedule_expression) + or RULE_SCHEDULE_RATE_REGEX.match(schedule_expression) + or RULE_SCHEDULE_AT_REGEX.match(schedule_expression) + ): + raise ValidationException(f"Invalid Schedule Expression {schedule_expression}.") + + +@patch(EventBridgeSchedulerBackend.create_schedule) +def create_schedule(fn, self, **kwargs): + if schedule_expression := kwargs.get("schedule_expression"): + _validate_schedule_expression(schedule_expression) + return fn(self, **kwargs) diff --git a/tests/aws/services/scheduler/test_scheduler.py b/tests/aws/services/scheduler/test_scheduler.py index 157c8addd4c14..08e24e84c78c6 100644 --- a/tests/aws/services/scheduler/test_scheduler.py +++ b/tests/aws/services/scheduler/test_scheduler.py @@ -1,4 +1,5 @@ import pytest +from botocore.exceptions import ClientError from localstack.testing.aws.util import in_default_partition from localstack.testing.pytest import markers @@ -58,3 +59,47 @@ def test_untag_resource(aws_client, events_scheduler_create_schedule_group, snap assert response["Tags"] == [] snapshot.match("list-untagged-schedule", response) + + +@markers.aws.validated +@pytest.mark.parametrize( + "schedule_expression", + [ + "cron(0 1 * * * *)", + "cron(7 20 * * NOT *)", + "cron(INVALID)", + "cron(0 dummy ? * MON-FRI *)", + "cron(71 8 1 * ? *)", + "cron()", + "rate(10 seconds)", + "rate(10 years)", + "rate()", + "rate(10)", + "rate(10 minutess)", + "rate(foo minutes)", + "rate(-10 minutes)", + "rate( 10 minutes )", + " rate(10 minutes)", + "at(2021-12-31T23:59:59Z)", + "at(2021-12-31)", + ], +) +def tests_create_schedule_with_invalid_schedule_expression( + schedule_expression, aws_client, region_name, account_id, snapshot +): + rule_name = f"rule-{short_uid()}" + + with pytest.raises(ClientError) as e: + aws_client.scheduler.create_schedule( + Name=rule_name, + ScheduleExpression=schedule_expression, + FlexibleTimeWindow={ + "MaximumWindowInMinutes": 4, + "Mode": "FLEXIBLE", + }, + Target={ + "Arn": f"arn:aws:lambda:{region_name}:{account_id}:function:dummy", + "RoleArn": f"arn:aws:iam::{account_id}:role/role-name", + }, + ) + snapshot.match("invalid-schedule-expression", e.value.response) diff --git a/tests/aws/services/scheduler/test_scheduler.snapshot.json b/tests/aws/services/scheduler/test_scheduler.snapshot.json index cb8ccf8a85b06..47eb5b5222c35 100644 --- a/tests/aws/services/scheduler/test_scheduler.snapshot.json +++ b/tests/aws/services/scheduler/test_scheduler.snapshot.json @@ -27,5 +27,277 @@ } } } + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[cron(0 1 * * * *)]": { + "recorded-date": "26-01-2025, 15:45:53", + "recorded-content": { + "invalid-schedule-expression": { + "Error": { + "Code": "ValidationException", + "Message": "Invalid Schedule Expression cron(0 1 * * * *)." + }, + "Message": "Invalid Schedule Expression cron(0 1 * * * *).", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[cron(7 20 * * NOT *)]": { + "recorded-date": "26-01-2025, 15:45:53", + "recorded-content": { + "invalid-schedule-expression": { + "Error": { + "Code": "ValidationException", + "Message": "Invalid Schedule Expression cron(7 20 * * NOT *)." + }, + "Message": "Invalid Schedule Expression cron(7 20 * * NOT *).", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[cron(INVALID)]": { + "recorded-date": "26-01-2025, 15:45:54", + "recorded-content": { + "invalid-schedule-expression": { + "Error": { + "Code": "ValidationException", + "Message": "Invalid Schedule Expression cron(INVALID)." + }, + "Message": "Invalid Schedule Expression cron(INVALID).", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[cron(0 dummy ? * MON-FRI *)]": { + "recorded-date": "26-01-2025, 15:45:54", + "recorded-content": { + "invalid-schedule-expression": { + "Error": { + "Code": "ValidationException", + "Message": "Invalid Schedule Expression cron(0 dummy ? * MON-FRI *)." + }, + "Message": "Invalid Schedule Expression cron(0 dummy ? * MON-FRI *).", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[cron(71 8 1 * ? *)]": { + "recorded-date": "26-01-2025, 15:45:54", + "recorded-content": { + "invalid-schedule-expression": { + "Error": { + "Code": "ValidationException", + "Message": "Invalid Schedule Expression cron(71 8 1 * ? *)." + }, + "Message": "Invalid Schedule Expression cron(71 8 1 * ? *).", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[cron()]": { + "recorded-date": "26-01-2025, 15:45:55", + "recorded-content": { + "invalid-schedule-expression": { + "Error": { + "Code": "ValidationException", + "Message": "Invalid Schedule Expression cron()." + }, + "Message": "Invalid Schedule Expression cron().", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[rate(10 seconds)]": { + "recorded-date": "26-01-2025, 15:45:55", + "recorded-content": { + "invalid-schedule-expression": { + "Error": { + "Code": "ValidationException", + "Message": "Invalid Schedule Expression rate(10 seconds)." + }, + "Message": "Invalid Schedule Expression rate(10 seconds).", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[rate(10 years)]": { + "recorded-date": "26-01-2025, 15:45:56", + "recorded-content": { + "invalid-schedule-expression": { + "Error": { + "Code": "ValidationException", + "Message": "Invalid Schedule Expression rate(10 years)." + }, + "Message": "Invalid Schedule Expression rate(10 years).", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[rate()]": { + "recorded-date": "26-01-2025, 15:45:56", + "recorded-content": { + "invalid-schedule-expression": { + "Error": { + "Code": "ValidationException", + "Message": "Invalid Schedule Expression rate()." + }, + "Message": "Invalid Schedule Expression rate().", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[rate(10)]": { + "recorded-date": "26-01-2025, 15:45:56", + "recorded-content": { + "invalid-schedule-expression": { + "Error": { + "Code": "ValidationException", + "Message": "Invalid Schedule Expression rate(10)." + }, + "Message": "Invalid Schedule Expression rate(10).", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[rate(10 minutess)]": { + "recorded-date": "26-01-2025, 15:45:57", + "recorded-content": { + "invalid-schedule-expression": { + "Error": { + "Code": "ValidationException", + "Message": "Invalid Schedule Expression rate(10 minutess)." + }, + "Message": "Invalid Schedule Expression rate(10 minutess).", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[rate(foo minutes)]": { + "recorded-date": "26-01-2025, 15:45:57", + "recorded-content": { + "invalid-schedule-expression": { + "Error": { + "Code": "ValidationException", + "Message": "Invalid Schedule Expression rate(foo minutes)." + }, + "Message": "Invalid Schedule Expression rate(foo minutes).", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[rate(-10 minutes)]": { + "recorded-date": "26-01-2025, 15:45:57", + "recorded-content": { + "invalid-schedule-expression": { + "Error": { + "Code": "ValidationException", + "Message": "Invalid Schedule Expression rate(-10 minutes)." + }, + "Message": "Invalid Schedule Expression rate(-10 minutes).", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[rate( 10 minutes )]": { + "recorded-date": "26-01-2025, 15:45:58", + "recorded-content": { + "invalid-schedule-expression": { + "Error": { + "Code": "ValidationException", + "Message": "Invalid Schedule Expression rate( 10 minutes )." + }, + "Message": "Invalid Schedule Expression rate( 10 minutes ).", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[ rate(10 minutes)]": { + "recorded-date": "26-01-2025, 15:45:58", + "recorded-content": { + "invalid-schedule-expression": { + "Error": { + "Code": "ValidationException", + "Message": "Invalid Schedule Expression rate(10 minutes)." + }, + "Message": "Invalid Schedule Expression rate(10 minutes).", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[at(2021-12-31T23:59:59Z)]": { + "recorded-date": "26-01-2025, 15:45:59", + "recorded-content": { + "invalid-schedule-expression": { + "Error": { + "Code": "ValidationException", + "Message": "Invalid Schedule Expression at(2021-12-31T23:59:59Z)." + }, + "Message": "Invalid Schedule Expression at(2021-12-31T23:59:59Z).", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[at(2021-12-31)]": { + "recorded-date": "26-01-2025, 15:45:59", + "recorded-content": { + "invalid-schedule-expression": { + "Error": { + "Code": "ValidationException", + "Message": "Invalid Schedule Expression at(2021-12-31)." + }, + "Message": "Invalid Schedule Expression at(2021-12-31).", + "ResponseMetadata": { + "HTTPHeaders": {}, + "HTTPStatusCode": 400 + } + } + } } } diff --git a/tests/aws/services/scheduler/test_scheduler.validation.json b/tests/aws/services/scheduler/test_scheduler.validation.json index 487c07eaf3e43..cd82a4895cbcb 100644 --- a/tests/aws/services/scheduler/test_scheduler.validation.json +++ b/tests/aws/services/scheduler/test_scheduler.validation.json @@ -7,5 +7,56 @@ }, "tests/aws/services/scheduler/test_scheduler.py::test_untag_resource": { "last_validated_date": "2024-12-04T10:08:11+00:00" + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[ rate(10 minutes)]": { + "last_validated_date": "2025-01-26T15:45:58+00:00" + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[at(2021-12-31)]": { + "last_validated_date": "2025-01-26T15:45:59+00:00" + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[at(2021-12-31T23:59:59Z)]": { + "last_validated_date": "2025-01-26T15:45:59+00:00" + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[cron()]": { + "last_validated_date": "2025-01-26T15:45:55+00:00" + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[cron(0 1 * * * *)]": { + "last_validated_date": "2025-01-26T15:45:53+00:00" + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[cron(0 dummy ? * MON-FRI *)]": { + "last_validated_date": "2025-01-26T15:45:54+00:00" + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[cron(7 20 * * NOT *)]": { + "last_validated_date": "2025-01-26T15:45:53+00:00" + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[cron(71 8 1 * ? *)]": { + "last_validated_date": "2025-01-26T15:45:54+00:00" + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[cron(INVALID)]": { + "last_validated_date": "2025-01-26T15:45:54+00:00" + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[rate( 10 minutes )]": { + "last_validated_date": "2025-01-26T15:45:58+00:00" + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[rate()]": { + "last_validated_date": "2025-01-26T15:45:56+00:00" + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[rate(-10 minutes)]": { + "last_validated_date": "2025-01-26T15:45:57+00:00" + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[rate(10 minutess)]": { + "last_validated_date": "2025-01-26T15:45:57+00:00" + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[rate(10 seconds)]": { + "last_validated_date": "2025-01-26T15:45:55+00:00" + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[rate(10 years)]": { + "last_validated_date": "2025-01-26T15:45:56+00:00" + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[rate(10)]": { + "last_validated_date": "2025-01-26T15:45:56+00:00" + }, + "tests/aws/services/scheduler/test_scheduler.py::tests_create_schedule_with_invalid_schedule_expression[rate(foo minutes)]": { + "last_validated_date": "2025-01-26T15:45:57+00:00" } }