From a0d66c6ba72bab673d1939999a8d445cfb766b83 Mon Sep 17 00:00:00 2001 From: Aleksandr Komissarov Date: Tue, 28 May 2024 16:42:38 +0200 Subject: [PATCH 1/4] fix: allow multistage Dockerfiles by removing AS in validation - Modified the validate_docker_compose_yml function to support multistage Dockerfiles. - Updated the FROM line parsing to remove the AS part for case-insensitive comparison. - This ensures compatibility with Dockerfiles using multistage builds. --- .../eaas/core/validation/validators/base.py | 2 +- .../core/validation/validators/test_base.py | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/connect/eaas/core/validation/validators/base.py b/connect/eaas/core/validation/validators/base.py index 80b6064..ec18bb3 100644 --- a/connect/eaas/core/validation/validators/base.py +++ b/connect/eaas/core/validation/validators/base.py @@ -224,7 +224,7 @@ def validate_docker_compose_yml(context): # noqa: CCR001 ), ) continue - image = from_cmd[4:].strip() + image = re.sub(r'\s+AS\s+.*', '', from_cmd[4:], flags=re.IGNORECASE).strip() if image != runner_image: messages.append( ValidationItem( diff --git a/tests/connect/eaas/core/validation/validators/test_base.py b/tests/connect/eaas/core/validation/validators/test_base.py index 8db1fef..4ecb009 100644 --- a/tests/connect/eaas/core/validation/validators/test_base.py +++ b/tests/connect/eaas/core/validation/validators/test_base.py @@ -566,6 +566,37 @@ def test_validate_docker_compose_yml_invalid_image_dockerfile(mocker): assert item.file == 'fake_dir/Dockerfile' +def test_validate_docker_compose_yml_multistage_image_dockerfile(mocker): + mocker.patch( + 'connect.eaas.core.validation.validators.base.os.path.isfile', + return_value=True, + ) + mocked_open = mocker.MagicMock() + mocked_open.read.return_value = ( + 'FROM cloudblueconnect/connect-extension-runner:0.3 as a-stage-name\n' + ) + mocker.patch( + 'connect.eaas.core.validation.validators.base.open', + side_effect=[None, mocked_open], + ) + mocker.patch( + 'connect.eaas.core.validation.validators.base.yaml.safe_load', + return_value={ + 'services': { + 'dev': { + 'build': {'dockerfile': 'Dockerfile'}, + }, + }, + }, + ) + + result = validate_docker_compose_yml({'project_dir': 'fake_dir', 'runner_version': '0.3'}) + + assert isinstance(result, ValidationResult) + assert result.must_exit is False + assert len(result.items) == 0 + + def test_validate_docker_compose_yml_invalid_image_no_dockerfile(mocker): mocker.patch( 'connect.eaas.core.validation.validators.base.os.path.isfile', From 0887be323b3ff9993e6c5f6bb5b12fee9b1cb711 Mon Sep 17 00:00:00 2001 From: Arnau Giralt Date: Wed, 11 Feb 2026 22:02:38 +0100 Subject: [PATCH 2/4] fix: use context manager for Dockerfile open in validation Prevent file handle leak by wrapping the Dockerfile open() call in a with statement. Update test mocks to support context manager protocol. --- .../eaas/core/validation/validators/base.py | 5 +- .../core/validation/validators/test_base.py | 87 +++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/connect/eaas/core/validation/validators/base.py b/connect/eaas/core/validation/validators/base.py index ec18bb3..977230e 100644 --- a/connect/eaas/core/validation/validators/base.py +++ b/connect/eaas/core/validation/validators/base.py @@ -210,8 +210,9 @@ def validate_docker_compose_yml(context): # noqa: CCR001 ), ) continue - content = open(dockerfile, 'r').read().splitlines() - from_cmd = next(filter(lambda x: x.startswith('FROM'), content), None) + with open(dockerfile, 'r') as f: + content = f.read().splitlines() + from_cmd = next(filter(lambda x: x.startswith('FROM'), reversed(content)), None) if not from_cmd: messages.append( ValidationItem( diff --git a/tests/connect/eaas/core/validation/validators/test_base.py b/tests/connect/eaas/core/validation/validators/test_base.py index 4ecb009..cbbb8f4 100644 --- a/tests/connect/eaas/core/validation/validators/test_base.py +++ b/tests/connect/eaas/core/validation/validators/test_base.py @@ -534,6 +534,8 @@ def test_validate_docker_compose_yml_invalid_image_dockerfile(mocker): return_value=True, ) mocked_open = mocker.MagicMock() + mocked_open.__enter__ = mocker.MagicMock(return_value=mocked_open) + mocked_open.__exit__ = mocker.MagicMock(return_value=False) mocked_open.read.return_value = 'FROM cloudblueconnect/connect-extension-runner:0.3\n' mocker.patch( 'connect.eaas.core.validation.validators.base.open', @@ -572,6 +574,8 @@ def test_validate_docker_compose_yml_multistage_image_dockerfile(mocker): return_value=True, ) mocked_open = mocker.MagicMock() + mocked_open.__enter__ = mocker.MagicMock(return_value=mocked_open) + mocked_open.__exit__ = mocker.MagicMock(return_value=False) mocked_open.read.return_value = ( 'FROM cloudblueconnect/connect-extension-runner:0.3 as a-stage-name\n' ) @@ -597,6 +601,89 @@ def test_validate_docker_compose_yml_multistage_image_dockerfile(mocker): assert len(result.items) == 0 +def test_validate_docker_compose_yml_multistage_runner_in_last_stage(mocker): + mocker.patch( + 'connect.eaas.core.validation.validators.base.os.path.isfile', + return_value=True, + ) + mocked_open = mocker.MagicMock() + mocked_open.__enter__ = mocker.MagicMock(return_value=mocked_open) + mocked_open.__exit__ = mocker.MagicMock(return_value=False) + mocked_open.read.return_value = ( + 'FROM python:3.10 AS builder\n' + 'RUN pip install stuff\n' + '\n' + 'FROM cloudblueconnect/connect-extension-runner:0.3\n' + 'COPY --from=builder /app /app\n' + ) + mocker.patch( + 'connect.eaas.core.validation.validators.base.open', + side_effect=[None, mocked_open], + ) + mocker.patch( + 'connect.eaas.core.validation.validators.base.yaml.safe_load', + return_value={ + 'services': { + 'dev': { + 'build': {'dockerfile': 'Dockerfile'}, + }, + }, + }, + ) + + result = validate_docker_compose_yml({'project_dir': 'fake_dir', 'runner_version': '0.3'}) + + assert isinstance(result, ValidationResult) + assert result.must_exit is False + assert len(result.items) == 0 + + +def test_validate_docker_compose_yml_multistage_wrong_last_stage(mocker): + mocker.patch( + 'connect.eaas.core.validation.validators.base.os.path.isfile', + return_value=True, + ) + mocked_open = mocker.MagicMock() + mocked_open.__enter__ = mocker.MagicMock(return_value=mocked_open) + mocked_open.__exit__ = mocker.MagicMock(return_value=False) + mocked_open.read.return_value = ( + 'FROM cloudblueconnect/connect-extension-runner:0.3 AS base\n' + 'RUN pip install stuff\n' + '\n' + 'FROM python:3.10\n' + 'COPY --from=base /app /app\n' + ) + mocker.patch( + 'connect.eaas.core.validation.validators.base.open', + side_effect=[None, mocked_open], + ) + mocker.patch( + 'connect.eaas.core.validation.validators.base.yaml.safe_load', + return_value={ + 'services': { + 'dev': { + 'build': {'dockerfile': 'Dockerfile'}, + }, + }, + }, + ) + + result = validate_docker_compose_yml({'project_dir': 'fake_dir', 'runner_version': '0.3'}) + + assert isinstance(result, ValidationResult) + assert result.must_exit is False + assert len(result.items) == 1 + item = result.items[0] + assert isinstance(item, ValidationItem) + assert item.level == 'ERROR' + assert ( + 'Invalid base image in Dockerfile of service *dev*: expected ' + '*cloudblueconnect/connect-extension-runner:0.3* ' + 'got *python:3.10*.' + ) in item.message + assert item.file == 'fake_dir/Dockerfile' + + def test_validate_docker_compose_yml_invalid_image_no_dockerfile(mocker): mocker.patch( 'connect.eaas.core.validation.validators.base.os.path.isfile', From d76d5833a4c491b3d1052b392aadb4fda9fec387 Mon Sep 17 00:00:00 2001 From: Arnau Giralt Date: Wed, 11 Feb 2026 22:19:21 +0100 Subject: [PATCH 3/4] fix: replace regex with str.split to avoid ReDoS in Dockerfile validation --- connect/eaas/core/validation/validators/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connect/eaas/core/validation/validators/base.py b/connect/eaas/core/validation/validators/base.py index 977230e..7ca6881 100644 --- a/connect/eaas/core/validation/validators/base.py +++ b/connect/eaas/core/validation/validators/base.py @@ -225,7 +225,7 @@ def validate_docker_compose_yml(context): # noqa: CCR001 ), ) continue - image = re.sub(r'\s+AS\s+.*', '', from_cmd[4:], flags=re.IGNORECASE).strip() + image = from_cmd[4:].split()[0] if image != runner_image: messages.append( ValidationItem( From c1f7aa5133c43973f7a13a8d234f9b2c51cde135 Mon Sep 17 00:00:00 2001 From: Arnau Giralt Date: Fri, 27 Feb 2026 15:28:50 +0100 Subject: [PATCH 4/4] fix: use case-insensitive comparison for Docker image validation Docker image names are case-insensitive, so the base image check in Dockerfile validation should not fail on casing differences. Co-Authored-By: Claude Opus 4.6 --- .../eaas/core/validation/validators/base.py | 2 +- .../core/validation/validators/test_base.py | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/connect/eaas/core/validation/validators/base.py b/connect/eaas/core/validation/validators/base.py index 7ca6881..db7d2df 100644 --- a/connect/eaas/core/validation/validators/base.py +++ b/connect/eaas/core/validation/validators/base.py @@ -226,7 +226,7 @@ def validate_docker_compose_yml(context): # noqa: CCR001 ) continue image = from_cmd[4:].split()[0] - if image != runner_image: + if image.lower() != runner_image.lower(): messages.append( ValidationItem( level='ERROR', diff --git a/tests/connect/eaas/core/validation/validators/test_base.py b/tests/connect/eaas/core/validation/validators/test_base.py index cbbb8f4..1f93701 100644 --- a/tests/connect/eaas/core/validation/validators/test_base.py +++ b/tests/connect/eaas/core/validation/validators/test_base.py @@ -601,6 +601,39 @@ def test_validate_docker_compose_yml_multistage_image_dockerfile(mocker): assert len(result.items) == 0 +def test_validate_docker_compose_yml_dockerfile_case_insensitive(mocker): + mocker.patch( + 'connect.eaas.core.validation.validators.base.os.path.isfile', + return_value=True, + ) + mocked_open = mocker.MagicMock() + mocked_open.__enter__ = mocker.MagicMock(return_value=mocked_open) + mocked_open.__exit__ = mocker.MagicMock(return_value=False) + mocked_open.read.return_value = ( + 'FROM CloudBlueConnect/Connect-Extension-Runner:1.0\n' + ) + mocker.patch( + 'connect.eaas.core.validation.validators.base.open', + side_effect=[None, mocked_open], + ) + mocker.patch( + 'connect.eaas.core.validation.validators.base.yaml.safe_load', + return_value={ + 'services': { + 'dev': { + 'build': {'dockerfile': 'Dockerfile'}, + }, + }, + }, + ) + + result = validate_docker_compose_yml({'project_dir': 'fake_dir', 'runner_version': '1.0'}) + + assert isinstance(result, ValidationResult) + assert result.must_exit is False + assert len(result.items) == 0 + + def test_validate_docker_compose_yml_multistage_runner_in_last_stage(mocker): mocker.patch( 'connect.eaas.core.validation.validators.base.os.path.isfile',