diff --git a/src/ploigos_step_runner/step_implementers/create_container_image/__init__.py b/src/ploigos_step_runner/step_implementers/create_container_image/__init__.py index bd9a4515f..b3a18b169 100644 --- a/src/ploigos_step_runner/step_implementers/create_container_image/__init__.py +++ b/src/ploigos_step_runner/step_implementers/create_container_image/__init__.py @@ -5,3 +5,5 @@ Buildah from ploigos_step_runner.step_implementers.create_container_image.maven_jkube_k8sbuild import \ MavenJKubeK8sBuild +from ploigos_step_runner.step_implementers.create_container_image.source_to_image import \ + SourceToImage diff --git a/src/ploigos_step_runner/step_implementers/create_container_image/source_to_image.py b/src/ploigos_step_runner/step_implementers/create_container_image/source_to_image.py new file mode 100644 index 000000000..d8f719961 --- /dev/null +++ b/src/ploigos_step_runner/step_implementers/create_container_image/source_to_image.py @@ -0,0 +1,231 @@ +"""`StepImplementer` for the `create-container-image step` using Source-to-Image (s2i) +to create a Containerfile that can be built by another `StepImplementer`, such as Buildah. + +Step Configuration +------------------ +Step configuration expected as input to this step. +Could come from: + + * static configuration + * runtime configuration + * previous step results + +Configuration Key | Required? | Default | Description +------------------------------|-----------|---------|----------- +`s2i-builder-image` | True | | Container image tag to use as the s2i builder image. +`s2i-image-scripts-url` | False | | Location of the s2i scripts in the given `s2i-builder-image`. \ + If not given s2i will assume they are in `image:///usr/libexec/s2i`. \ +
\ + EX:
\ + s2i-builder-image: registry.redhat.io/redhat-openjdk-18/openjdk18-openshift
\ + s2i-image-scripts-url: `image:///usr/local/s2i` +`s2i-loglevel` | False | 1 | s2i log level to specify. See s2i docs. +`s2i-additional-arguments` | False | `[]` | List of additional arguments to append to s2i call. +`context` | True | `'.'` | Context to build the container image in +`tls-verify` | True | `True` | Whether to verify TLS when pulling parent images +`containers-config-auth-file` | False | | Path to the container registry authentication \ + file to use for container registry authentication. \ + If one is not provided one will be created in the \ + working directory. +`container-registries` | False | | Hash of container registries to authenticate with. + + +Result Artifacts +---------------- +Results artifacts output by this step. + +Result Artifact Key | Description +----------------------------------------|------------ +`container-image-registry-uri` | Registry URI poriton of the container image tag of the built container image. +`container-image-registry-organization` | Organization portion of the container image tag of the built container image. +`container-image-repository` | Repository portion of the container image tag of the built container image. +`container-image-name` | Another way to reference the repository portion of the container image tag \ + of the built container image. +`container-image-version` | Version portion of the container image tag of the built container image. +`container-image-tag` | Full container image tag of the built container, including the registry URI.
\ + Takes the form of: \ + `container-image-registry-organization/container-image-repository:container-image-version` +`container-image-short-tag` | Short container image tag of the built container image, excluding the registry URI.
\ + Takes the form of: \ + `container-image-registry-uri/container-image-registry-organization/container-image-repository:container-image-version` + +"""# pylint: disable=line-too-long + +import os +import sys +from distutils import util + +import sh +from ploigos_step_runner import StepImplementer, StepResult +from ploigos_step_runner.utils.containers import (container_registries_login, + inspect_container_image) + +DEFAULT_CONFIG = { + 'context': '.', + 'tls-verify': True, + 's2i-additional-arguments': [], + 's2i-loglevel': 1 +} + +REQUIRED_CONFIG_OR_PREVIOUS_STEP_RESULT_ARTIFACT_KEYS = [ + 'context', + 'tls-verify', + 's2i-builder-image', + 'organization', + 'service-name', + 'application-name' +] + +class SourceToImage(StepImplementer): + """`StepImplementer` for the `create-container-image step` using Source-to-Image (s2i) + to create a Containerfile that can be built by another `StepImplementer`, such as Buildah. + """ + + CONTAINER_LABEL_SCRIPTS_URL = 'io.openshift.s2i.scripts-url' + + @staticmethod + def step_implementer_config_defaults(): + """Getter for the StepImplementer's configuration defaults. + + Notes + ----- + These are the lowest precedence configuration values. + + Returns + ------- + dict + Default values to use for step configuration values. + """ + return DEFAULT_CONFIG + + @staticmethod + def _required_config_or_result_keys(): + """Getter for step configuration or previous step result artifacts that are required before + running this step. + + See Also + -------- + _validate_required_config_or_previous_step_result_artifact_keys + + Returns + ------- + array_list + Array of configuration keys or previous step result artifacts + that are required before running the step. + """ + return REQUIRED_CONFIG_OR_PREVIOUS_STEP_RESULT_ARTIFACT_KEYS + + def _run_step(self): + """Runs the step implemented by this StepImplementer. + + Returns + ------- + StepResult + Object containing the dictionary results of this step. + """ + step_result = StepResult.from_step_implementer(self) + + # get values + builder_image = self.get_value('s2i-builder-image') + + # determine tls flag + tls_verify = self.get_value('tls-verify') + if isinstance(tls_verify, str): + tls_verify = bool(util.strtobool(tls_verify)) + if tls_verify: + s2i_tls_flags = ['--tlsverify'] + else: + s2i_tls_flags = [] + + # determine the generated imagespec file + s2i_working_dir = self.create_working_dir_sub_dir('s2i-context') + imagespecfile = self.write_working_file( + os.path.join(s2i_working_dir, 'Containerfile.s2i-gen') + ) + + # determine image scripts url flags + # use user provided url if given, + # else try and inspect from builder image + s2i_image_scripts_url = self.get_value('s2i-image-scripts-url') + if not s2i_image_scripts_url: + print('Attempt to inspect builder image for label for image scripts url') + + # attempt to auth with container image registries + # login to any provider container registries + # NOTE: important to specify the auth file because depending on the context this is + # being run in python process may not have permissions to default location + containers_config_auth_file = self.get_value('containers-config-auth-file') + if not containers_config_auth_file: + containers_config_auth_file = os.path.join( + self.work_dir_path, + 'container-auth.json' + ) + try: + container_registries_login( + registries=self.get_value('container-registries'), + containers_config_auth_file=containers_config_auth_file, + containers_config_tls_verify=tls_verify + ) + except RuntimeError as error: + step_result.message += "WARNING: error authenticating with" \ + " container image registries to be able to pull s2i builder image" \ + f" to inspect for image scripts url: {error}\n" + + # if not given, attempt to get from builder image labels + try: + container_image_details = inspect_container_image( + container_image_uri=builder_image, + containers_config_auth_file=containers_config_auth_file + ) + + s2i_image_scripts_url = container_image_details['OCIv1']['config']['Labels']\ + [SourceToImage.CONTAINER_LABEL_SCRIPTS_URL] + except RuntimeError as error: + step_result.message += "WARNING: failed to inspect s2i builder image" \ + f" ({builder_image}) to dynamically determine image scripts url." \ + f" S2I default will be used: {error}\n" + except KeyError as error: + step_result.message += "WARNING: failed to find s2i scripts url label" \ + f" ({SourceToImage.CONTAINER_LABEL_SCRIPTS_URL}) on s2i builder image" \ + f" ({builder_image}) to dynamically determine image scripts url." \ + f" S2I default will be used: Could not find key ({error}).\n" + + # if determined image scripts url set the flag + # else s2i will use its default (image:///usr/libexec/s2i) + if s2i_image_scripts_url: + s2i_image_scripts_url_flags = ['--image-scripts-url', s2i_image_scripts_url] + else: + s2i_image_scripts_url_flags = [] + + try: + # perform build + print('Use s2i to generate imagespecfile and accompanying resources') + sh.s2i.build( # pylint: disable=no-member + self.get_value('context'), + builder_image, + '--loglevel', self.get_value('s2i-loglevel'), + *s2i_tls_flags, + '--as-dockerfile', imagespecfile, + *s2i_image_scripts_url_flags, + *self.get_value('s2i-additional-arguments'), + _out=sys.stdout, + _err=sys.stderr, + _tee='err' + ) + except sh.ErrorReturnCode as error: # pylint: disable=undefined-variable + step_result.success = False + step_result.message += f'Issue invoking s2i build: {error}' + + # add artifacts + step_result.add_artifact( + name='imagespecfile', + value=imagespecfile, + description='File defining the container image to build generated by s2i' + ) + step_result.add_artifact( + name='context', + value=s2i_working_dir, + description='Context to use when building the imagespecfile generated by S2I.' + ) + + return step_result diff --git a/src/ploigos_step_runner/utils/containers.py b/src/ploigos_step_runner/utils/containers.py index e42560d49..10e15378e 100644 --- a/src/ploigos_step_runner/utils/containers.py +++ b/src/ploigos_step_runner/utils/containers.py @@ -1,6 +1,7 @@ """Shared utils for dealing with containers. """ +import json import sys from io import StringIO @@ -415,3 +416,63 @@ def determine_container_image_build_tag_info( build_full_tag = f"{image_registry_uri}/{build_short_tag}" return build_full_tag, build_short_tag, image_registry_uri, image_repository, image_version + +def inspect_container_image( + container_image_uri, + containers_config_auth_file=None +): + """Inspects a given container image for all its details. Useful for getting image labels + and such. + + Parameters + ---------- + container_image_uri : str + URI to the container image to inspect + containers_config_auth_file : str + Path to container image registries authentication file. + + Raises + ------ + RuntimeError + If issue running `buildah inspect` + + Returns + ------- + dict + Container image details from `buildah inspect` + """ + buildah_inspect = None + + # determine auth flags + if containers_config_auth_file: + buildah_authfile_flags = ['--authfile', containers_config_auth_file] + else: + buildah_authfile_flags = [] + + # pull container image (can't inspect remote image) + try: + sh.buildah.pull( # pylint: disable=no-member + *buildah_authfile_flags, + container_image_uri + ) + except sh.ErrorReturnCode as error: # pylint: disable=undefined-variable + raise RuntimeError( + f"Error pulling container image ({container_image_uri}) for inspection: {error}" + ) from error + + # get container image information + try: + + buildah_inspect_out_buff = StringIO() + sh.buildah.inspect( # pylint: disable=no-member + container_image_uri, + _out=buildah_inspect_out_buff + ) + buildah_inspect_out = buildah_inspect_out_buff.getvalue().rstrip() + buildah_inspect = json.loads(buildah_inspect_out) + except sh.ErrorReturnCode as error: # pylint: disable=undefined-variable + raise RuntimeError( + f"Error inspecting container image ({container_image_uri}): {error}" + ) from error + + return buildah_inspect diff --git a/tests/step_implementers/create_container_image/test_source_to_image.py b/tests/step_implementers/create_container_image/test_source_to_image.py new file mode 100644 index 000000000..82281bc8f --- /dev/null +++ b/tests/step_implementers/create_container_image/test_source_to_image.py @@ -0,0 +1,657 @@ +from unittest.mock import ANY, patch + +import sh +from ploigos_step_runner import StepResult +from ploigos_step_runner.step_implementers.create_container_image import \ + SourceToImage +from tests.helpers.base_step_implementer_test_case import \ + BaseStepImplementerTestCase + + +class TestStepImplementerSourceToImage_step_implementer_config_defaults( + BaseStepImplementerTestCase +): + def test_result(self): + self.assertEqual( + SourceToImage.step_implementer_config_defaults(), + { + 'context': '.', + 'tls-verify': True, + 's2i-additional-arguments': [], + 's2i-loglevel': 1 + } + ) + +class TestStepImplementerSourceToImage__required_config_or_result_keys( + BaseStepImplementerTestCase +): + def test_result(self): + self.assertEqual( + SourceToImage._required_config_or_result_keys(), + [ + 'context', + 'tls-verify', + 's2i-builder-image', + 'organization', + 'service-name', + 'application-name' + ] + ) + +@patch('sh.s2i', create=True) +@patch('ploigos_step_runner.step_implementers.create_container_image.source_to_image.inspect_container_image') +@patch('ploigos_step_runner.step_implementers.create_container_image.source_to_image.container_registries_login') +@patch.object( + SourceToImage, + 'write_working_file', + return_value='/mock/working/s2i-context/Containerfile.s2i-gen' +) +@patch.object( + SourceToImage, + 'create_working_dir_sub_dir', + return_value='/mock/working/s2i-context' +) +class TestStepImplementerSourceToImage__run_step( + BaseStepImplementerTestCase +): + def create_step_implementer( + self, + step_config={}, + workflow_result=None, + parent_work_dir_path='' + ): + return self.create_given_step_implementer( + step_implementer=SourceToImage, + step_config=step_config, + step_name='create-container-image', + implementer='SourceToImage', + workflow_result=workflow_result, + parent_work_dir_path=parent_work_dir_path + ) + + def test_success_builder_image_with_scripts_url_label( + self, + mock_create_working_dir_sub_dir, + mock_write_working_file, + mock_container_registries_login, + mock_inspect_container_image, + mock_s2i + ): + # setup + step_config = { + 's2i-builder-image': 'mock.io/awesome-image:v42' + } + step_implementer = self.create_step_implementer( + step_config=step_config + ) + + # setup mocks + mock_inspect_container_image.return_value = { + 'OCIv1': { + 'config': { + 'Labels': { + 'io.openshift.s2i.scripts-url': 'image:///moock-image-label/s2i' + } + } + } + } + + # run test + actual_step_result= step_implementer._run_step() + + # validate + expected_step_result = StepResult( + step_name='create-container-image', + sub_step_name='SourceToImage', + sub_step_implementer_name='SourceToImage' + ) + expected_step_result.add_artifact( + name='imagespecfile', + value='/mock/working/s2i-context/Containerfile.s2i-gen', + description='File defining the container image to build generated by s2i' + ) + expected_step_result.add_artifact( + name='context', + value='/mock/working/s2i-context', + description='Context to use when building the imagespecfile generated by S2I.' + ) + self.assertEqual(actual_step_result, expected_step_result) + + mock_inspect_container_image.assert_called_once_with( + container_image_uri='mock.io/awesome-image:v42', + containers_config_auth_file='create-container-image/container-auth.json' + ) + mock_container_registries_login.assert_called_once_with( + registries=None, + containers_config_auth_file='create-container-image/container-auth.json', + containers_config_tls_verify=True + ) + mock_s2i.build.assert_called_once_with( + '.', + 'mock.io/awesome-image:v42', + '--loglevel', 1, + '--tlsverify', + '--as-dockerfile', '/mock/working/s2i-context/Containerfile.s2i-gen', + '--image-scripts-url', 'image:///moock-image-label/s2i', + _out=ANY, + _err=ANY, + _tee='err' + ) + + def test_success_given_containers_config_auth_file( + self, + mock_create_working_dir_sub_dir, + mock_write_working_file, + mock_container_registries_login, + mock_inspect_container_image, + mock_s2i + ): + # setup + step_config = { + 's2i-builder-image': 'mock.io/awesome-image:v42', + 'containers-config-auth-file': '/mock/mock-regs-auths.json' + } + step_implementer = self.create_step_implementer( + step_config=step_config + ) + + # setup mocks + mock_inspect_container_image.return_value = { + 'OCIv1': { + 'config': { + 'Labels': { + 'io.openshift.s2i.scripts-url': 'image:///moock-image-label/s2i' + } + } + } + } + + # run test + actual_step_result= step_implementer._run_step() + + # validate + expected_step_result = StepResult( + step_name='create-container-image', + sub_step_name='SourceToImage', + sub_step_implementer_name='SourceToImage' + ) + expected_step_result.add_artifact( + name='imagespecfile', + value='/mock/working/s2i-context/Containerfile.s2i-gen', + description='File defining the container image to build generated by s2i' + ) + expected_step_result.add_artifact( + name='context', + value='/mock/working/s2i-context', + description='Context to use when building the imagespecfile generated by S2I.' + ) + self.assertEqual(actual_step_result, expected_step_result) + + mock_inspect_container_image.assert_called_once_with( + container_image_uri='mock.io/awesome-image:v42', + containers_config_auth_file='/mock/mock-regs-auths.json' + ) + mock_container_registries_login.assert_called_once_with( + registries=None, + containers_config_auth_file='/mock/mock-regs-auths.json', + containers_config_tls_verify=True + ) + mock_s2i.build.assert_called_once_with( + '.', + 'mock.io/awesome-image:v42', + '--loglevel', 1, + '--tlsverify', + '--as-dockerfile', '/mock/working/s2i-context/Containerfile.s2i-gen', + '--image-scripts-url', 'image:///moock-image-label/s2i', + _out=ANY, + _err=ANY, + _tee='err' + ) + + def test_success_error_inspecting_builder_image_to_get_scripts_url( + self, + mock_create_working_dir_sub_dir, + mock_write_working_file, + mock_container_registries_login, + mock_inspect_container_image, + mock_s2i + ): + # setup + step_config = { + 's2i-builder-image': 'mock.io/awesome-image:v42' + } + step_implementer = self.create_step_implementer( + step_config=step_config + ) + + # setup mocks + mock_inspect_container_image.side_effect = RuntimeError('mock buildah inspect error') + + # run test + actual_step_result= step_implementer._run_step() + + # validate + expected_step_result = StepResult( + step_name='create-container-image', + sub_step_name='SourceToImage', + sub_step_implementer_name='SourceToImage' + ) + expected_step_result.message = "WARNING: failed to inspect s2i builder image" \ + " (mock.io/awesome-image:v42) to dynamically determine image scripts url." \ + " S2I default will be used: mock buildah inspect error\n" + expected_step_result.add_artifact( + name='imagespecfile', + value='/mock/working/s2i-context/Containerfile.s2i-gen', + description='File defining the container image to build generated by s2i' + ) + expected_step_result.add_artifact( + name='context', + value='/mock/working/s2i-context', + description='Context to use when building the imagespecfile generated by S2I.' + ) + self.assertEqual(actual_step_result, expected_step_result) + + mock_inspect_container_image.assert_called_once_with( + container_image_uri='mock.io/awesome-image:v42', + containers_config_auth_file='create-container-image/container-auth.json' + ) + mock_s2i.build.assert_called_once_with( + '.', + 'mock.io/awesome-image:v42', + '--loglevel', 1, + '--tlsverify', + '--as-dockerfile', '/mock/working/s2i-context/Containerfile.s2i-gen', + _out=ANY, + _err=ANY, + _tee='err' + ) + + def test_success_error_finding_label_on_build_image_inspection_details( + self, + mock_create_working_dir_sub_dir, + mock_write_working_file, + mock_container_registries_login, + mock_inspect_container_image, + mock_s2i + ): + # setup + step_config = { + 's2i-builder-image': 'mock.io/awesome-image:v42' + } + step_implementer = self.create_step_implementer( + step_config=step_config + ) + + # setup mocks + mock_inspect_container_image.return_value = {} + + # run test + actual_step_result= step_implementer._run_step() + + # validate + expected_step_result = StepResult( + step_name='create-container-image', + sub_step_name='SourceToImage', + sub_step_implementer_name='SourceToImage' + ) + expected_step_result.message = "WARNING: failed to find s2i scripts url label" \ + " (io.openshift.s2i.scripts-url) on s2i builder image" \ + " (mock.io/awesome-image:v42) to dynamically determine image scripts url." \ + " S2I default will be used: Could not find key ('OCIv1').\n" + expected_step_result.add_artifact( + name='imagespecfile', + value='/mock/working/s2i-context/Containerfile.s2i-gen', + description='File defining the container image to build generated by s2i' + ) + expected_step_result.add_artifact( + name='context', + value='/mock/working/s2i-context', + description='Context to use when building the imagespecfile generated by S2I.' + ) + self.assertEqual(actual_step_result, expected_step_result) + + mock_inspect_container_image.assert_called_once_with( + container_image_uri='mock.io/awesome-image:v42', + containers_config_auth_file='create-container-image/container-auth.json' + ) + mock_s2i.build.assert_called_once_with( + '.', + 'mock.io/awesome-image:v42', + '--loglevel', 1, + '--tlsverify', + '--as-dockerfile', '/mock/working/s2i-context/Containerfile.s2i-gen', + _out=ANY, + _err=ANY, + _tee='err' + ) + + def test_success_no_tls_verify_bool( + self, + mock_create_working_dir_sub_dir, + mock_write_working_file, + mock_container_registries_login, + mock_inspect_container_image, + mock_s2i + ): + # setup + step_config = { + 's2i-builder-image': 'mock.io/awesome-image:v42', + 'tls-verify': False + } + step_implementer = self.create_step_implementer( + step_config=step_config + ) + + # setup mocks + mock_inspect_container_image.return_value = { + 'OCIv1': { + 'config': { + 'Labels': { + 'io.openshift.s2i.scripts-url': 'image:///moock-image-label/s2i' + } + } + } + } + + # run test + actual_step_result= step_implementer._run_step() + + # validate + expected_step_result = StepResult( + step_name='create-container-image', + sub_step_name='SourceToImage', + sub_step_implementer_name='SourceToImage' + ) + expected_step_result.add_artifact( + name='imagespecfile', + value='/mock/working/s2i-context/Containerfile.s2i-gen', + description='File defining the container image to build generated by s2i' + ) + expected_step_result.add_artifact( + name='context', + value='/mock/working/s2i-context', + description='Context to use when building the imagespecfile generated by S2I.' + ) + self.assertEqual(actual_step_result, expected_step_result) + + mock_inspect_container_image.assert_called_once_with( + container_image_uri='mock.io/awesome-image:v42', + containers_config_auth_file='create-container-image/container-auth.json' + ) + mock_s2i.build.assert_called_once_with( + '.', + 'mock.io/awesome-image:v42', + '--loglevel', 1, + '--as-dockerfile', '/mock/working/s2i-context/Containerfile.s2i-gen', + '--image-scripts-url', 'image:///moock-image-label/s2i', + _out=ANY, + _err=ANY, + _tee='err' + ) + + def test_success_no_tls_verify_str( + self, + mock_create_working_dir_sub_dir, + mock_write_working_file, + mock_container_registries_login, + mock_inspect_container_image, + mock_s2i + ): + # setup + step_config = { + 's2i-builder-image': 'mock.io/awesome-image:v42', + 'tls-verify': 'false' + } + step_implementer = self.create_step_implementer( + step_config=step_config + ) + + # setup mocks + mock_inspect_container_image.return_value = { + 'OCIv1': { + 'config': { + 'Labels': { + 'io.openshift.s2i.scripts-url': 'image:///moock-image-label/s2i' + } + } + } + } + + # run test + actual_step_result= step_implementer._run_step() + + # validate + expected_step_result = StepResult( + step_name='create-container-image', + sub_step_name='SourceToImage', + sub_step_implementer_name='SourceToImage' + ) + expected_step_result.add_artifact( + name='imagespecfile', + value='/mock/working/s2i-context/Containerfile.s2i-gen', + description='File defining the container image to build generated by s2i' + ) + expected_step_result.add_artifact( + name='context', + value='/mock/working/s2i-context', + description='Context to use when building the imagespecfile generated by S2I.' + ) + self.assertEqual(actual_step_result, expected_step_result) + + mock_inspect_container_image.assert_called_once_with( + container_image_uri='mock.io/awesome-image:v42', + containers_config_auth_file='create-container-image/container-auth.json' + ) + mock_s2i.build.assert_called_once_with( + '.', + 'mock.io/awesome-image:v42', + '--loglevel', 1, + '--as-dockerfile', '/mock/working/s2i-context/Containerfile.s2i-gen', + '--image-scripts-url', 'image:///moock-image-label/s2i', + _out=ANY, + _err=ANY, + _tee='err' + ) + + def test_success_given_image_scripts_url( + self, + mock_create_working_dir_sub_dir, + mock_write_working_file, + mock_container_registries_login, + mock_inspect_container_image, + mock_s2i + ): + # setup + step_config = { + 's2i-builder-image': 'mock.io/awesome-image:v42', + 's2i-image-scripts-url': 'image:///mock-user-given/s2i' + } + step_implementer = self.create_step_implementer( + step_config=step_config + ) + + # setup mocks + mock_inspect_container_image.return_value = { + 'OCIv1': { + 'config': { + 'Labels': { + 'io.openshift.s2i.scripts-url': 'image:///moock/s2i' + } + } + } + } + + # run test + actual_step_result= step_implementer._run_step() + + # validate + expected_step_result = StepResult( + step_name='create-container-image', + sub_step_name='SourceToImage', + sub_step_implementer_name='SourceToImage' + ) + expected_step_result.add_artifact( + name='imagespecfile', + value='/mock/working/s2i-context/Containerfile.s2i-gen', + description='File defining the container image to build generated by s2i' + ) + expected_step_result.add_artifact( + name='context', + value='/mock/working/s2i-context', + description='Context to use when building the imagespecfile generated by S2I.' + ) + self.assertEqual(actual_step_result, expected_step_result) + + mock_inspect_container_image.assert_not_called() + mock_s2i.build.assert_called_once_with( + '.', + 'mock.io/awesome-image:v42', + '--loglevel', 1, + '--tlsverify', + '--as-dockerfile', '/mock/working/s2i-context/Containerfile.s2i-gen', + '--image-scripts-url', 'image:///mock-user-given/s2i', + _out=ANY, + _err=ANY, + _tee='err' + ) + + def test_fail_builder_image_with_scripts_url_label( + self, + mock_create_working_dir_sub_dir, + mock_write_working_file, + mock_container_registries_login, + mock_inspect_container_image, + mock_s2i + ): + # setup + step_config = { + 's2i-builder-image': 'mock.io/awesome-image:v42' + } + step_implementer = self.create_step_implementer( + step_config=step_config + ) + + # setup mocks + mock_s2i.build.side_effect = sh.ErrorReturnCode('s2i', b'mock out', b'mock err') + mock_inspect_container_image.return_value = { + 'OCIv1': { + 'config': { + 'Labels': { + 'io.openshift.s2i.scripts-url': 'image:///moock-image-label/s2i' + } + } + } + } + + # run test + actual_step_result= step_implementer._run_step() + + # validate + expected_step_result = StepResult( + step_name='create-container-image', + sub_step_name='SourceToImage', + sub_step_implementer_name='SourceToImage' + ) + expected_step_result.success = False + expected_step_result.message = "Issue invoking s2i build:" \ + " \n\n RAN: s2i\n\n STDOUT:\nmock out\n\n STDERR:\nmock err" + expected_step_result.add_artifact( + name='imagespecfile', + value='/mock/working/s2i-context/Containerfile.s2i-gen', + description='File defining the container image to build generated by s2i' + ) + expected_step_result.add_artifact( + name='context', + value='/mock/working/s2i-context', + description='Context to use when building the imagespecfile generated by S2I.' + ) + self.assertEqual(actual_step_result, expected_step_result) + + mock_inspect_container_image.assert_called_once_with( + container_image_uri='mock.io/awesome-image:v42', + containers_config_auth_file='create-container-image/container-auth.json' + ) + mock_s2i.build.assert_called_once_with( + '.', + 'mock.io/awesome-image:v42', + '--loglevel', 1, + '--tlsverify', + '--as-dockerfile', '/mock/working/s2i-context/Containerfile.s2i-gen', + '--image-scripts-url', 'image:///moock-image-label/s2i', + _out=ANY, + _err=ANY, + _tee='err' + ) + + def test_success_error_container_registries_login( + self, + mock_create_working_dir_sub_dir, + mock_write_working_file, + mock_container_registries_login, + mock_inspect_container_image, + mock_s2i + ): + # setup + step_config = { + 's2i-builder-image': 'mock.io/awesome-image:v42' + } + step_implementer = self.create_step_implementer( + step_config=step_config + ) + + # setup mocks + mock_inspect_container_image.return_value = { + 'OCIv1': { + 'config': { + 'Labels': { + 'io.openshift.s2i.scripts-url': 'image:///moock-image-label/s2i' + } + } + } + } + mock_container_registries_login.side_effect = RuntimeError('mock error loging in') + + # run test + actual_step_result= step_implementer._run_step() + + # validate + expected_step_result = StepResult( + step_name='create-container-image', + sub_step_name='SourceToImage', + sub_step_implementer_name='SourceToImage' + ) + expected_step_result.message = "WARNING: error authenticating with" \ + " container image registries to be able to pull s2i builder image" \ + " to inspect for image scripts url: mock error loging in\n" + expected_step_result.add_artifact( + name='imagespecfile', + value='/mock/working/s2i-context/Containerfile.s2i-gen', + description='File defining the container image to build generated by s2i' + ) + expected_step_result.add_artifact( + name='context', + value='/mock/working/s2i-context', + description='Context to use when building the imagespecfile generated by S2I.' + ) + self.assertEqual(actual_step_result, expected_step_result) + + mock_inspect_container_image.assert_called_once_with( + container_image_uri='mock.io/awesome-image:v42', + containers_config_auth_file='create-container-image/container-auth.json' + ) + mock_container_registries_login.assert_called_once_with( + registries=None, + containers_config_auth_file='create-container-image/container-auth.json', + containers_config_tls_verify=True + ) + mock_s2i.build.assert_called_once_with( + '.', + 'mock.io/awesome-image:v42', + '--loglevel', 1, + '--tlsverify', + '--as-dockerfile', '/mock/working/s2i-context/Containerfile.s2i-gen', + '--image-scripts-url', 'image:///moock-image-label/s2i', + _out=ANY, + _err=ANY, + _tee='err' + ) \ No newline at end of file diff --git a/tests/utils/test_containers.py b/tests/utils/test_containers.py index 1206fbf6a..815a32277 100644 --- a/tests/utils/test_containers.py +++ b/tests/utils/test_containers.py @@ -1,6 +1,6 @@ import re from io import IOBase -from unittest.mock import call, patch +from unittest.mock import ANY, call, patch import sh from ploigos_step_runner.config import ConfigValue @@ -1196,3 +1196,120 @@ def test_default_image_version(self): actual_image_version, 'latest' ) + +@patch('sh.buildah', create=True) +class Test_inspect_container_image(BaseTestCase): + def test_success_no_auth(self, mock_buildah): + # setup mock + def buildah_inspect_side_effect(container_image_uri, _out): + _out.write('''{ + "mock-value": "mock container details" +}''') + mock_buildah.inspect.side_effect = buildah_inspect_side_effect + + # run test + actual_container_details = inspect_container_image( + container_image_uri='mock.io/mock/awesome-image:latest' + ) + + # validate + self.assertEqual( + actual_container_details, + { + 'mock-value': 'mock container details' + } + ) + mock_buildah.pull.assert_called_once_with( + 'mock.io/mock/awesome-image:latest' + ) + mock_buildah.inspect.assert_called_once_with( + 'mock.io/mock/awesome-image:latest', + _out=ANY + ) + + def test_success_with_auth(self, mock_buildah): + # setup mock + def buildah_inspect_side_effect(*args, _out): + _out.write('''{ + "mock-value": "mock container details" +}''') + mock_buildah.inspect.side_effect = buildah_inspect_side_effect + + # run test + actual_container_details = inspect_container_image( + container_image_uri='mock.io/mock/awesome-image:latest', + containers_config_auth_file='/mock/auth-file' + ) + + # validate + self.assertEqual( + actual_container_details, + { + 'mock-value': 'mock container details' + } + ) + mock_buildah.pull.assert_called_once_with( + '--authfile', '/mock/auth-file', + 'mock.io/mock/awesome-image:latest' + ) + mock_buildah.inspect.assert_called_once_with( + 'mock.io/mock/awesome-image:latest', + _out=ANY + ) + + def test_failure_pull_no_auth(self, mock_buildah): + # setup mock + mock_buildah.pull.side_effect = sh.ErrorReturnCode('buildah pull', b'mock out', b'mock error') + + # run test + with self.assertRaisesRegex( + RuntimeError, + re.compile( + "Error pulling container image \(mock.io/mock/awesome-image:latest\) for inspection:" + r".*RAN: buildah pull" + r".*STDOUT:" + r".*mock out" + r".*STDERR:" + r".*mock error", + re.DOTALL + ) + ): + inspect_container_image( + container_image_uri='mock.io/mock/awesome-image:latest' + ) + + # validate + mock_buildah.pull.assert_called_once_with( + 'mock.io/mock/awesome-image:latest' + ) + mock_buildah.inspect.assert_not_called() + + def test_failure_inspect_no_auth(self, mock_buildah): + # setup mock + mock_buildah.inspect.side_effect = sh.ErrorReturnCode('buildah', b'mock out', b'mock error') + + # run test + with self.assertRaisesRegex( + RuntimeError, + re.compile( + "Error inspecting container image \(mock.io/mock/awesome-image:latest\)" + r".*RAN: buildah" + r".*STDOUT:" + r".*mock out" + r".*STDERR:" + r".*mock error", + re.DOTALL + ) + ): + inspect_container_image( + container_image_uri='mock.io/mock/awesome-image:latest' + ) + + # validate + mock_buildah.pull.assert_called_once_with( + 'mock.io/mock/awesome-image:latest' + ) + mock_buildah.inspect.assert_called_once_with( + 'mock.io/mock/awesome-image:latest', + _out=ANY + )