diff --git a/connect/eaas/core/validation/validators/base.py b/connect/eaas/core/validation/validators/base.py index 80b6064..db7d2df 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( @@ -224,8 +225,8 @@ def validate_docker_compose_yml(context): # noqa: CCR001 ), ) continue - image = from_cmd[4:].strip() - if image != runner_image: + image = from_cmd[4:].split()[0] + 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 8db1fef..1f93701 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', @@ -566,6 +568,155 @@ 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.__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' + ) + 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_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', + 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',