Skip to content
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

add to layout replacedescriptor and removedescriptor #848

Merged
merged 1 commit into from
Dec 21, 2020

Conversation

deitch
Copy link
Collaborator

@deitch deitch commented Nov 29, 2020

Fixes #842

adds the following to v1/layout:

  • RemoveDescriptors(matcher match.Matcher) - removes from index.json any descriptors that match
  • ReplaceDescriptor(desc v1.Descriptor, matcher match.Matcher) - equivalent of RemoveDescriptors(matcher) followed by AppendDescriptor(desc)
  • ReplaceImage(image v1.Image, matcher match.Matcher) - equivalent of RemoveDescriptors(matcher) followed by AppendImage(image)
  • ReplaceIndex(index v1.ImageIndex, matcher match.Matcher) - equivalent of RemoveDescriptors(matcher) followed by AppendIndex(index)

None of these does garbage collection in the blobs dir, which is a completely separate topic, and should be handled under separate cover.

@deitch deitch mentioned this pull request Nov 29, 2020
@jonjohnsonjr
Copy link
Collaborator

I like Remove and Replace as primitives, but these should probably be part of the mutate package. Ideally, everything that does mutations is in the mutate package... but layout has a couple of exceptions already. The Append stuff was an oversight on my part that I wish I could walk back. The WriteFile etc. enable some things that would be otherwise impossible, so that was a concession of purity. For things like this PR (everything is generic except the source and the sink), the calling should look something like:

layout.Write(path, mutate.Remove(layout.FromPath(path), matcher)) and
layout.Write(path, mutate.AppendManifests(mutate.Remove(layout.FromPath(path), matcher), addendum)).

In the same way we have mutate.AppendManifests, we should have mutate.RemoveManifest[s]. You can just shove a Matcher in here and use that in compute().

If we get cute with type-casting, we could just have mutate.Remove that acts on both v1.Image and v1.ImageIndex, but we could also do mutate.RemoveManifests and mutate.RemoveLayers.

For your replace implementation, I would really expect:

Layout([A, B, C]).ReplaceImage(D, Matcher(B)) == Layout([A, D, C]) and also

Layout([A, A]).ReplaceImage(B, Matcher(A)) == Layout([B, B]),

but these are currently just equivalent to Append(Remove(Matcher(...), X).

If just Remove then Append is your desired behavior, how would you feel about adding Remove to mutate, then doing the remove && append in your client code? I'd be okay with a "replace" for mutate if it did in-place and not just remove + append.

@deitch
Copy link
Collaborator Author

deitch commented Nov 29, 2020

The Append stuff was an oversight on my part that I wish I could walk back.

Is it really? Given that Append*() does:

  1. Write the Image/Index contents to the layout dir
  2. Append the descriptor to the root index
  3. Write that index to the layout dir as an updated index.json

how would you place that in any other package?

If we get cute with type-casting, we could just have mutate.Remove that acts on both v1.Image and v1.ImageIndex, but we could also do mutate.RemoveManifests and mutate.RemoveLayers.

Please not, or at least not yet. The need for distinct Image and ImageIndex in various places is an issue throughout, as you raised. I would rather be consistent here with what we do elsewhere, and if we find a way to unify it, then do it everywhere at once.

For things like this PR (everything is generic except the source and the sink), the calling should look something like:

layout.Write(path, mutate.Remove(layout.FromPath(path), matcher)) and
layout.Write(path, mutate.AppendManifests(mutate.Remove(layout.FromPath(path), matcher), addendum)).

I really don't understand (which leaves me concerned others might not as well). I fully get that I can open a layout and get a Path and I can AppendImage or AppendIndex, etc.

The above is complex to understand, especially if I already have a Path, that I want to modify.

If just Remove then Append is your desired behavior, how would you feel about adding Remove to mutate, then doing the remove && append in your client code? I'd be okay with a "replace" for mutate if it did in-place and not just remove + append.

I have no objection to adding Remove and Replace to mutate, but then what goes in here that makes it easy to understand.

What do I, as a newbie user of this library, call to remove or replace a manifest from a Path?

With this PR - or a variant - I can do:

p, err := layout.FromPath(dir) 
// OR
p, err := layout.Write(dir, empty.Index)
// and then
p.ReplaceDescriptor(desc, someMatcher)

but now you are saying Replace*() goes in mutate, so I end up with:

p, err := layout.FromPath(dir)
// OR
p, err := layout.Write(dir, empty.Index)
// and then
err = p.WriteImage(img)
index, err := p.ImageIndex()
// generate the descriptor from img, redoing everything that is at https://github.com/google/go-containerregistry/blob/3c3abfb82ca884fb7cc6db0efe77264f1c817f1f/pkg/v1/layout/write.go#L42-L69
desc := generateDescriptorFromImage(img)
index, err = mutate.ReplaceDescriptor(desc, someMatcher)
err = p.WriteIndex(index) // this is a function that does not exist now

That is a lot messier and a lot more work than the original

If you say, you want to keep the conveniences in layout, just move some of the core logic into mutate, I can see that, but that will get messy with the Option as well.

@deitch
Copy link
Collaborator Author

deitch commented Nov 30, 2020

I think I figured out what you meant @jonjohnsonjr . Take a look at it now. All of the mutation is done in mutate, while the convenience functions wrap them in layout.

@codecov-io
Copy link

codecov-io commented Nov 30, 2020

Codecov Report

Merging #848 (f336189) into master (d29eb11) will decrease coverage by 0.08%.
The diff coverage is 56.86%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #848      +/-   ##
==========================================
- Coverage   74.33%   74.24%   -0.09%     
==========================================
  Files         107      107              
  Lines        4523     4598      +75     
==========================================
+ Hits         3362     3414      +52     
- Misses        661      675      +14     
- Partials      500      509       +9     
Impacted Files Coverage Δ
pkg/v1/layout/write.go 47.53% <44.44%> (-0.89%) ⬇️
pkg/v1/mutate/index.go 82.82% <83.33%> (+5.05%) ⬆️
pkg/v1/mutate/mutate.go 72.50% <100.00%> (+0.52%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d29eb11...3af4f8f. Read the comment docs.

@deitch
Copy link
Collaborator Author

deitch commented Dec 9, 2020

OK, we have #841 in, which makes using this easier. Any other comments on this?

return err
}

mt, err := img.MediaType()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Use partial.Descriptor here, for some bad reasons and some good reasons.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't understand

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sure, sorry I should have provided more details. It's two things:

  1. This is ~9 lines of code to produce a v1.Descriptor for an artifact, and we've consolidated that into a partial.Descriptor method, just for brevity.
  2. There are situations where you have some artifact, say a v1.Image, which was originally referenced by a v1.ImageIndex descriptor that contained platform information. If you just reconstruct the v1.Descriptor as you're doing here, we lose that information. Thus, artifacts can optionally implement Descriptor() (v1.Descriptor, error) to return the original v1.Descriptor by which they were referenced. This is especially interesting for foreign layers. See here and Add partial.Descriptor #654.

It unfortunately needs to live in partial because the way we wrap things in partial means we shadow the wrapped artifact, but fortunately it makes sense to live there anyway.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, I see that I bungled the multiline comment thing in GitHub. This comment should apply to lines 139-158.

pkg/v1/layout/write.go Outdated Show resolved Hide resolved
@jonjohnsonjr
Copy link
Collaborator

I really don't understand (which leaves me concerned others might not as well).

I agree this is a problem, which maybe we can solve with docs and examples, but the "model" for how this library works will be hard to change without a big, breaking overhaul.

think I figured out what you meant

I think we're roughly on the same page, but if there's anything from this comment that I still need to address, let me know.

@deitch
Copy link
Collaborator Author

deitch commented Dec 9, 2020

I think we're roughly on the same page

I didn't understand the three comments above. Can you clarify?

pkg/v1/layout/write.go Outdated Show resolved Hide resolved
@deitch deitch force-pushed the remove-replace-layout branch 2 times, most recently from 5b59c86 to 88adcf7 Compare December 9, 2020 18:53
@deitch
Copy link
Collaborator Author

deitch commented Dec 9, 2020

OK, now I think I got what you meant. See the latest.

Copy link
Collaborator

@jonjohnsonjr jonjohnsonjr left a comment

Choose a reason for hiding this comment

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

Nice, I think this is a lot cleaner -- with tests this would lgtm.

@deitch
Copy link
Collaborator Author

deitch commented Dec 14, 2020

OK, lots of tests. Back at you @jonjohnsonjr

@deitch deitch force-pushed the remove-replace-layout branch 2 times, most recently from 8d0a120 to 90c2d31 Compare December 14, 2020 18:43
@deitch deitch force-pushed the remove-replace-layout branch 2 times, most recently from 3e271bc to bc13ec6 Compare December 18, 2020 10:59
@deitch
Copy link
Collaborator Author

deitch commented Dec 18, 2020

And rebased

Copy link
Collaborator

@jonjohnsonjr jonjohnsonjr left a comment

Choose a reason for hiding this comment

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

Small suggestion to dedupe some code now that ReplaceDescriptor is removed, otherwise lgtm, feel free to push back if I'm missing something with my suggestion.

pkg/v1/layout/write.go Outdated Show resolved Hide resolved
pkg/v1/layout/write.go Outdated Show resolved Hide resolved
pkg/v1/layout/write.go Outdated Show resolved Hide resolved
pkg/v1/layout/write.go Outdated Show resolved Hide resolved
@deitch
Copy link
Collaborator Author

deitch commented Dec 19, 2020

Oh, yeah, I like that. Nice and clean.

@deitch
Copy link
Collaborator Author

deitch commented Dec 19, 2020

Updated with your suggestions @jonjohnsonjr ; back at you.

@@ -92,6 +93,26 @@ func AppendManifests(base v1.ImageIndex, adds ...IndexAddendum) v1.ImageIndex {
}
}

// RemoveManifests removes any descriptors that match the match.Matcher.
func RemoveManifests(base v1.ImageIndex, matcher match.Matcher) (v1.ImageIndex, error) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Okay, fine, one last thing before I merge this. Sorry... thank you for your patience.

We could defer this to be a little bit lazier (happens in compute instead of here) so that we don't return an error here, which would make using it a little easier (one less error check, matching AppendManifests).

This would allow for:

	ii = mutate.AppendManifests(mutate.RemoveManifests(ii, matcher), add)

Not a huge deal, but something I often find frustrating when using this library is that I am constantly checking for errors for things that don't really need to return an error, and I wish I could go back and fix that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I understand what you are trying to do. If this only returns a v1.ImageIndex and not (v1.ImageIndex, error), then you have the option to chain them.

I just don't see how.

The source for this is:

// RemoveManifests removes any descriptors that match the match.Matcher.
func RemoveManifests(base v1.ImageIndex, matcher match.Matcher) (v1.ImageIndex, error) {
	ii, err := base.IndexManifest()
	if err != nil {
		return base, err
	}

	// create a list without any existing ones
	manifests := []v1.Descriptor{}
	for _, manifest := range ii.Manifests {
		if !matcher(manifest) {
			manifests = append(manifests, manifest)
		}
	}
	return &index{
		base:     base,
		replaces: manifests,
	}, nil
}

The only potential error is from getting base.IndexManifest(), but it could return an error. How would we handle it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

but it could return an error. How would we handle it?

Drop index.replaces, for an index.removeMatcher and set it to the match.Matcher we pass in, then instead of doing:

        manifests := manifest.Manifests
	if len(i.replaces) > 0 {
		manifests = i.replaces
	}

in index.compute, do:

	manifests := []v1.Descriptor
        for _, desc := range manifest.Manifests {
            if !i.removeMatcher(desc) {
                manifests = append(manifests, desc)
            }
        }

We're basically just deferring that IndexManifest() call until compute() is called.

RemoveManifests just becomes:

// RemoveManifests removes any descriptors that match the match.Matcher.
func RemoveManifests(base v1.ImageIndex, matcher match.Matcher) v1.ImageIndex {
	return &index{
		base:     base,
		removeMatcher: matcher,
	}
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think I got what you had in mind. I added it as another commit, so I can roll back if that is not it. Take a look and comment here. If it is, I will squash the two commits.

@@ -90,6 +95,20 @@ func (i *index) compute() error {
}
manifest := m.DeepCopy()
manifests := manifest.Manifests
if len(i.replaces) > 0 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is just unused now, right? We can drop index.replaces.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We can, but having figured it out how to work it, maybe we should just leave it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Having an unreachable/untested path is a bit strange to me, I'd just drop it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

OK done.

// replaces are applied before adds
replaces []v1.Descriptor
// removes are removed after replaces and before adds
removes []match.Matcher
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think this needs to be a list (yet?). In theory, we may be able to optimize some things in the future to do introspection and directly modify these fields instead of returning an entirely new mutate.index each time, but right now this can only be a single matcher, I think.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It probably can, but it parallels adds, for a clean API, and I made it similarly variadic. Let's just leave it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Eh... I'm not sure.

I do like that this mirrors AddManifests, but I think making this variadic is going to be a source of confusion. There's more than one way to compose matchers, and this interface forces the operator to only ever be "or".

You can achieve the same thing now with multiple calls to RemoveManifests, which is definitely less efficient, but you can also just implement a match.Matcher that matches multiple predicates.

Having RemoveManifests accept multiple matchers encodes some matcher composition within mutate, and I'd rather that live in match if we need it.

Of course, you know that I will advocate for adding match.Or and match.And to solve this, but we're not there yet :P

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

OK done. And rebased.

Signed-off-by: Avi Deitcher <avi@deitcher.net>
Copy link
Collaborator

@jonjohnsonjr jonjohnsonjr left a comment

Choose a reason for hiding this comment

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

Thanks :)

@jonjohnsonjr jonjohnsonjr merged commit 39d4b23 into google:master Dec 21, 2020
@deitch deitch deleted the remove-replace-layout branch December 22, 2020 08:11
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.

layout.RemoveDescriptor
3 participants