def __init__(self, source, image, **kwargs): """ """ LastLogger.__init__(self) BuilderStateMachine.__init__(self) print_version_of_tools() self.tasker = DockerTasker() info, version = self.tasker.get_info(), self.tasker.get_version() logger.debug(json.dumps(info, indent=2)) logger.info(json.dumps(version, indent=2)) # arguments for build self.source = source self.base_image_id = None self.image_id = None self.built_image_info = None self.image = ImageName.parse(image) # get info about base image from dockerfile self.df_path, self.df_dir = self.source.get_dockerfile_path() self.base_image = ImageName.parse(DockerfileParser(self.df_path).baseimage) logger.debug("base image specified in dockerfile = '%s'", self.base_image) if not self.base_image.tag: self.base_image.tag = 'latest'
def test_pull_parent_images(organization, reactor_config_map, inspect_only): builder_image = 'builder:image' parent_images = {BASE_IMAGE_NAME.copy(): None, ImageName.parse(builder_image): None} enclosed_base_image = BASE_IMAGE_W_REGISTRY enclosed_builder_image = LOCALHOST_REGISTRY + '/' + builder_image if organization and reactor_config_map: base_image_name = ImageName.parse(enclosed_base_image) base_image_name.enclose(organization) enclosed_base_image = base_image_name.to_str() builder_image_name = ImageName.parse(enclosed_builder_image) builder_image_name.enclose(organization) enclosed_builder_image = builder_image_name.to_str() test_pull_base_image_plugin( LOCALHOST_REGISTRY, BASE_IMAGE, [ # expected to pull enclosed_base_image, enclosed_builder_image, ], [], # should not be pulled reactor_config_map=reactor_config_map, inspect_only=inspect_only, parent_images=parent_images, organization=organization)
def test_hostdocker_build(caplog, source_params): if MOCK: mock_docker() image_name = ImageName(repo="atomic-reactor-test-ssh-image") remote_image = image_name.copy() remote_image.registry = LOCALHOST_REGISTRY m = DockerhostBuildManager("buildroot-dh-fedora", { "source": source_params, "image": remote_image.to_str(), "parent_registry": LOCALHOST_REGISTRY, # faster "target_registries_insecure": True, "parent_registry_insecure": True, }) results = m.build() dt = DockerTasker() dt.pull_image(remote_image, insecure=True) if source_params['provider'] == 'path': assert_source_from_path_mounted_ok(caplog, m.temp_dir) assert len(results.build_logs) > 0 #assert re.search(r'build json mounted in container .+"uri": %s' % # os.path.join(dconstants.CONTAINER_SHARE_PATH, 'source')) # assert isinstance(results.built_img_inspect, dict) # assert len(results.built_img_inspect.items()) > 0 # assert isinstance(results.built_img_info, dict) # assert len(results.built_img_info.items()) > 0 # assert isinstance(results.base_img_info, dict) # assert len(results.base_img_info.items()) > 0 # assert len(results.base_plugins_output) > 0 # assert len(results.built_img_plugins_output) > 0 dt.remove_container(results.container_id) dt.remove_image(remote_image)
def test_update_base_image(organization, tmpdir, reactor_config_map, docker_tasker): df_content = dedent("""\ FROM {} LABEL horses=coconuts CMD whoah """) dfp = df_parser(str(tmpdir)) image_str = "base:image" dfp.content = df_content.format(image_str) base_str = "base@sha256:1234" base_image_name = ImageName.parse("base@sha256:1234") enclosed_parent = ImageName.parse(image_str) if organization and reactor_config_map: enclosed_parent.enclose(organization) workflow = mock_workflow() workflow.builder.set_df_path(dfp.dockerfile_path) workflow.builder.parent_images = {enclosed_parent: base_image_name} workflow.builder.base_image = base_image_name workflow.builder.set_parent_inspection_data(base_str, dict(Id=base_str)) workflow.builder.tasker.inspect_image = lambda *_: dict(Id=base_str) run_plugin(workflow, reactor_config_map, docker_tasker, organization=organization) expected_df = df_content.format(base_str) assert dfp.content == expected_df
def test_privileged_build(caplog, source_params): if MOCK: mock_docker() image_name = ImageName(repo=TEST_IMAGE) remote_image = image_name.copy() remote_image.registry = LOCALHOST_REGISTRY m = PrivilegedBuildManager( "buildroot-fedora", { "source": source_params, "image": remote_image.to_str(), "parent_registry": LOCALHOST_REGISTRY, # faster "target_registries_insecure": True, "parent_registry_insecure": True, }, ) results = m.build() dt = DockerTasker() dt.pull_image(remote_image, insecure=True) if source_params["provider"] == "path": assert_source_from_path_mounted_ok(caplog, m.temp_dir) assert len(results.build_logs) > 0 # assert isinstance(results.built_img_inspect, dict) # assert len(results.built_img_inspect.items()) > 0 # assert isinstance(results.built_img_info, dict) # assert len(results.built_img_info.items()) > 0 # assert isinstance(results.base_img_info, dict) # assert len(results.base_img_info.items()) > 0 # assert len(results.base_plugins_output) > 0 # assert len(results.built_img_plugins_output) > 0 dt.remove_container(results.container_id) dt.remove_image(remote_image)
def test_get_primary_images(tag_conf, tag_annotation, expected): template_image = ImageName.parse('registry.example.com/fedora') workflow = DockerBuildWorkflow(MOCK_SOURCE, 'test-image') for tag in tag_conf: image_name = ImageName.parse(str(template_image)) image_name.tag = tag workflow.tag_conf.add_primary_image(str(image_name)) annotations = {} for tag in tag_annotation: annotations.setdefault('repositories', {}).setdefault('primary', []) image_name = ImageName.parse(str(template_image)) image_name.tag = tag annotations['repositories']['primary'].append(str(image_name)) build_result = BuildResult(annotations=annotations, image_id='foo') workflow.build_result = build_result actual = get_primary_images(workflow) assert len(actual) == len(expected) for index, primary_image in enumerate(actual): assert primary_image.registry == template_image.registry assert primary_image.namespace == template_image.namespace assert primary_image.repo == template_image.repo assert primary_image.tag == expected[index]
def test_image_download(tmpdir, docker_tasker, parents, skip_plugin, architecture, architectures, download_filesystem, reactor_config_map, caplog): if MOCK: mock_docker() workflow = mock_workflow(tmpdir) if not skip_plugin: mock_koji_session(download_filesystem=download_filesystem) mock_image_build_file(str(tmpdir)) workflow.builder.base_image = ImageName.parse(parents[-1]) workflow.builder.parents_ordered = parents workflow.builder.custom_parent_image = 'koji/image-build' in parents workflow.builder.custom_base_image = 'koji/image-build' == parents[-1] workflow.builder.parent_images = {} for image in parents: if image == 'scratch': continue workflow.builder.parent_images[ImageName.parse(image)] = None if architectures: workflow.prebuild_results[PLUGIN_CHECK_AND_SET_PLATFORMS_KEY] = set(architectures) if reactor_config_map: make_and_store_reactor_config_map(workflow, {'root_url': '', 'auth': {}}) runner = PreBuildPluginsRunner( docker_tasker, workflow, [{ 'name': PLUGIN_ADD_FILESYSTEM_KEY, 'args': { 'koji_hub': KOJI_HUB, 'architecture': architecture, } }] ) results = runner.run() plugin_result = results[PLUGIN_ADD_FILESYSTEM_KEY] if skip_plugin: message = 'Nothing to do for non-custom base images' assert message in caplog.text assert plugin_result is None return assert 'base-image-id' in plugin_result assert 'filesystem-koji-task-id' in plugin_result if download_filesystem: assert plugin_result['base-image-id'] == IMPORTED_IMAGE_ID assert plugin_result['filesystem-koji-task-id'] == FILESYSTEM_TASK_ID else: assert plugin_result['base-image-id'] is None assert plugin_result['filesystem-koji-task-id'] is None
def test_try_with_library_pull_base_image(library, reactor_config_map): if MOCK: mock_docker(remember_images=True) tasker = DockerTasker(retry_times=0) workflow = DockerBuildWorkflow(MOCK_SOURCE, 'test-image') workflow.builder = MockBuilder() if library: base_image = 'library/parent-image' else: base_image = 'parent-image' workflow.builder.base_image = ImageName.parse(base_image) workflow.builder.parent_images = {ImageName.parse(base_image): None} class MockResponse(object): content = '' cr = CommandResult() cr._error = "cmd_error" cr._error_detail = {"message": "error_detail"} if library: call_wait = 1 else: call_wait = 2 (flexmock(atomic_reactor.util) .should_receive('wait_for_command') .times(call_wait) .and_return(cr)) error_message = 'registry.example.com/' + base_image if reactor_config_map: workflow.plugin_workspace[ReactorConfigPlugin.key] = {} workflow.plugin_workspace[ReactorConfigPlugin.key][WORKSPACE_CONF_KEY] =\ ReactorConfig({'version': 1, 'source_registry': {'url': 'registry.example.com', 'insecure': True}}) runner = PreBuildPluginsRunner( tasker, workflow, [{ 'name': PullBaseImagePlugin.key, 'args': {'parent_registry': 'registry.example.com', 'parent_registry_insecure': True}, }], ) with pytest.raises(PluginFailedException) as exc: runner.run() assert error_message in exc.value.args[0]
def workflow_callback(workflow): workflow = self.prepare(workflow) release = 'rel1' version = 'ver1' config_blob = {'config': {'Labels': {'release': release, 'version': version}}} (flexmock(atomic_reactor.util) .should_receive('get_config_from_registry') .and_return(config_blob) .times(0 if sha_is_manifest_list else 1)) manifest_list = { 'manifests': [ {'platform': {'architecture': 'amd64'}, 'digest': 'sha256:123456'}, {'platform': {'architecture': 'ppc64le'}, 'digest': 'sha256:654321'}, ] } manifest_tag = 'registry.example.com' + '/' + BASE_IMAGE_W_SHA base_image_result = ImageName.parse(manifest_tag) manifest_image = base_image_result.copy() if sha_is_manifest_list: (flexmock(atomic_reactor.util) .should_receive('get_manifest_list') .with_args(image=manifest_image, registry=manifest_image.registry, insecure=True, dockercfg_path=None) .and_return(flexmock(json=lambda: manifest_list, content=json.dumps(manifest_list).encode('utf-8'))) .once()) else: (flexmock(atomic_reactor.util) .should_receive('get_manifest_list') .with_args(image=manifest_image, registry=manifest_image.registry, insecure=True, dockercfg_path=None) .and_return(None) .once() .ordered()) docker_tag = "%s-%s" % (version, release) manifest_tag = 'registry.example.com' + '/' +\ BASE_IMAGE_W_SHA[:BASE_IMAGE_W_SHA.find('@sha256')] +\ ':' + docker_tag base_image_result = ImageName.parse(manifest_tag) manifest_image = base_image_result.copy() (flexmock(atomic_reactor.util) .should_receive('get_manifest_list') .with_args(image=manifest_image, registry=manifest_image.registry, insecure=True, dockercfg_path=None) .and_return(flexmock(json=lambda: manifest_list)) .once() .ordered()) return workflow
def test_parent_images_mismatch_base_image(tmpdir, docker_tasker): """test when base_image has been updated differently from parent_images.""" dfp = df_parser(str(tmpdir)) dfp.content = "FROM base:image" workflow = mock_workflow() workflow.builder.set_df_path(dfp.dockerfile_path) workflow.builder.base_image = ImageName.parse("base:image") workflow.builder.parent_images = { ImageName.parse("base:image"): ImageName.parse("different-parent-tag") } with pytest.raises(BaseImageMismatch): ChangeFromPlugin(docker_tasker, workflow).run()
def __init__(self): self.tasker = flexmock() self.base_image = ImageName(repo='Fedora', tag='22') self.original_base_image = ImageName(repo='Fedora', tag='22') self.base_from_scratch = False self.custom_base_image = False self.parent_images = {ImageName.parse('base'): ImageName.parse('base:stubDigest')} base_inspect = {INSPECT_CONFIG: {'Labels': BASE_IMAGE_LABELS.copy()}} self._parent_images_inspect = {ImageName.parse('base:stubDigest'): base_inspect} self.parent_images_digests = {'base:latest': {V2_LIST: 'stubDigest'}} self.image_id = 'image_id' self.image = 'image' self._df_path = 'df_path' self.df_dir = 'df_dir'
def test_pull_parent_wrong_registry(reactor_config_map, inspect_only): # noqa: F811 parent_images = { ImageName.parse("base:image"): None, ImageName.parse("some.registry:8888/builder:image"): None} with pytest.raises(PluginFailedException) as exc: test_pull_base_image_plugin( 'different.registry:5000', "base:image", [], [], reactor_config_map=reactor_config_map, inspect_only=inspect_only, parent_images=parent_images ) assert "Dockerfile: 'some.registry:8888/builder:image'" in str(exc.value) assert "expected registry: 'different.registry:5000'" in str(exc.value) assert "base:image" not in str(exc.value)
def _find_image(img, ignore_registry=False): global mock_images tagged_img = ImageName.parse(img).to_str(explicit_tag=True) for im in mock_images: im_name = im['RepoTags'][0] if im_name == tagged_img: return im if ignore_registry: im_name_wo_reg = ImageName.parse(im_name).to_str(registry=False) if im_name_wo_reg == tagged_img: return im return None
def test_parent_images_unresolved(tmpdir, docker_tasker): """test when parent_images hasn't been filled in with unique tags.""" dfp = df_parser(str(tmpdir)) dfp.content = "FROM spam" workflow = mock_workflow() workflow.builder.set_df_path(dfp.dockerfile_path) workflow.builder.base_image = ImageName.parse('eggs') # we want to fail because some img besides base was not resolved workflow.builder.parent_images = { ImageName.parse('spam'): ImageName.parse('eggs'), ImageName.parse('extra:image'): None } with pytest.raises(ParentImageUnresolved): ChangeFromPlugin(docker_tasker, workflow).run()
def run(self): dockerfile = DockerfileParser(self.workflow.builder.df_path) image_name = ImageName.parse(dockerfile.baseimage) if image_name.namespace != 'koji' or image_name.repo != 'image-build' : self.log.info('Base image not supported: %s', dockerfile.baseimage) return image_build_conf = image_name.tag or 'image-build.conf' self.session = create_koji_session(self.koji_hub, self.koji_auth_info) task_id, filesystem_regex = self.build_filesystem(image_build_conf) task = TaskWatcher(self.session, task_id, self.poll_interval) task.wait() if task.failed(): raise RuntimeError('Create filesystem task failed: {}' .format(task_id)) filesystem = self.download_filesystem(task_id, filesystem_regex) base_image = self.import_base_image(filesystem) dockerfile.baseimage = base_image return base_image
def create_image(self, df_dir_path, image, use_cache=False): """ create image: get atomic-reactor sdist tarball, build image and tag it :param df_path: :param image: :return: """ logger.debug("creating build image: df_dir_path = '%s', image = '%s'", df_dir_path, image) if not os.path.isdir(df_dir_path): raise RuntimeError("Directory '%s' does not exist.", df_dir_path) tmpdir = tempfile.mkdtemp() df_tmpdir = os.path.join(tmpdir, 'df-%s' % uuid.uuid4()) git_tmpdir = os.path.join(tmpdir, 'git-%s' % uuid.uuid4()) os.mkdir(df_tmpdir) logger.debug("tmp dir with dockerfile '%s' created", df_tmpdir) os.mkdir(git_tmpdir) logger.debug("tmp dir with atomic-reactor '%s' created", git_tmpdir) try: for f in glob(os.path.join(df_dir_path, '*')): shutil.copy(f, df_tmpdir) logger.debug("cp '%s' -> '%s'", f, df_tmpdir) logger.debug("df dir: %s", os.listdir(df_tmpdir)) reactor_tarball = self.get_reactor_tarball_path(tmpdir=git_tmpdir) reactor_tb_path = os.path.join(df_tmpdir, DOCKERFILE_REACTOR_TARBALL_NAME) shutil.copy(reactor_tarball, reactor_tb_path) image_name = ImageName.parse(image) logs_gen = self.tasker.build_image_from_path(df_tmpdir, image_name, stream=True, use_cache=use_cache) wait_for_command(logs_gen) finally: shutil.rmtree(tmpdir)
def run(self): image_names = self.workflow.tag_conf.images[:] # Add in additional image names, if any if self.image_names: self.log.info("extending image names: %s", self.image_names) image_names += [ImageName.parse(x) for x in self.image_names] if self.load_exported_image: if len(self.workflow.exported_image_sequence) == 0: raise RuntimeError('no exported image to push to pulp') export_path = self.workflow.exported_image_sequence[-1].get("path") top_layer, crane_repos = self.push_tar(export_path, image_names) else: # Work out image ID image = self.workflow.image self.log.info("fetching image %s from docker", image) with tempfile.NamedTemporaryFile(prefix='docker-image-', suffix='.tar') as image_file: image_file.write(self.tasker.d.get_image(image).data) # This file will be referenced by its filename, not file # descriptor - must ensure contents are written to disk image_file.flush() top_layer, crane_repos = self.push_tar(image_file.name, image_names) if self.publish: for image_name in crane_repos: self.log.info("image available at %s", str(image_name)) return top_layer, crane_repos
def mock_environment(tmpdir, primary_images=None, annotations={}): if MOCK: mock_docker() tasker = DockerTasker() workflow = DockerBuildWorkflow(SOURCE, "test-image") base_image_id = '123456parent-id' setattr(workflow, '_base_image_inspect', {'Id': base_image_id}) setattr(workflow, 'builder', X()) setattr(workflow.builder, 'image_id', '123456imageid') setattr(workflow.builder, 'base_image', ImageName(repo='Fedora', tag='22')) setattr(workflow.builder, 'source', X()) setattr(workflow.builder, 'built_image_info', {'ParentId': base_image_id}) setattr(workflow.builder.source, 'dockerfile_path', None) setattr(workflow.builder.source, 'path', None) setattr(workflow, 'tag_conf', TagConf()) if primary_images: for image in primary_images: if '-' in ImageName.parse(image).tag: workflow.tag_conf.add_primary_image(image) workflow.tag_conf.add_unique_image(primary_images[0]) workflow.build_result = BuildResult(image_id='123456', annotations=annotations) return tasker, workflow
def tag_image(self, image, target_image, force=False): """ tag provided image with specified image_name, registry and tag :param image: str or ImageName, image to tag :param target_image: ImageName, new name for the image :param force: bool, force tag the image? :return: str, image (reg.om/img:v1) """ logger.info("tagging image '%s' as '%s'", image, target_image) logger.debug("image = '%s', target_image_name = '%s'", image, target_image) if not isinstance(image, ImageName): image = ImageName.parse(image) if image != target_image: response = self.d.tag( image.to_str(), target_image.to_str(tag=False), tag=target_image.tag, force=force) # returns True/False if not response: logger.error("failed to tag image") raise RuntimeError("Failed to tag image '%s': target_image = '%s'" % image.to_str(), target_image) else: logger.debug('image already tagged correctly, nothing to do') return target_image.to_str() # this will be the proper name, not just repo/img
def run(self): image = self.workflow.builder.image_id if not image: self.log.error("no built image, nothing to remove") return try: self.tasker.remove_image(image, force=True) except APIError as ex: if ex.is_client_error(): self.log.warning("failed to remove built image %s (%s: %s), ignoring", image, ex.response.status_code, ex.response.reason) else: raise if self.remove_base_image and self.workflow.pulled_base_images: # FIXME: we may need to add force here, let's try it like this for now # FIXME: when ID of pulled img matches an ID of an image already present, don't remove for base_image_tag in self.workflow.pulled_base_images: try: self.tasker.remove_image(ImageName.parse(base_image_tag)) except APIError as ex: if ex.is_client_error(): self.log.warning("failed to remove base image %s (%s: %s), ignoring", base_image_tag, ex.response.status_code, ex.response.reason) else: raise
def build_image_privileged_container(self, build_image, json_args_path): """ Build image inside privileged container: this will run another docker instance inside This operation is asynchronous and you should wait for container to finish. :param build_image: str, name of image where build is performed :param json_args_path: str, this dir is mounted inside build container and used as a way to transport data between host and buildroot; there has to be a file inside this dir with name atomic_reactor.BUILD_JSON which is used to feed build :return: dict, keys container_id and stream """ logger.info("building image '%s' inside privileged container", build_image) self._check_build_input(build_image, json_args_path) self._obtain_source_from_path_if_needed(json_args_path, CONTAINER_SHARE_PATH) volume_bindings = {json_args_path: {"bind": CONTAINER_SHARE_PATH}} if self._volume_bind_understands_mode(): volume_bindings[json_args_path]["mode"] = "rw,Z" else: volume_bindings[json_args_path]["rw"] = True logger.debug("build json mounted in container: %s", open(os.path.join(json_args_path, BUILD_JSON)).read()) container_id = self.tasker.run( ImageName.parse(build_image), create_kwargs={"volumes": [json_args_path]}, start_kwargs={"binds": volume_bindings, "privileged": True}, ) return container_id
def test_pull_base_image_plugin(df_base, parent_registry, expected_w_reg, expected_wo_reg): if MOCK: mock_docker(remember_images=True) tasker = DockerTasker() workflow = DockerBuildWorkflow(MOCK_SOURCE, 'test-image') workflow.builder = MockBuilder() workflow.builder.base_image = ImageName.parse(df_base) assert not tasker.image_exists(BASE_IMAGE) assert not tasker.image_exists(BASE_IMAGE_W_REGISTRY) runner = PreBuildPluginsRunner( tasker, workflow, [{ 'name': PullBaseImagePlugin.key, 'args': {'parent_registry': parent_registry, 'parent_registry_insecure': True} }] ) runner.run() assert tasker.image_exists(BASE_IMAGE) == expected_wo_reg assert tasker.image_exists(BASE_IMAGE_W_REGISTRY) == expected_w_reg try: tasker.remove_image(BASE_IMAGE) tasker.remove_image(BASE_IMAGE_W_REGISTRY) except: pass
def test_retry_pull_base_image(exc, failures, should_succeed): if MOCK: mock_docker(remember_images=True) tasker = DockerTasker() workflow = DockerBuildWorkflow(MOCK_SOURCE, 'test-image') workflow.builder = MockBuilder() workflow.builder.base_image = ImageName.parse('parent-image') class MockResponse(object): content = '' expectation = flexmock(tasker).should_receive('tag_image') for _ in range(failures): expectation = expectation.and_raise(exc('', MockResponse())) expectation.and_return('foo') expectation.and_return('parent-image') runner = PreBuildPluginsRunner( tasker, workflow, [{ 'name': PullBaseImagePlugin.key, 'args': {'parent_registry': 'registry.example.com', 'parent_registry_insecure': True}, }], ) if should_succeed: runner.run() else: with pytest.raises(Exception): runner.run()
def __init__(self, source, image, **kwargs): """ """ LastLogger.__init__(self) BuilderStateMachine.__init__(self) print_version_of_tools() self.tasker = DockerTasker() info, version = self.tasker.get_info(), self.tasker.get_version() logger.debug(json.dumps(info, indent=2)) logger.info(json.dumps(version, indent=2)) # arguments for build self.source = source self.base_image = None self.image_id = None self.built_image_info = None self.image = ImageName.parse(image) # get info about base image from dockerfile build_file_path, build_file_dir = self.source.get_build_file_path() self.df_dir = build_file_dir self._df_path = None # If the build file isn't a Dockerfile, but say, a flatpak.json then a # plugin needs to create the Dockerfile and set the base image if build_file_path.endswith(DOCKERFILE_FILENAME): self.set_df_path(build_file_path)
def test_parent_images_missing(tmpdir, docker_tasker): """test when parent_images has been mangled and lacks parents compared to dockerfile.""" dfp = df_parser(str(tmpdir)) dfp.content = dedent("""\ FROM first:parent AS builder1 FROM second:parent AS builder2 FROM monty """) workflow = mock_workflow() workflow.builder.set_df_path(dfp.dockerfile_path) workflow.builder.parent_images = {ImageName.parse("monty"): ImageName.parse("build-name:3")} workflow.builder.base_image = ImageName.parse("build-name:3") with pytest.raises(ParentImageMissing): ChangeFromPlugin(docker_tasker, workflow).run()
def test_get_image_info_by_name_tag_in_name_library(): if MOCK: mock_docker() t = DockerTasker() image_name = ImageName.parse("library/busybox") response = t.get_image_info_by_image_name(image_name) assert len(response) == 1
def __init__(self): self.image_id = "xxx" self.base_image = ImageName.parse("koji/image-build") self.parent_images = {self.base_image: None} self.parents_ordered = "koji/image-build" self.custom_base_image = True self.custom_parent_image = True self.set_base_image = flexmock()
def mock_workflow(): """ Provide just enough structure that workflow can be used to run the plugin. Defaults below are solely to enable that purpose; tests where those values matter should provide their own. """ workflow = DockerBuildWorkflow(SOURCE, "mock:default_built") workflow.source = StubSource() builder = StubInsideBuilder().for_workflow(workflow) builder.set_df_path('/mock-path') base_image_name = ImageName.parse("mock:tag") builder.parent_images[ImageName.parse("mock:base")] = base_image_name builder.base_image = base_image_name builder.tasker = flexmock() workflow.builder = flexmock(builder) return workflow
def test_update_base_image_inspect_broken(tmpdir, caplog, docker_tasker): """exercise code branch where the base image inspect comes back without an Id""" df_content = "FROM base:image" dfp = df_parser(str(tmpdir)) dfp.content = df_content image_str = "base@sha256:1234" image_name = ImageName.parse(image_str) workflow = mock_workflow() workflow.builder.set_df_path(dfp.dockerfile_path) workflow.builder.parent_images = {ImageName.parse("base:image"): image_name} workflow.builder.base_image = image_name workflow.builder.set_parent_inspection_data(image_str, dict(no_id="here")) with pytest.raises(NoIdInspection): ChangeFromPlugin(docker_tasker, workflow).run() assert dfp.content == df_content # nothing changed assert "missing in inspection" in caplog.text
def set_base_image(self, base_image, parents_pulled=True, insecure=False, dockercfg_path=None): self.base_from_scratch = base_image_is_scratch(base_image) if not self.custom_base_image: self.custom_base_image = base_image_is_custom(base_image) self.base_image = ImageName.parse(base_image) self.original_base_image = self.original_base_image or self.base_image self.recreate_parent_images() if not self.base_from_scratch: self.parent_images[self.original_base_image] = self.base_image
def test_get_manifest_digests_connection_error(tmpdir): # Test that our code to handle falling back from https to http # doesn't do anything unexpected when a connection can't be # made at all. kwargs = {} kwargs['image'] = ImageName.parse('example.com/spam:latest') kwargs['registry'] = 'https://example.com' url = 'https://example.com/v2/spam/manifests/latest' responses.add(responses.GET, url, body=ConnectionError()) with pytest.raises(ConnectionError): get_manifest_digests(**kwargs)
def _find_image(img, ignore_registry=False): global mock_images for im in mock_images: im_name = im['RepoTags'][0] if im_name == img: return im if ignore_registry: im_name_wo_reg = ImageName.parse(im_name).to_str(registry=False) if im_name_wo_reg == img: return im return None
def __init__(self, failed=False): self.tasker = MockDockerTasker() self.base_image = ImageName(repo='Fedora', tag='22') self.image_id = 'asd' self.image = 'image' self.failed = failed self.df_path = 'some' self.df_dir = 'some' def simplegen(x, y): yield "some\u2018".encode('utf-8') flexmock(self.tasker, build_image_from_path=simplegen)
def __init__(self): mock_docker() self.tasker = DockerTasker() self.base_image = ImageName(repo='fedora', tag='25') self.image_id = 'image_id' self.image = INPUT_IMAGE self.df_path = 'df_path' self.df_dir = 'df_dir' def simplegen(x, y): yield "some\u2018".encode('utf-8') flexmock(self.tasker, build_image_from_path=simplegen)
def test_parent_images(parents_pulled, tmpdir, source_params): if MOCK: mock_docker() s = get_source_instance_for(source_params) b = InsideBuilder(s, '') orig_base = b.base_image if not b.base_from_scratch: assert orig_base in b.parent_images assert b.parent_images[orig_base] is None b.set_base_image("spam:eggs", parents_pulled=parents_pulled) assert b.parent_images[orig_base] == ImageName.parse("spam:eggs") assert b.parents_pulled == parents_pulled
def test_yuminject_multiline_wrapped_with_chown(tmpdir, docker_tasker): # noqa df_content = """\ FROM fedora RUN yum install -y --setopt=tsflags=nodocs bind-utils gettext iproute v8314 mongodb24-mongodb mongodb24 && \ yum clean all && \ mkdir -p /var/lib/mongodb/data && chown -R mongodb:mongodb /var/lib/mongodb/ && \ test "$(id mongodb)" = "uid=184(mongodb) gid=998(mongodb) groups=998(mongodb)" && \ chmod o+w -R /var/lib/mongodb && chmod o+w -R /opt/rh/mongodb24/root/var/lib/mongodb CMD blabla""" # noqa df = df_parser(str(tmpdir)) df.content = df_content workflow = DockerBuildWorkflow(SOURCE, "test-image") setattr(workflow, 'builder', X()) metalink = r'https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch' # noqa workflow.files[os.path.join(YUM_REPOS_DIR, DEFAULT_YUM_REPOFILE_NAME)] = \ render_yum_repo(OrderedDict((('name', 'my-repo'), ('metalink', metalink), ('enabled', 1), ('gpgcheck', 0)), )) setattr(workflow.builder, 'image_id', "asd123") setattr(workflow.builder, 'df_path', df.dockerfile_path) setattr(workflow.builder, 'df_dir', str(tmpdir)) setattr(workflow.builder, 'base_image', ImageName(repo='Fedora', tag='21')) setattr(workflow.builder, 'git_dockerfile_path', None) setattr(workflow.builder, 'git_path', None) setattr(workflow.builder, 'source', X()) setattr(workflow.builder.source, 'dockerfile_path', None) setattr(workflow.builder.source, 'path', '') runner = PreBuildPluginsRunner(docker_tasker, workflow, [{ 'name': InjectYumRepoPlugin.key, 'args': { "wrap_commands": True } }]) runner.run() assert InjectYumRepoPlugin.key is not None expected_output = """FROM fedora RUN printf "[my-repo]\nname=my-repo\nmetalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-\\$releasever&arch=\ \\$basearch\nenabled=1\ngpgcheck=0\n" >/etc/yum.repos.d/atomic-reactor-injected.repo && \ yum install -y --setopt=tsflags=nodocs bind-utils gettext iproute v8314 mongodb24-mongodb mongodb24 && \ yum clean all && mkdir -p /var/lib/mongodb/data && chown -R mongodb:mongodb /var/lib/mongodb/ && \ test "$(id mongodb)" = "uid=184(mongodb) gid=998(mongodb) groups=998(mongodb)" && \ chmod o+w -R /var/lib/mongodb && chmod o+w -R /opt/rh/mongodb24/root/var/lib/mongodb && \ yum clean all && rm -f /etc/yum.repos.d/atomic-reactor-injected.repo CMD blabla""" # noqa assert df.content == expected_output
def run(self): pulp = dockpulp.Pulp(env=self.pulp_registry_name) self.set_auth(pulp) # We only want the hostname[:port] hostname_and_port = re.compile(r'^https?://([^/]*)/?.*') pulp_registry = hostname_and_port.sub(lambda m: m.groups()[0], pulp.registry) # Store the registry URI in the push configuration self.workflow.push_conf.add_pulp_registry(self.pulp_registry_name, pulp_registry) self.log.info("syncing from docker V2 registry %s", self.docker_registry) docker_registry = hostname_and_port.sub(lambda m: m.groups()[0], self.docker_registry) kwargs = self.get_dockercfg_credentials(docker_registry) if self.insecure_registry is not None: kwargs['ssl_validation'] = not self.insecure_registry images = [] repos = {} # pulp repo -> repo id for image in self.workflow.tag_conf.images: if image.pulp_repo not in repos: repo_id = self.create_repo_if_missing( pulp, image.pulp_repo, image.to_str(registry=False, tag=False)) self.log.info("syncing %s", repo_id) pulp.syncRepo(repo=repo_id, feed=self.docker_registry, **kwargs) repos[image.pulp_repo] = repo_id images.append( ImageName(registry=pulp_registry, repo=image.repo, namespace=image.namespace, tag=image.tag)) self.log.info("publishing to crane") pulp.crane(list(repos.values()), wait=True) if self.publish: for image_name in images: self.log.info("image available at %s", image_name.to_str()) # Return the set of qualified repo names for this image return images
def build_image_dockerhost(self, build_image, json_args_path): """ Build docker image inside privileged container using docker from host (mount docker socket inside container). There are possible races here. Use wisely. This operation is asynchronous and you should wait for container to finish. :param build_image: str, name of image where build is performed :param json_args_path: str, this dir is mounted inside build container and used as a way to transport data between host and buildroot; there has to be a file inside this dir with name atomic_reactor.BUILD_JSON which is used to feed build :return: str, container id """ logger.info("building image '%s' in container using docker from host", build_image) self._check_build_input(build_image, json_args_path) self._obtain_source_from_path_if_needed(json_args_path, CONTAINER_SHARE_PATH) if not os.path.exists(DOCKER_SOCKET_PATH): logger.error( "looks like docker is not running because there is no socket at: %s", DOCKER_SOCKET_PATH) raise RuntimeError("docker socket not found: %s" % DOCKER_SOCKET_PATH) volume_bindings = { DOCKER_SOCKET_PATH: { 'bind': DOCKER_SOCKET_PATH, 'mode': 'ro', }, json_args_path: { 'bind': CONTAINER_SHARE_PATH, 'mode': 'rw,Z', }, } with open(os.path.join(json_args_path, BUILD_JSON)) as fp: logger.debug('build json mounted in container: %s', fp.read()) container_id = self.tasker.run( ImageName.parse(build_image), create_kwargs={'volumes': [DOCKER_SOCKET_PATH, json_args_path]}, volume_bindings=volume_bindings, privileged=True, ) return container_id
def test_base_image_missing_labels(self, workflow, koji_session, remove_labels, exp_result, reactor_config_map, external, caplog): base_tag = ImageName.parse('base:stubDigest') workflow.builder.base_image_inspect[INSPECT_CONFIG]['Labels'] =\ BASE_IMAGE_LABELS_W_ALIASES.copy() workflow.builder._parent_images_inspect[base_tag][INSPECT_CONFIG]['Labels'] =\ BASE_IMAGE_LABELS_W_ALIASES.copy() for label in remove_labels: del workflow.builder.base_image_inspect[INSPECT_CONFIG]['Labels'][ label] del workflow.builder._parent_images_inspect[base_tag][ INSPECT_CONFIG]['Labels'][label] if not exp_result: if not (external and reactor_config_map): with pytest.raises(PluginFailedException) as exc: self.run_plugin_with_args( workflow, expect_result=exp_result, reactor_config_map=reactor_config_map, external_base=external) assert 'Was this image built in OSBS?' in str(exc.value) else: result = { PARENT_IMAGES_KOJI_BUILDS: { ImageName.parse('base'): None } } self.run_plugin_with_args( workflow, expect_result=result, reactor_config_map=reactor_config_map, external_base=external) assert 'Was this image built in OSBS?' in caplog.text else: self.run_plugin_with_args(workflow, expect_result=exp_result, reactor_config_map=reactor_config_map)
def workflow(docker_repos, registry=None): images = [] for tag in ['1.0-1', '1.0', 'latest', 'unique-timestamp']: images.extend([ ImageName.parse('{0}/{1}:{2}'.format(registry, repo, tag).lstrip('/')) for repo in docker_repos ]) tag_conf = flexmock(images=images) push_conf = PushConf() return flexmock(tag_conf=tag_conf, push_conf=push_conf, postbuild_plugins_conf=[])
def set_base_image(self, base_image, parents_pulled=True, insecure=False): self.base_from_scratch = base_image_is_scratch(base_image) if not self.custom_base_image: self.custom_base_image = base_image_is_custom(base_image) self.base_image = ImageName.parse(base_image) self.original_base_image = self.original_base_image or self.base_image self.recreate_parent_images() if not self.base_from_scratch: self.parent_images[self.original_base_image] = self.base_image self.parents_pulled = parents_pulled self.base_image_insecure = insecure logger.info("set base image to '%s' with original base '%s'", self.base_image, self.original_base_image)
def test_hostdocker_build(caplog, source_params): if MOCK: mock_docker() image_name = ImageName(repo="atomic-reactor-test-ssh-image") remote_image = image_name.copy() remote_image.registry = LOCALHOST_REGISTRY m = DockerhostBuildManager( "buildroot-dh-fedora", { "source": source_params, "image": remote_image.to_str(), "parent_registry": LOCALHOST_REGISTRY, # faster "target_registries_insecure": True, "parent_registry_insecure": True, }) results = m.build() dt = DockerTasker() dt.pull_image(remote_image, insecure=True) assert len(results.build_logs) > 0 dt.remove_container(results.container_id) dt.remove_image(remote_image)
def test_replace_repo_schema_validation(self, site_replacements, exc_msg): image = ImageName.parse('a/x/foo') mock_package_mapping_files(site_replacements) mock_inspect_query(image, {}, times=0) site_config = get_site_config(repo_replacements=site_replacements) replacer = PullspecReplacer(user_config={}, site_config=site_config) with pytest.raises(jsonschema.ValidationError) as exc_info: replacer.replace_repo(image) assert exc_msg in str(exc_info.value)
def test_prebuild_plugin_failure(docker_tasker): # noqa workflow = DockerBuildWorkflow("test-image", source=SOURCE) setattr(workflow, 'builder', X()) setattr(workflow.builder, 'image_id', "asd123") setattr(workflow.builder, 'base_image', ImageName(repo='fedora', tag='21')) setattr(workflow.builder, "source", X()) setattr(workflow.builder.source, 'dockerfile_path', "/non/existent") setattr(workflow.builder.source, 'path', "/non/existent") runner = PreBuildPluginsRunner(docker_tasker, workflow, [{"name": AddYumRepoByUrlPlugin.key, "args": {'repourls': True}}]) with pytest.raises(PluginFailedException): runner.run() assert workflow.build_process_failed is True
def prepare(tmpdir): if MOCK: mock_docker() tasker = DockerTasker() workflow = DockerBuildWorkflow("test-image", source=SOURCE) setattr(workflow, 'builder', X()) source = MockSource(tmpdir) setattr(workflow.builder, 'image_id', "asd123") setattr(workflow.builder, 'base_image', ImageName(repo='Fedora', tag='21')) setattr(workflow.builder, 'source', source) setattr(workflow, 'source', source) return tasker, workflow
def prepare(): tasker = DockerTasker() workflow = DockerBuildWorkflow(SOURCE, "test-image") setattr(workflow, 'builder', X()) setattr(workflow.builder, 'image_id', "asd123") setattr(workflow.builder, 'base_image', ImageName(repo='Fedora', tag='21')) setattr(workflow.builder, 'source', X()) setattr(workflow.builder.source, 'dockerfile_path', None) setattr(workflow.builder.source, 'path', None) flexmock(koji, ClientSession=MockedClientSession, PathInfo=MockedPathInfo) return tasker, workflow
def test_parent_images_unresolved(tmpdir): """test when parent_images hasn't been filled in with unique tags.""" dfp = df_parser(str(tmpdir)) dfp.content = "FROM spam" workflow = mock_workflow() workflow.builder.set_df_path(dfp.dockerfile_path) workflow.builder.base_image = ImageName.parse('eggs') # we want to fail because some img besides base was not resolved workflow.builder.parent_images = {'spam': 'eggs', 'extra:image': None} with pytest.raises(ParentImageUnresolved): ChangeFromPlugin(docker_tasker(), workflow).run()
def test_hostdocker_build(caplog, source_params): if MOCK: mock_docker() image_name = ImageName(repo="atomic-reactor-test-ssh-image") remote_image = image_name.copy() remote_image.registry = LOCALHOST_REGISTRY m = DockerhostBuildManager( "buildroot-dh-fedora", { "source": source_params, "image": remote_image.to_str(), "parent_registry": LOCALHOST_REGISTRY, # faster "target_registries_insecure": True, "parent_registry_insecure": True, }) results = m.build() dt = DockerTasker() dt.pull_image(remote_image, insecure=True) if source_params['provider'] == 'path': assert_source_from_path_mounted_ok(caplog, m.temp_dir) assert len(results.build_logs) > 0 #assert re.search(r'build json mounted in container .+"uri": %s' % # os.path.join(dconstants.CONTAINER_SHARE_PATH, 'source')) # assert isinstance(results.built_img_inspect, dict) # assert len(results.built_img_inspect.items()) > 0 # assert isinstance(results.built_img_info, dict) # assert len(results.built_img_info.items()) > 0 # assert isinstance(results.base_img_info, dict) # assert len(results.base_img_info.items()) > 0 # assert len(results.base_plugins_output) > 0 # assert len(results.built_img_plugins_output) > 0 dt.remove_container(results.container_id) dt.remove_image(remote_image)
def get_component_name(self): try: labels = Labels(self.labels) _, name = labels.get_name_and_value(Labels.LABEL_TYPE_NAME) except KeyError: self.log.error('Unable to determine component from "Labels"') raise organization = get_registries_organization(self.workflow) if organization: image = ImageName.parse(name) image.enclose(organization) name = image.get_repo() return name
def test_rpmqa_plugin_exception(docker_tasker): # noqa workflow = DockerBuildWorkflow(SOURCE, "test-image") setattr(workflow, 'builder', X()) setattr(workflow.builder, 'image_id', "asd123") setattr(workflow.builder, 'base_image', ImageName(repo='fedora', tag='21')) setattr(workflow.builder, "source", X()) setattr(workflow.builder.source, 'dockerfile_path', "/non/existent") setattr(workflow.builder.source, 'path', "/non/existent") flexmock(docker.Client, logs=mock_logs_raise) runner = PostBuildPluginsRunner(docker_tasker, workflow, [{"name": PostBuildRPMqaPlugin.key, "args": {'image_id': TEST_IMAGE}}]) with pytest.raises(PluginFailedException): runner.run()
def _resolve_base_image(self, build_json): """If this is an auto-rebuild, adjust the base image to use the triggering build""" spec = build_json.get("spec") try: image_id = spec['triggeredBy'][0]['imageChangeBuild']['imageID'] except (TypeError, KeyError, IndexError): # build not marked for auto-rebuilds; use regular base image base_image = self.workflow.builder.base_image self.log.info("using %s as base image.", base_image) else: # build has auto-rebuilds enabled self.log.info("using %s from build spec[triggeredBy] as base image.", image_id) base_image = ImageName.parse(image_id) # any exceptions will propagate return base_image
def workflow_callback(workflow): workflow = self.prepare(workflow) (flexmock(atomic_reactor.util).should_receive( 'get_config_from_registry').and_raise( exception('', response=MockResponse())).once()) manifest_tag = 'registry.example.com' + '/' + BASE_IMAGE_W_SHA base_image_result = ImageName.parse(manifest_tag) manifest_image = base_image_result.copy() (flexmock(atomic_reactor.util).should_receive( 'get_manifest_list').with_args( image=manifest_image, registry=manifest_image.registry, insecure=True).and_return(None).once()) return workflow
def _mock_pull(repo, tag='latest', **kwargs): im = ImageName.parse(repo) if im.repo == 'library-only' and im.namespace != 'library': return iter(mock_pull_logs_failed) if tag and 'sha256' in tag: repotag = '%s@%s' % (repo, tag) else: repotag = '%s:%s' % (repo, tag) if _find_image(repotag) is None: new_image = mock_image.copy() new_image['RepoTags'] = [repotag] mock_images.append(new_image) return iter(mock_pull_logs)
def _replace(self, image, registry=_KEEP, namespace=_KEEP, repo=_KEEP, tag=_KEEP): """ Replace specified parts of image pullspec, keep the rest """ return ImageName( registry=image.registry if registry is _KEEP else registry, namespace=image.namespace if namespace is _KEEP else namespace, repo=image.repo if repo is _KEEP else repo, tag=image.tag if tag is _KEEP else tag, )
def test_get_manifest_digests_missing(tmpdir, v1_digest, v2_digest): kwargs = {} image = ImageName.parse('example.com/spam:latest') kwargs['image'] = image kwargs['registry'] = 'https://example.com' url = 'https://example.com/v2/spam/manifests/latest' def request_callback(request): media_type = request.headers['Accept'] media_type_prefix = media_type.split('+')[0] # If requested schema version is not available, attempt to # fallback to other version if possible to simulate how # a docker registry behaves if media_type.endswith('v2+json') and v2_digest: digest = 'v2-digest' elif media_type.endswith('v2+json') and v1_digest: digest = 'not-used' media_type_prefix = media_type_prefix.replace('v2', 'v1', 1) elif media_type.endswith('v1+json') and v1_digest: digest = 'v1-digest' elif media_type.endswith('v1+json') and v2_digest: digest = 'not-used' media_type_prefix = media_type_prefix.replace('v1', 'v2', 1) else: raise ValueError('Unexpected media type {}'.format(media_type)) headers = { 'Content-Type': '{}+jsonish'.format(media_type_prefix), 'Docker-Content-Digest': digest } return (200, headers, '') responses.add_callback(responses.GET, url, callback=request_callback) actual_digests = get_manifest_digests(**kwargs) if v1_digest: assert actual_digests.v1 == 'v1-digest' else: assert actual_digests.v1 is None if v2_digest: assert actual_digests.v2 == 'v2-digest' else: assert actual_digests.v2 is None
def test_ensure_primary(tmpdir, monkeypatch, osbs_error, tag_conf, annotations, tag_prefix, reactor_config_map): """ Test that primary image tags are ensured """ runner = prepare(tmpdir, primary_images_annotations=annotations, primary_images_tag_conf=tag_conf, reactor_config_map=reactor_config_map) monkeypatch.setenv("BUILD", json.dumps({"metadata": {}})) tags = [] primary_images = runner.workflow.tag_conf.primary_images if not primary_images: primary_images = [ ImageName.parse(primary) for primary in runner.workflow.build_result.annotations['repositories']['primary'] ] for primary_image in primary_images: tag = primary_image.tag if '-' in tag: continue tags.append(tag) (flexmock(OSBS).should_receive('get_image_stream').once().with_args( TEST_IMAGESTREAM).and_return(ImageStreamResponse())) # By using a combination of ordered and once, we verify that # ensure_image_stream_tag is not called with version-release tag for x in range(DEFAULT_TAGS_AMOUNT): expectation = ( flexmock(OSBS).should_receive('ensure_image_stream_tag').with_args( dict, tag_prefix + str(x)).once().ordered()) if osbs_error: expectation.and_raise(OsbsResponseException('None', 500)) (flexmock(OSBS).should_receive('import_image_tags').once().and_raise( AttributeError)) (flexmock(OSBS).should_receive('import_image').with_args( TEST_IMAGESTREAM, tags=tags).times(0 if osbs_error else 1).and_return(True)) if osbs_error: with pytest.raises(PluginFailedException): runner.run() else: runner.run()
def test_update_parent_images(organization, df_content, expected_df_content, base_from_scratch, tmpdir, reactor_config_map, docker_tasker): """test the happy path for updating multiple parents""" dfp = df_parser(str(tmpdir)) dfp.content = df_content # maps from dockerfile image to unique tag and then to ID first = ImageName.parse("first:parent") second = ImageName.parse("second:parent") monty = ImageName.parse("monty") custom = ImageName.parse("koji/image-build") build1 = ImageName.parse('build-name:1') build2 = ImageName.parse('build-name:2') build3 = ImageName.parse('build-name:3') if organization and reactor_config_map: first.enclose(organization) second.enclose(organization) monty.enclose(organization) pimgs = { first: build1, second: build2, monty: build3, custom: build3, } img_ids = { 'build-name:1': 'id:1', 'build-name:2': 'id:2', 'build-name:3': 'id:3', } workflow = mock_workflow() workflow.builder.set_df_path(dfp.dockerfile_path) workflow.builder.set_base_from_scratch(base_from_scratch) workflow.builder.base_image = ImageName.parse('build-name:3') workflow.builder.parent_images = pimgs workflow.builder.tasker.inspect_image = lambda img: dict(Id=img_ids[img]) for image_name, image_id in img_ids.items(): workflow.builder.set_parent_inspection_data(image_name, dict(Id=image_id)) original_base = workflow.builder.base_image run_plugin(workflow, reactor_config_map, docker_tasker, organization=organization) assert dfp.content == expected_df_content assert workflow.builder.original_df == df_content if base_from_scratch: assert original_base == workflow.builder.base_image
def get_pullspecs(self): """ Find pullspecs in predefined locations. :return: set of ImageName pullspecs """ named_pullspecs = self._named_pullspecs() pullspecs = set() for p in named_pullspecs: image = ImageName.parse(p.image) log.debug("%s - Found pullspec for %s: %s", self.path, p.description, image) pullspecs.add(image) return pullspecs
def replace_pullspecs(self, replacement_pullspecs): """ Replace pullspecs in predefined locations. :param replacement_pullspecs: mapping of pullspec -> replacement """ named_pullspecs = self._named_pullspecs() for p in named_pullspecs: old = ImageName.parse(p.image) new = replacement_pullspecs.get(old) if new is not None and old != new: log.debug("%s - Replaced pullspec for %s: %s -> %s", self.path, p.description, old, new) p.image = new.to_str() # `new` is an ImageName
def test_update_base_image_inspect_broken(tmpdir, caplog): """exercise code branch where the base image inspect comes back without an Id""" df_content = "FROM base:image" dfp = df_parser(str(tmpdir)) dfp.content = df_content workflow = mock_workflow() workflow.builder.set_df_path(dfp.dockerfile_path) workflow.builder.parent_images = {"base:image": "base@sha256:1234"} workflow.builder.base_image = ImageName.parse("base@sha256:1234") workflow.builder.tasker.inspect_image = lambda img: dict(no_id="here") with pytest.raises(NoIdInspection): ChangeFromPlugin(docker_tasker(), workflow).run() assert dfp.content == df_content # nothing changed assert "missing in inspection" in caplog.text()