Implement group=True for group_manifests plugin#778
Implement group=True for group_manifests plugin#778lcarva merged 4 commits intocontainerbuildsystem:masterfrom
Conversation
|
I just spotted this: estesp/manifest-tool#32 Since manifest-tool-0.6.0 you can add a list of additional tags! So we'd just need a single call to manifest-tool. |
| runner = PostBuildPluginsRunner(tasker, workflow, plugins_conf) | ||
| if valid: | ||
| result = runner.run() | ||
| test_results = [] |
There was a problem hiding this comment.
Make this (and expected_results) a set() and use .add() instead of .append(), so that the order of the elements is not significant.
atomic_reactor/util.py
Outdated
| """Wrapper for digests for a docker manifest.""" | ||
|
|
||
| def __init__(self, v1=None, v2=None): | ||
| def __init__(self, v1=None, v2=None, list=None): |
There was a problem hiding this comment.
This shadows the built-in type list. Maybe v2_list or something?
Should the default property prefer the manifest list digest to either the schema 2 or schema 1 image manifest digest? I think it should.
| push_conf_registry = self.workflow.push_conf.add_docker_registry(registry, | ||
| insecure=insecure) | ||
| push_conf_registry.config = get_config_from_registry(registry_image, registry, None, | ||
| insecure, secret_path, 'v2') |
There was a problem hiding this comment.
This will fetch the 'config' blob for the architecture=amd64 image. Are we only fetching 'config' because of the way push_conf works?
The only thing atomic-reactor does with 'config' is store an edited version of it in the Koji build metadata, and the worker builds will already have taken care of that.
There was a problem hiding this comment.
If we don't need the config, I am happy to not get it. I was just copying the code from post_tag_and_push but looking at the workflow, it doesn't look like anyone requires a config for the docker registries.
f9fea7f to
1933f1c
Compare
|
|
||
| image_tags = [] | ||
| for image in self.workflow.tag_conf.images: | ||
| image_tags.append(image.to_str(registry=False)) |
There was a problem hiding this comment.
We want just the suffix part here I think, not the repository name. Also, we only need to set additional tags other than the one used in 'image'.
registry_image = self.workflow.tag_conf.images[0].copy()
image_tags = [image.tag for image in self.workflow.tag_conf.images[1:]]
atomic_reactor/util.py
Outdated
|
|
||
|
|
||
| def get_manifest_media_type(version): | ||
| if version == 'list': |
There was a problem hiding this comment.
Maybe use 'v2_list' throughout, instead of 'list'? There's another type in the works as well, 'oci', so at some point we might need to define some constants to hold these names.
1933f1c to
423eebe
Compare
| """ | ||
| # call parent constructor | ||
| super(GroupManifestsPlugin, self).__init__(tasker, workflow) | ||
| self.group = True # group |
There was a problem hiding this comment.
E261 at least two spaces before inline comment
| def submit_manifest_list(self, registry, registry_conf, manifest_list_spec): | ||
| docker_secret_path = registry_conf.get('secret', None) | ||
| # dockercfg = "{0}/.dockercfg".format(registry_conf.get('secret', None)) | ||
|
|
There was a problem hiding this comment.
W293 blank line contains whitespace
| import json | ||
| import responses | ||
| from copy import deepcopy | ||
| from tempfile import mkdtemp |
There was a problem hiding this comment.
F401 'tempfile.mkdtemp' imported but unused
| import responses | ||
| from copy import deepcopy | ||
| from tempfile import mkdtemp | ||
| import os |
|
|
||
| class TestGroupManifests(object): | ||
| @pytest.mark.parametrize(('goarch', 'worker_annotations', 'valid'), [ | ||
| # ({}, {}, False), |
There was a problem hiding this comment.
E122 continuation line missing indentation or outdented
| # ({}, {}, False), | ||
| ({'ppc64le': 'powerpc', 'x86_64': 'amd64'}, | ||
| {'ppc64le': PPC_ANNOTATIONS, 'x86_64': X86_ANNOTATIONS}, True), | ||
| # ({'ppc64le': 'powerpc', 'x86_64': 'amd64'}, |
There was a problem hiding this comment.
E122 continuation line missing indentation or outdented
| ({'ppc64le': 'powerpc', 'x86_64': 'amd64'}, | ||
| {'ppc64le': PPC_ANNOTATIONS, 'x86_64': X86_ANNOTATIONS}, True), | ||
| # ({'ppc64le': 'powerpc', 'x86_64': 'amd64'}, | ||
| # {'ppc64le': PPC_ANNOTATIONS, 'x86_64': X86_ANNOTATIONS}, False), |
There was a problem hiding this comment.
E122 continuation line missing indentation or outdented
| if valid: | ||
| repo_and_tag = workflow.tag_conf.images[0].to_str(registry=False).split(':') | ||
| url = 'http://{0}/v2/{1}/manifests/{2}'.format(registry, repo_and_tag[0], | ||
| repo_and_tag[1]) |
There was a problem hiding this comment.
E127 continuation line over-indented for visual indent
twaugh
left a comment
There was a problem hiding this comment.
Don't forget to update koji_import to make it add the new image.index metadata when a manifest list was created.
|
|
||
| cmd = ['manifest-tool', '--docker-cfg=%s' % docker_secret_path, | ||
| 'push', 'from-spec', fp.name] | ||
| subprocess.check_call(cmd, env={'HOME': docker_secret_path}) |
There was a problem hiding this comment.
Could add a comment explaining that docker/cli (used by manifest-tool) only reads .dockercfg from $HOME.
| fp.flush() | ||
| self.log.debug("Wrote to file %s with config %s", fp.name, docker_secret_path) | ||
|
|
||
| cmd = ['manifest-tool', '--docker-cfg=%s' % docker_secret_path, |
There was a problem hiding this comment.
Do we think --docker-cfg is useful here at all, given how docker/cli behaves with .dockercfg?
It seems to me that only the newer-style 'config.json' file is read from the --docker-cfg location.
| """ | ||
| # call parent constructor | ||
| super(GroupManifestsPlugin, self).__init__(tasker, workflow) | ||
| self.group = True # group |
There was a problem hiding this comment.
Let's not hard-code it to True. :-)
| manifest_list_spec['manifests'].append(arch_entry) | ||
|
|
||
| manifest_list_spec['tags'] = [image.tag for image in self.workflow.tag_conf.images] | ||
| # use a unique image tag because manifest_list doesn't like digests |
There was a problem hiding this comment.
It's manifest-tool that doesn't like digests. I still don't understand why.
There was a problem hiding this comment.
Ah... we need to use a tag for the output we want manifest-tool to create because the output doesn't exist yet, so we can't very well know its digest.
| registry_image = self.workflow.tag_conf.unique_images[0] | ||
| registry_image.registry = registry | ||
| manifest_list_spec['image'] = registry_image.to_str() | ||
| self.log.info("Submitting manifest-list %s", manifest_list_spec) |
There was a problem hiding this comment.
The manifest-tool documentation calls this input file a 'spec'. Let's call it that in the log message too, to avoid confusion with the actual 'manifest list' that manifest-tool creates as its output.
| digest = worker_image['digest'] | ||
| repository = worker_image['repository'] | ||
| arch_entry = { | ||
| 'image': '{0}/{1}@{2}'.format(registry, repository, digest), |
There was a problem hiding this comment.
I think we need to use the unique tag instead of the digest here, and move the comment about manifest-tool not liking digests to here.
89c6e8d to
69830bc
Compare
| repo = ImageName.parse(repositories[0]).repo | ||
| # group_manifests added the registry, so this should be valid | ||
| for registry in self.workflow.push_conf.docker_registries: | ||
| pullspec = "{0}/{1}@{2}".format(registry.uri, repo, manifest_list_digests[0].v2_list) |
There was a problem hiding this comment.
E501 line too long (101 > 100 characters)
tests/plugins/test_koji_import.py
Outdated
| @pytest.mark.parametrize('digests', [ | ||
| [], | ||
| [{'tag': 'testtag', 'schemaVersion': '2'}], | ||
| [ManifestDigest(v2_list='sha256:e6593f3e14000439dc62829a50945221c612e47cf5aa85a101677f7b76cf5811')], |
There was a problem hiding this comment.
E501 line too long (108 > 100 characters)
77ab82e to
0d9f03e
Compare
| def set_manifest_list_info(self, extra, worker_metadatas): | ||
| manifest_list_digests = self.workflow.postbuild_results.get(PLUGIN_GROUP_MANIFESTS_KEY) | ||
| # group_manifests returns either a list of manifest digests of the manifest lists, | ||
| # or a list of the image_manifests |
There was a problem hiding this comment.
By the way, can we fix this? I think it should always return a list of manifest list digests, and that this list should be empty it didn't create any manifest list (i.e. when group=False). That way the test on the following line can be much simpler: if manifest_list_digests:.
There was a problem hiding this comment.
Let's remove this comment as it doesn't appear to be relevant anymore.
| index['pull'] = [pullspec] | ||
| for tag in index['tags']: | ||
| pullspec = "{0}/{1}:{2}".format(registry.uri, repo, tag) | ||
| index['pull'].append(pullspec) |
There was a problem hiding this comment.
We must not put "floating" tags here. Ideally we should put the {version}-{release} tag here but no other tags, so it looks like:
pull: [ "registry.example.com/foo@sha256:...", "registry.example.com/foo:1.0-1" ]
tags: [ "1.0-1", "latest", ... ]
One way to arrange this is to use the knowledge that only the {version}-{release} tag may contain a dash character:
...
index['pull'] = [pullspec]
for tag in index['tags']:
if '-' in tag: # {version}-{release}
index['pull'].append("{0}/{1}:{2}".format(registry.uri, repo, tag))
break
| # --docker-cfg may be rendundant here, but it's how the tool should work | ||
| cmd = ['manifest-tool', '--docker-cfg=%s' % docker_secret_path, | ||
| 'push', 'from-spec', fp.name] | ||
| # docker always looks in $HOME for the .docker_cfg, so set $HOME to the path |
| manifest_list_spec['manifests'].append(arch_entry) | ||
|
|
||
| manifest_list_spec['tags'] = [image.tag for image in self.workflow.tag_conf.images] | ||
| # use a unique image tag because manifest_list can't accept a digest that |
There was a problem hiding this comment.
Nit: typo: "... because manifest-tool can't... "
| registry_image = self.workflow.tag_conf.unique_images[0] | ||
| registry_image.registry = registry | ||
| manifest_list_spec['image'] = registry_image.to_str() | ||
| self.log.info("Submitting manifest-list %s", manifest_list_spec) |
There was a problem hiding this comment.
Can we say "spec" in this log message? Submitting manifest-list spec %s
| repositories = self.workflow.build_result.annotations['repositories']['unique'] | ||
| repo = ImageName.parse(repositories[0]).repo | ||
| # group_manifests added the registry, so this should be valid | ||
| for registry in self.workflow.push_conf.docker_registries: |
There was a problem hiding this comment.
If Pulp is used, we only want to include the Pulp registry. Otherwise, we want to include all registries we pushed to. So it should be:
registries = self.workflow.push_conf.pulp_registries
if not registries:
registries = self.workflow.push_conf.all_registries
for registry in registry:
|
|
||
| def get_grouped_manifests(self): | ||
| grouped_manifests = [] | ||
| for registry, registry_conf in self.registries.items(): |
There was a problem hiding this comment.
Skip registries where registry.get('version') == 'v1'
|
|
||
| manifest_list_spec['tags'] = [image.tag for image in self.workflow.tag_conf.images] | ||
| # use a unique image tag because manifest_list can't accept a digest that | ||
| # isn't in the respoistory yet |
| expected_results.add('v2_list-digest:{0}'.format(url)) | ||
| for image in workflow.tag_conf.images: | ||
| repo_and_tag = image.to_str(registry=False).split(':') | ||
| path = '/v2/{0}/manifests/{1}'.format(repo_and_tag[0], repo_and_tag[1]) |
There was a problem hiding this comment.
E501 line too long (87 > 79 characters)
| responses.add(responses.GET, https_url, body=ConnectionError()) | ||
| url = 'http://' + registry + path | ||
| if valid: | ||
| responses.add_callback(responses.GET, url, callback=request_callback) |
There was a problem hiding this comment.
E501 line too long (89 > 79 characters)
| from atomic_reactor.source import GitSource, PathSource | ||
| from atomic_reactor.build import BuildResult | ||
| from atomic_reactor.constants import PLUGIN_PULP_PULL_KEY, PLUGIN_PULP_SYNC_KEY | ||
| from atomic_reactor.constants import (PLUGIN_PULP_PULL_KEY, PLUGIN_PULP_SYNC_KEY, |
There was a problem hiding this comment.
E501 line too long (81 > 79 characters)
6109db3 to
7b7d7f0
Compare
| session=session, | ||
| docker_registry=True) | ||
| workflow.postbuild_results[PLUGIN_GROUP_MANIFESTS_KEY] = digests | ||
| orchestrate_plugin = workflow.plugin_workspace[OrchestrateBuildPlugin.key] |
There was a problem hiding this comment.
E501 line too long (82 > 79 characters)
| if digests: | ||
| assert 'index' in image.keys() | ||
| expected_results = {} | ||
| expected_results['tags'] = [tag.tag for tag in workflow.tag_conf.images] |
There was a problem hiding this comment.
E501 line too long (84 > 79 characters)
tests/plugins/test_koji_import.py
Outdated
| assert 'index' in image.keys() | ||
| expected_results = {} | ||
| expected_results['tags'] = [tag.tag for tag in workflow.tag_conf.images] | ||
| pullspec = "docker.example.com/hello-world@{0}".format(digests[0].v2_list) |
There was a problem hiding this comment.
E501 line too long (86 > 79 characters)
| try: | ||
| subprocess.check_call(cmd) | ||
| except CalledProcessError as exc: | ||
| self.log.error("manifest-tool failed with %s", exc.output) |
There was a problem hiding this comment.
check_output(cmd) must be used to have output in exception, check_call won't cut it
7b7d7f0 to
aff2dae
Compare
| {'ppc64le': PPC_ANNOTATIONS, 'x86_64': X86_ANNOTATIONS}, False), | ||
| ]) | ||
| @responses.activate # noqa | ||
| def test_group_manifests_true(self, tmpdir, goarch, worker_annotations, valid): |
There was a problem hiding this comment.
E501 line too long (83 > 79 characters)
| }, | ||
| }] | ||
| tasker, workflow = mock_environment(tmpdir, primary_images=test_images, | ||
| worker_annotations=worker_annotations) |
There was a problem hiding this comment.
E501 line too long (82 > 79 characters)
| mock_docker() | ||
|
|
||
| goarch = {'ppc64le': 'powerpc', 'x86_64': 'amd64'} | ||
| worker_annotations = {'ppc64le': PPC_ANNOTATIONS, 'x86_64': X86_ANNOTATIONS} |
There was a problem hiding this comment.
E501 line too long (84 > 79 characters)
aff2dae to
1ec1d2d
Compare
| 'push', 'from-spec', fp.name] | ||
| # docker always looks in $HOME for the .dockercfg, so set $HOME to the path | ||
| try: | ||
| check_output(cmd, stderr=STDOUT, env={'HOME': docker_secret_path}) |
There was a problem hiding this comment.
E501 line too long (82 > 79 characters)
…t list get_manifest_digests now supports v1, v2, and list.v2 (as v2_list). Signed-off-by: Mark Langsdorf <mlangsdo@redhat.com>
1ec1d2d to
2862afc
Compare
| check_output(cmd, stderr=STDOUT, env={'HOME': docker_secret_path}) | ||
| except CalledProcessError as exc: | ||
| self.log.error("manifest-tool failed with %s", exc.output) | ||
| raise RuntimeError |
There was a problem hiding this comment.
How about just re-raising the previous exception? (raise statement with no exception class)
As it is, the exception that gets propagated contains no useful information.
2862afc to
9bd0795
Compare
tests/plugins/test_koji_import.py
Outdated
| assert 'index' in image.keys() | ||
| expected_results = {} | ||
| expected_results['tags'] = [tag.tag for tag in workflow.tag_conf.images] | ||
| pullspec = "docker.example.com/hello-world@{0}".format(digests[0].v2_list) |
There was a problem hiding this comment.
This is incorrect! It should be "docker.example.com/myproject/hello-world@{0}". Without the namespace part included, the image is not pullable.
| index = {} | ||
| index['tags'] = [image.tag for image in self.workflow.tag_conf.images] | ||
| repositories = self.workflow.build_result.annotations['repositories']['unique'] | ||
| repo = ImageName.parse(repositories[0]).repo |
There was a problem hiding this comment.
This should be: repo = ImageName.parse(repositories[0]).to_str(registry=False, tag=False).
9bd0795 to
3cdd337
Compare
| test_results = [{'tag': 'testtag', 'schemaVersion': '2'}] | ||
| test_images = ['registry.example.com/namespace/httpd:2.4', | ||
| 'registry.example.com/namespace/httpd:latest'] | ||
| # test_results = [{'tag': 'testtag', 'schemaVersion': '2'}] |
when group=True, group_manifest is meant to create a manifest list. It loops through all the platforms in the worker annotations and creates a manifest_list_spec, using that platform's worker image to create the image entry and the goarch information to create the platform information. The manifest_list_spec is then repeatedly retagged with the tags from tag_conf.images, written to a yaml file, and pushed via manifest-list. After all the images are tagged, the last image is used to create a manifest digest of the manifest list. The manifest digest is added to push_conf under the registry's name and the image's tag, so that delete_from_registry can delete the manifest list later. A list of the manifest digest from each registry is returned. Signed-off-by: Mark Langsdorf <mlangsdo@redhat.com>
If group_manifests created a manifest list, koji_import should create a dict in 'extras': 'index' that contains the pullspec and tags for the manifest list. Have koji_import check the return of group_manifests, and if it got a list of manifest digests, have it set up the 'extras': 'index' dictionary. Signed-off-by: Mark Langsdorf <mlangsdo@redhat.com>
If group_manifests creates a manifest list, it stores the registry and tag in push_conf. If handle_worker_digests works correctly, we need to check to see if there is a push_conf registry. If there isn't, there's no need to post a warning. Signed-off-by: Mark Langsdorf <mlangsdo@redhat.com>
3cdd337 to
9326aac
Compare
|
Updated release notes. |
Add support for creating manifest lists. Includes four commits:
and
and
and