-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Check for existence of layers in cache before returning cached base image #3767
Conversation
…ing for layer directory presence
@emmileaf Took a first look at the changes! To make sure that I'm understanding the two approaches correctly:
Both approaches are valid but I'm a little concerned about the performance overhead approach 2 might add especially since the change will result in the cached layer being retrieved twice. Approach 1 might be a sufficient check. In terms of readability it also makes it more explicit that we are checking for the presence of the layer. Catching exceptions also works but there is usually a risk involved with it if we're not as specific as possible since methods can throw an exception for a number of reasons and it is usually recommended to let exceptions propagate to the caller or be handled by surrounding frameworks (testing frameworks, for example). Lmk what you think. |
@mpeddada1 Thanks for the review! Your understanding of the two approaches is spot on. I also agree with the concerns here around approach 2’s redundant work overhead and relying on exceptions. The downside of approach 1's check I was trying to work around is that, Maybe there is some way to make this check more rigorous without fully retrieving the layer? I'll revert the last commit to switch back to approach 1, and see if I can make any improvements from there. |
…st checking for layer directory presence" This reverts commit b231d79.
Just realized this statement isn't true and I had misunderstood this cache write logic earlier - it looks like a temp folder is created for writing the layer first, and then the contents get moved into |
Ah |
jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java
Outdated
Show resolved
Hide resolved
* @param manifest the image manifest | ||
* @return a boolean | ||
*/ | ||
boolean allLayersCached(ManifestTemplate manifest) { |
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.
Would it possible to move this method directly to PullBaseImageStep? Since that is the only class where this is used and it could help us avoid having to go through an extra layer of classes to reach this method. Or does this method require any variables that are specific to CacheStorageReader
?
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.
Yeah, I was thinking about this too - this method goes through a few layers because it depends on CacheStorageReader
’s cacheStorageFiles
variable here.
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.
Got it, thanks! Hm it looks like Cache takes in cacheStorageFiles
as a parameter to it's constructor, could we use that maybe?
private Cache(CacheStorageFiles cacheStorageFiles) { |
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 you’re right - I missed that we could just add cacheStorageFiles
as a field and move this logic to Cache
.
I went ahead and tried to make this change, but noticed that the Cache
class is set up in a way that many of its methods are wrappers around calls to either CacheStorageReader
or CacheStorageWriter
. Looking at the existing test suites, CacheStorageReaderTest
is also more straightforward to add to than CacheTest
for unit testing the new check.
I am tempted to leave this logic in CacheStorageReader
just to stay consistent with the existing setup here - lmk what you think!
} else { | ||
throw new IllegalArgumentException("Unknown manifest type: " + manifest); | ||
} |
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 wonder if this would break current behavior for those doing multi-platform image building? For context #2730, we also do caching for manifest lists in addition to single 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.
That’s a good callout, and please let me know if I'm misunderstanding anything here!
Right now the added logic in PullBaseImageStep.getCachedBaseImages()
never passes anything of V22ManifestList
type into areAllLayersCached()
calls explicitly. In the case of manifest lists, this check is made individually when looping over the platform-specific manifests, and returns an overall cache miss if any of the platform-specific manifests has incomplete layers.
But, do you think there is value here to have CacheStorageReader.areAllLayersCached()
itself handle the manifest list type, rather than rely on the code calling it? Perhaps instead of throwing an exception here, it can also just return false, and leave the rest of the behavior to existing logic?
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 thanks for the detailed explanation! You're right, it is called for the individual manifests in the manifest list.
But, do you think there is value here to have
CacheStorageReader.areAllLayersCached()
itself handle the manifest list type, rather than rely on the code calling it?
Hm that's a good question. I was initially thinking about this too but looking at the code for getCachedBaseImage
, we would probably still have to iterate through the manifests in the manifest list again to retrieve the collection of images? What you have currently (which a more fail-fast approach) seems like a better choice.
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.
Sounds good!
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.
Thanks for the thorough explanations. We're getting close!
} else { | ||
throw new IllegalArgumentException("Unknown manifest type: " + manifest); | ||
} |
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 thanks for the detailed explanation! You're right, it is called for the individual manifests in the manifest list.
But, do you think there is value here to have
CacheStorageReader.areAllLayersCached()
itself handle the manifest list type, rather than rely on the code calling it?
Hm that's a good question. I was initially thinking about this too but looking at the code for getCachedBaseImage
, we would probably still have to iterate through the manifests in the manifest list again to retrieve the collection of images? What you have currently (which a more fail-fast approach) seems like a better choice.
if (!baseImageLayersCache.allLayersCached(Verify.verifyNotNull(manifest))) { | ||
return Collections.emptyList(); | ||
} |
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 we move the Verify.verifyNotNull
condition to allLayersCached
?
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.
Moved this to a line above, since this condition is applied again to the manifest in toImage()
a few lines down
} | ||
|
||
ManifestTemplate manifest = Verify.verifyNotNull(manifestAndConfigFound.get().getManifest()); | ||
// Verify all layers described in manifest are present in cache | ||
if (!baseImageLayersCache.allLayersCached(manifest)) { | ||
return Collections.emptyList(); | ||
} | ||
|
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.
Is it possible to combine !manifestAndConfigFound.isPresent()
and areAllLayersCached
with an &&
?
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 gets messier here to combine them I think? (Since grabbing the manifest is conditional on manifestAndConfigFound.isPresent()
, plus it also needs to perform the verifyNotNull
check)
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 I see! thanks for trying this out. Hm thinking of a way in which we could remove the number of if statements since they are all testing similar things but just at different levels of granularity. Is it possible to put them in a helper method with a descriptive name?
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.
You're right, the many if statements did get more cumbersome with the changes added in this PR. I think the refactoring suggestion from @elefeint will help with this here!
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.
Excellent refactoring!
* @param manifest the image manifest | ||
* @return a boolean | ||
*/ | ||
boolean allLayersCached(ManifestTemplate manifest) { |
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.
Got it, thanks! Hm it looks like Cache takes in cacheStorageFiles
as a parameter to it's constructor, could we use that maybe?
private Cache(CacheStorageFiles cacheStorageFiles) { |
jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java
Outdated
Show resolved
Hide resolved
jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java
Outdated
Show resolved
Hide resolved
jib-core/src/test/java/com/google/cloud/tools/jib/builder/steps/PullBaseImageStepTest.java
Outdated
Show resolved
Hide resolved
jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java
Outdated
Show resolved
Hide resolved
public void testAllLayersCached_v21SingleManifest() | ||
throws IOException, CacheCorruptedException, DigestException, URISyntaxException { | ||
|
||
setupCachedMetadataV21(cacheDirectory); |
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.
Minor suggestion: If possible, let's format the test into three blocks: arrange, act and assert, with a space between each of these blocks. The arrange
block will take care of all setup, the act
block will call the method we're testing and the assert
block will do all the verification.
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.
Thanks for the suggestion, this makes a lot of sense! Tried to follow this idea though with the act
and assert
block more or less combined, since for a few of the tests had asserts both before and after certain actions.
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 we store these calls in variables? For example:
cacheAfterFirstLayerDirectory = Files.createDirectories(cacheStorageFiles.getLayerDirectory(firstLayerDigest));
areAllLayersCachedAfterFirstLayer = cacheStorageReader.areAllLayersCached(manifest)
cacheAfterSecondLayerDirectory = ...
areAllLayersCachedAfterSecondLayer= ..
//Assert block
This is just an example so you can pick a name you think works better.
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.
Ohh apologies I completely misunderstood earlier! Thank you for the explanation, I see what is meant by having separate act
and assert
blocks now. Will update the tests 😃
jib-core/src/test/java/com/google/cloud/tools/jib/cache/CacheStorageReaderTest.java
Outdated
Show resolved
Hide resolved
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.
Broadly LGTM with my limited knowledge. A couple of random questions inline.
jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageReader.java
Show resolved
Hide resolved
@@ -486,6 +492,11 @@ List<Image> getCachedBaseImages() | |||
} | |||
|
|||
ManifestTemplate manifest = Verify.verifyNotNull(manifestAndConfigFound.get().getManifest()); | |||
// Verify all layers described in manifest are present in cache |
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.
(optional) there was quite a bit of code duplication in this method even in the past between the section that deals with general and platform-based manifest processing. Could there be an opportunity for refactoring common logic out into a helper method?
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 this is a great suggestion - will refactor this and see if I can make the methods cleaner to read.
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.
LGTM. This fix required in-depth knowledge into Jib's caching mechanism and you really hit it out of the park! Also made great refactorings along the way.
} else { | ||
return Collections.singletonList(cachedImage.get()); | ||
} |
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.
super nit: we can do away with the else
here and return Collections.singletonList(...)
outside the if
.
} | ||
|
||
ManifestTemplate manifest = Verify.verifyNotNull(manifestAndConfigFound.get().getManifest()); | ||
// Verify all layers described in manifest are present in cache | ||
if (!baseImageLayersCache.allLayersCached(manifest)) { | ||
return Collections.emptyList(); | ||
} | ||
|
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.
Excellent refactoring!
@mpeddada1 @elefeint @chanseokoh Thank you so much for all the help here with this PR! |
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.
Nice refactoring!
Thank you! Will update changelog and merge. |
Kudos, SonarCloud Quality Gate passed! |
Hi @herder - we currently have some work in progress on compatibility in our release build environment, but can plan for a release later this week including this change. |
@herder |
Great, thank you so much @emmileaf ! |
In this PR:
Implementation of a proposed solution in #3733’s discussions, to check for presence of layer files in cache in addition to the manifest json before returning the cached base image(s). This would also address the unauthorized error described in #2220 and also reported in #2007’s comments.
Details:
allLayersCached()
method toCache
andCacheStorageReader
that checks for existence of cached layers for all layers described in the image manifest.PullBaseImageStep.getCachedBaseImages()
to invoke this check, and returns no cached images if layers are missing.Two approaches for layer check:
755dcfc
is a simpler check which verifies that the corresponding layer directory exists in the cache (but not that its contents are valid)b231d79
checks layers by making a call to retrieve the layer, though the retrieved layer is unused and the laterObtainBaseImageLayerStep
would retrieve it againOpen to suggestions on what the better approach might be here - the second option does redundant work, but is a more rigorous validation.