Skip to content

Conversation

@mlangsdorf
Copy link
Contributor

@mlangsdorf mlangsdorf commented Aug 17, 2017

Add support for creating manifest lists. Includes four commits:

utils: allow get_manifest_digests() to return a digest from a manifest list

get_manifest_digests now supports v1, v2, and list.v2.

and

post_group_manifests: implement creation of the manifest list

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.

and

group_manifests: add koji_import support for manifest lists

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.

and

delete_from_registry: delete manifest lists if they exist

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.

@twaugh twaugh self-requested a review August 18, 2017 09:21
@twaugh
Copy link
Member

twaugh commented Aug 18, 2017

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 = []
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

"""Wrapper for digests for a docker manifest."""

def __init__(self, v1=None, v2=None):
def __init__(self, v1=None, v2=None, list=None):
Copy link
Member

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.

Copy link
Contributor Author

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')
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@mlangsdorf mlangsdorf force-pushed the group_manifests_true branch 2 times, most recently from f9fea7f to 1933f1c Compare August 18, 2017 14:15

image_tags = []
for image in self.workflow.tag_conf.images:
image_tags.append(image.to_str(registry=False))
Copy link
Member

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:]]



def get_manifest_media_type(version):
if version == 'list':
Copy link
Member

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.

"""
# call parent constructor
super(GroupManifestsPlugin, self).__init__(tasker, workflow)
self.group = True # group

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))

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

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

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),

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'},

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),

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])

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

Copy link
Member

@twaugh twaugh left a 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})
Copy link
Member

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,
Copy link
Member

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
Copy link
Member

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
Copy link
Member

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.

Copy link
Member

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)
Copy link
Member

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),
Copy link
Member

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.

@mlangsdorf mlangsdorf force-pushed the group_manifests_true branch 3 times, most recently from 89c6e8d to 69830bc Compare August 22, 2017 11:52
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)

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)

@pytest.mark.parametrize('digests', [
[],
[{'tag': 'testtag', 'schemaVersion': '2'}],
[ManifestDigest(v2_list='sha256:e6593f3e14000439dc62829a50945221c612e47cf5aa85a101677f7b76cf5811')],

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)

@mlangsdorf mlangsdorf force-pushed the group_manifests_true branch 2 times, most recently from 77ab82e to 0d9f03e Compare August 22, 2017 20:30
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
Copy link
Member

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:.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed.

Copy link
Contributor

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)
Copy link
Member

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

Copy link
Contributor Author

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: typo: ".dockercfg"

Copy link
Contributor Author

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
Copy link
Member

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... "

Copy link
Contributor Author

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)
Copy link
Member

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

Copy link
Contributor Author

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:
Copy link
Member

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:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed.

@twaugh twaugh changed the title Group manifests true Implement group=True for group_manifests plugin Aug 24, 2017

def get_grouped_manifests(self):
grouped_manifests = []
for registry, registry_conf in self.registries.items():
Copy link
Member

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'

Copy link
Contributor Author

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: typo: repository

Copy link
Contributor Author

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])

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)

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,

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)

@mlangsdorf mlangsdorf force-pushed the group_manifests_true branch from 6109db3 to 7b7d7f0 Compare August 25, 2017 12:47
session=session,
docker_registry=True)
workflow.postbuild_results[PLUGIN_GROUP_MANIFESTS_KEY] = digests
orchestrate_plugin = workflow.plugin_workspace[OrchestrateBuildPlugin.key]

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]

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)

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)

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)
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@mlangsdorf mlangsdorf force-pushed the group_manifests_true branch from 7b7d7f0 to aff2dae Compare August 25, 2017 13:26
{'ppc64le': PPC_ANNOTATIONS, 'x86_64': X86_ANNOTATIONS}, False),
])
@responses.activate # noqa
def test_group_manifests_true(self, tmpdir, goarch, worker_annotations, valid):

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)

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}

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)

@mlangsdorf mlangsdorf force-pushed the group_manifests_true branch from aff2dae to 1ec1d2d Compare August 25, 2017 13:33
'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})

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>
@mlangsdorf mlangsdorf force-pushed the group_manifests_true branch from 1ec1d2d to 2862afc Compare August 25, 2017 14:23
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
Copy link
Contributor

@lcarva lcarva Aug 25, 2017

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@mlangsdorf mlangsdorf force-pushed the group_manifests_true branch from 2862afc to 9bd0795 Compare August 25, 2017 15:09
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)
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@mlangsdorf mlangsdorf force-pushed the group_manifests_true branch from 9bd0795 to 3cdd337 Compare August 25, 2017 17:43
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'}]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed?

Copy link
Contributor Author

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>
@mlangsdorf mlangsdorf force-pushed the group_manifests_true branch from 3cdd337 to 9326aac Compare August 28, 2017 12:22
@lcarva lcarva merged commit 2d7ea16 into containerbuildsystem:master Aug 28, 2017
@lcarva
Copy link
Contributor

lcarva commented Aug 28, 2017

Updated release notes.

@mlangsdorf mlangsdorf deleted the group_manifests_true branch August 31, 2017 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants