Skip to content

Commit 8ced550

Browse files
authored
Merge pull request #112 from cloudblue/issue-103-allow-multistage
fix: allow multistage Dockerfiles by removing AS <stage name> in validation
2 parents 42a14e6 + c1f7aa5 commit 8ced550

2 files changed

Lines changed: 156 additions & 4 deletions

File tree

  • connect/eaas/core/validation/validators
  • tests/connect/eaas/core/validation/validators

connect/eaas/core/validation/validators/base.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,8 +210,9 @@ def validate_docker_compose_yml(context): # noqa: CCR001
210210
),
211211
)
212212
continue
213-
content = open(dockerfile, 'r').read().splitlines()
214-
from_cmd = next(filter(lambda x: x.startswith('FROM'), content), None)
213+
with open(dockerfile, 'r') as f:
214+
content = f.read().splitlines()
215+
from_cmd = next(filter(lambda x: x.startswith('FROM'), reversed(content)), None)
215216
if not from_cmd:
216217
messages.append(
217218
ValidationItem(
@@ -224,8 +225,8 @@ def validate_docker_compose_yml(context): # noqa: CCR001
224225
),
225226
)
226227
continue
227-
image = from_cmd[4:].strip()
228-
if image != runner_image:
228+
image = from_cmd[4:].split()[0]
229+
if image.lower() != runner_image.lower():
229230
messages.append(
230231
ValidationItem(
231232
level='ERROR',

tests/connect/eaas/core/validation/validators/test_base.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,8 @@ def test_validate_docker_compose_yml_invalid_image_dockerfile(mocker):
534534
return_value=True,
535535
)
536536
mocked_open = mocker.MagicMock()
537+
mocked_open.__enter__ = mocker.MagicMock(return_value=mocked_open)
538+
mocked_open.__exit__ = mocker.MagicMock(return_value=False)
537539
mocked_open.read.return_value = 'FROM cloudblueconnect/connect-extension-runner:0.3\n'
538540
mocker.patch(
539541
'connect.eaas.core.validation.validators.base.open',
@@ -566,6 +568,155 @@ def test_validate_docker_compose_yml_invalid_image_dockerfile(mocker):
566568
assert item.file == 'fake_dir/Dockerfile'
567569

568570

571+
def test_validate_docker_compose_yml_multistage_image_dockerfile(mocker):
572+
mocker.patch(
573+
'connect.eaas.core.validation.validators.base.os.path.isfile',
574+
return_value=True,
575+
)
576+
mocked_open = mocker.MagicMock()
577+
mocked_open.__enter__ = mocker.MagicMock(return_value=mocked_open)
578+
mocked_open.__exit__ = mocker.MagicMock(return_value=False)
579+
mocked_open.read.return_value = (
580+
'FROM cloudblueconnect/connect-extension-runner:0.3 as a-stage-name\n'
581+
)
582+
mocker.patch(
583+
'connect.eaas.core.validation.validators.base.open',
584+
side_effect=[None, mocked_open],
585+
)
586+
mocker.patch(
587+
'connect.eaas.core.validation.validators.base.yaml.safe_load',
588+
return_value={
589+
'services': {
590+
'dev': {
591+
'build': {'dockerfile': 'Dockerfile'},
592+
},
593+
},
594+
},
595+
)
596+
597+
result = validate_docker_compose_yml({'project_dir': 'fake_dir', 'runner_version': '0.3'})
598+
599+
assert isinstance(result, ValidationResult)
600+
assert result.must_exit is False
601+
assert len(result.items) == 0
602+
603+
604+
def test_validate_docker_compose_yml_dockerfile_case_insensitive(mocker):
605+
mocker.patch(
606+
'connect.eaas.core.validation.validators.base.os.path.isfile',
607+
return_value=True,
608+
)
609+
mocked_open = mocker.MagicMock()
610+
mocked_open.__enter__ = mocker.MagicMock(return_value=mocked_open)
611+
mocked_open.__exit__ = mocker.MagicMock(return_value=False)
612+
mocked_open.read.return_value = (
613+
'FROM CloudBlueConnect/Connect-Extension-Runner:1.0\n'
614+
)
615+
mocker.patch(
616+
'connect.eaas.core.validation.validators.base.open',
617+
side_effect=[None, mocked_open],
618+
)
619+
mocker.patch(
620+
'connect.eaas.core.validation.validators.base.yaml.safe_load',
621+
return_value={
622+
'services': {
623+
'dev': {
624+
'build': {'dockerfile': 'Dockerfile'},
625+
},
626+
},
627+
},
628+
)
629+
630+
result = validate_docker_compose_yml({'project_dir': 'fake_dir', 'runner_version': '1.0'})
631+
632+
assert isinstance(result, ValidationResult)
633+
assert result.must_exit is False
634+
assert len(result.items) == 0
635+
636+
637+
def test_validate_docker_compose_yml_multistage_runner_in_last_stage(mocker):
638+
mocker.patch(
639+
'connect.eaas.core.validation.validators.base.os.path.isfile',
640+
return_value=True,
641+
)
642+
mocked_open = mocker.MagicMock()
643+
mocked_open.__enter__ = mocker.MagicMock(return_value=mocked_open)
644+
mocked_open.__exit__ = mocker.MagicMock(return_value=False)
645+
mocked_open.read.return_value = (
646+
'FROM python:3.10 AS builder\n'
647+
'RUN pip install stuff\n'
648+
'\n'
649+
'FROM cloudblueconnect/connect-extension-runner:0.3\n'
650+
'COPY --from=builder /app /app\n'
651+
)
652+
mocker.patch(
653+
'connect.eaas.core.validation.validators.base.open',
654+
side_effect=[None, mocked_open],
655+
)
656+
mocker.patch(
657+
'connect.eaas.core.validation.validators.base.yaml.safe_load',
658+
return_value={
659+
'services': {
660+
'dev': {
661+
'build': {'dockerfile': 'Dockerfile'},
662+
},
663+
},
664+
},
665+
)
666+
667+
result = validate_docker_compose_yml({'project_dir': 'fake_dir', 'runner_version': '0.3'})
668+
669+
assert isinstance(result, ValidationResult)
670+
assert result.must_exit is False
671+
assert len(result.items) == 0
672+
673+
674+
def test_validate_docker_compose_yml_multistage_wrong_last_stage(mocker):
675+
mocker.patch(
676+
'connect.eaas.core.validation.validators.base.os.path.isfile',
677+
return_value=True,
678+
)
679+
mocked_open = mocker.MagicMock()
680+
mocked_open.__enter__ = mocker.MagicMock(return_value=mocked_open)
681+
mocked_open.__exit__ = mocker.MagicMock(return_value=False)
682+
mocked_open.read.return_value = (
683+
'FROM cloudblueconnect/connect-extension-runner:0.3 AS base\n'
684+
'RUN pip install stuff\n'
685+
'\n'
686+
'FROM python:3.10\n'
687+
'COPY --from=base /app /app\n'
688+
)
689+
mocker.patch(
690+
'connect.eaas.core.validation.validators.base.open',
691+
side_effect=[None, mocked_open],
692+
)
693+
mocker.patch(
694+
'connect.eaas.core.validation.validators.base.yaml.safe_load',
695+
return_value={
696+
'services': {
697+
'dev': {
698+
'build': {'dockerfile': 'Dockerfile'},
699+
},
700+
},
701+
},
702+
)
703+
704+
result = validate_docker_compose_yml({'project_dir': 'fake_dir', 'runner_version': '0.3'})
705+
706+
assert isinstance(result, ValidationResult)
707+
assert result.must_exit is False
708+
assert len(result.items) == 1
709+
item = result.items[0]
710+
assert isinstance(item, ValidationItem)
711+
assert item.level == 'ERROR'
712+
assert (
713+
'Invalid base image in Dockerfile of service *dev*: expected '
714+
'*cloudblueconnect/connect-extension-runner:0.3* '
715+
'got *python:3.10*.'
716+
) in item.message
717+
assert item.file == 'fake_dir/Dockerfile'
718+
719+
569720
def test_validate_docker_compose_yml_invalid_image_no_dockerfile(mocker):
570721
mocker.patch(
571722
'connect.eaas.core.validation.validators.base.os.path.isfile',

0 commit comments

Comments
 (0)