-
Notifications
You must be signed in to change notification settings - Fork 62
Implement group=True for group_manifests plugin #778
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement group=True for group_manifests plugin #778
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make this (and expected_results) a set() and use .add() instead of .append(), so that the order of the elements is not significant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
W293 blank line contains whitespace
| import json | ||
| import responses | ||
| from copy import deepcopy | ||
| from tempfile import mkdtemp |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
F401 'tempfile.mkdtemp' imported but unused
| import responses | ||
| from copy import deepcopy | ||
| from tempfile import mkdtemp | ||
| import os |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
F401 'os' imported but unused
|
|
||
| class TestGroupManifests(object): | ||
| @pytest.mark.parametrize(('goarch', 'worker_annotations', 'valid'), [ | ||
| # ({}, {}, False), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
E127 continuation line over-indented for visual indent
twaugh
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's manifest-tool that doesn't like digests. I still don't understand why.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed.
| # --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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: typo: ".dockercfg"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed.
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: typo: "... because manifest-tool can't... "
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed.
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we say "spec" in this log message? Submitting manifest-list spec %s
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed.
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed.
|
|
||
| def get_grouped_manifests(self): | ||
| grouped_manifests = [] | ||
| for registry, registry_conf in self.registries.items(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Skip registries where registry.get('version') == 'v1'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed.
|
|
||
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: typo: repository
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed.
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
check_output(cmd) must be used to have output in exception, check_call won't cut it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is incorrect! It should be "docker.example.com/myproject/hello-world@{0}". Without the namespace part included, the image is not pullable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
| 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be: repo = ImageName.parse(repositories[0]).to_str(registry=False, tag=False).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
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'}] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
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