-
Notifications
You must be signed in to change notification settings - Fork 64
🌱 Monorepo pt2: fully consolidate image pull/cache implementations #1731
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
🌱 Monorepo pt2: fully consolidate image pull/cache implementations #1731
Conversation
✅ Deploy Preview for olmv1 ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
catalogd/internal/controllers/core/clustercatalog_controller.go
Outdated
Show resolved
Hide resolved
if err != nil { | ||
unpackErr := fmt.Errorf("source catalog content: %w", err) | ||
updateStatusProgressing(&catalog.Status, catalog.GetGeneration(), unpackErr) | ||
return ctrl.Result{}, unpackErr | ||
} | ||
|
||
switch unpackResult.State { | ||
case source.StateUnpacked: |
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.
source.StateUnpacked
was also a rukpak leftover that we left behind. In rukpak, we had async image pulling based on running a pod. Now, we have synchronous image pulling in-process, so we know that if we've gotten this far we have an image unpacked.
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 see that we are moving from ``r.Unpacker.Unpackto
ImagePuller.Pull`
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 am looking at https://github.com/search?q=repo%3Aoperator-framework%2Foperator-controller%20StateUnpacked&type=code to see if we covered all places and it seems fine 👍
catalogd/internal/controllers/core/clustercatalog_controller.go
Outdated
Show resolved
Hide resolved
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1731 +/- ##
==========================================
+ Coverage 67.45% 68.29% +0.84%
==========================================
Files 61 63 +2
Lines 5245 5116 -129
==========================================
- Hits 3538 3494 -44
+ Misses 1446 1392 -54
+ Partials 261 230 -31
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
catalogd/internal/controllers/core/clustercatalog_controller_test.go
Outdated
Show resolved
Hide resolved
catalogd/internal/controllers/core/clustercatalog_controller_test.go
Outdated
Show resolved
Hide resolved
@@ -927,7 +920,7 @@ func TestPollingReconcilerUnpack(t *testing.T) { | |||
}, | |||
Status: successfulUnpackStatus(), | |||
}, | |||
storedCatalogData: successfulStoredCatalogData(metav1.Now()), | |||
storedCatalogData: successfulStoredCatalogData(time.Now()), |
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.
These metav1.Now()
to time.Now()
changes are there because our image handling libraries should not be coupled to kubernetes-isms. I moved conversion to metav1.Time
into the reconciler code, iirc.
cl, reconciler := newClientAndReconciler(t) | ||
reconciler.Unpacker = &MockUnpacker{ | ||
result: &source.Result{ | ||
State: "unexpected", |
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.
Because the rukpak source.Result
struct is gone and there is no concept of unpack state anymore, this entire test is no longer relevant.
internal/util/image/disk_cache.go
Outdated
func CatalogCache(basePath string) Cache { | ||
return &diskCache{ | ||
basePath: basePath, | ||
filterFunc: func(ctx context.Context, srcRef reference.Named, image ocispecv1.Image) (archive.Filter, error) { | ||
_, specIsCanonical := srcRef.(reference.Canonical) | ||
dirToUnpack, ok := image.Config.Labels[ConfigDirLabel] | ||
if !ok { | ||
// If the spec is a tagged ref, retries could end up resolving a new digest, where the label | ||
// might show up. If the spec is canonical, no amount of retries will make the label appear. | ||
// Therefore, we treat the error as terminal if the reference from the spec is canonical. | ||
return nil, errorutil.WrapTerminal(fmt.Errorf("catalog image is missing the required label %q", ConfigDirLabel), specIsCanonical) | ||
} | ||
|
||
return allFilters( | ||
onlyPath(dirToUnpack), | ||
forceOwnershipRWX(), | ||
), nil | ||
}, | ||
} | ||
} | ||
|
||
func BundleCache(basePath string) Cache { | ||
return &diskCache{ | ||
basePath: basePath, | ||
filterFunc: func(_ context.Context, _ reference.Named, _ ocispecv1.Image) (archive.Filter, error) { | ||
return forceOwnershipRWX(), nil | ||
}, | ||
} | ||
} |
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.
These filter funcs are literally the only difference between catalog and bundle image unpacking. 😄
internal/util/image/disk_cache.go
Outdated
return filepath.Join(a.idPath(id), digest.String()) | ||
} | ||
|
||
func (a *diskCache) Store(ctx context.Context, id string, srcRef reference.Named, canonicalRef reference.Canonical, imgCfg ocispecv1.Image, layers iter.Seq[LayerData]) (fs.FS, time.Time, error) { |
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.
First use of a go1.23 iterator!
I implemented this interface/function this way in order to eliminate the cache's knowledge of the puller's internals. The puller provides the layers
iterator to the cache. And the cache simply needs to iterate the layers.
internal/util/image/filters.go
Outdated
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 file is not actually new. It existed before as layers.go
, but I moved a bunch of it into the disk cache file. Only change in this PR was to unexport the filters.
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 file still needs to be consolidated with pull_catalog_test.go
. We can likely re-write all the tests now that the abstractions are better separated.
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 essentially a move/update of the containers_image_test.go
from operator-controller's original rukpak/source
package.
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 essentially a move/update of the containers_image_test.go from catalogd's original rukpak/source package. Thankfully, it looks like it didn't change enough for GH to decide it was a different file.
1909622
to
19d10a0
Compare
fb9cc60
to
1d61874
Compare
e1021fb
to
5da55b0
Compare
bc48637
to
d9682c0
Compare
"github.com/operator-framework/operator-controller/internal/catalogd/storage" | ||
imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image" |
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.
👍 What we use from source is now in image
@@ -272,7 +286,7 @@ func (r *ClusterCatalogReconciler) getCurrentState(catalog *catalogdv1.ClusterCa | |||
// Set expected status based on what we see in the stored catalog | |||
clearUnknownConditions(expectedStatus) | |||
if hasStoredCatalog && r.Storage.ContentExists(catalog.Name) { | |||
updateStatusServing(expectedStatus, storedCatalog.unpackResult, r.Storage.BaseURL(catalog.Name), storedCatalog.observedGeneration) | |||
updateStatusServing(expectedStatus, storedCatalog.ref, storedCatalog.lastUnpack, r.Storage.BaseURL(catalog.Name), storedCatalog.observedGeneration) |
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 seems that we covered all places where have before unpackResult
https://github.com/search?q=repo%3Aoperator-framework%2Foperator-controller+unpackResult&type=code 👍
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
/hold adding a hold for couple of hours to finish reviewing the PR. The hold is there to make sure it does not get merged in between. |
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.
Generally looks fine.
return ctrl.Result{}, err | ||
} | ||
if catalog.Spec.Source.Image == nil { | ||
err := reconcile.TerminalError(fmt.Errorf("error parsing catalog, catalog %s has a nil image source", catalog.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.
err := reconcile.TerminalError(fmt.Errorf("error parsing catalog, catalog %s has a nil image source", catalog.Name)) | |
err := reconcile.TerminalError(fmt.Errorf("error parsing ClusterCatalog %s, image source is nil", catalog.Name)) |
"catalog" is repeated, and you probably want to indicate the actual resource type.
bundleSource := &rukpaksource.BundleSource{ | ||
Name: ext.GetName(), | ||
Type: rukpaksource.SourceTypeImage, | ||
Image: &rukpaksource.ImageSource{ | ||
Ref: resolvedBundle.Image, | ||
}, | ||
} |
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.
Yay for getting rid of this.
if err != nil { | ||
return false, err | ||
} | ||
if !keep { | ||
return false, nil | ||
} |
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 err != nil { | |
return false, err | |
} | |
if !keep { | |
return false, nil | |
} | |
if err != nil || !keep { | |
return false, err | |
} |
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 can personally go either way on this. The condensed version, IMO, is ever so slightly harder to reason about because I would assume that most go developers will have their eye drawn primarily to err != nil
and return false, err
and initialize their memory model with "this code path returns an error", only then to see !keep
and have to reason their way through to understand that err
may or may not be nil
.
I think I'll leave as is for clarity unless there is broad consensus to condense.
internal/shared/util/image/pull.go
Outdated
// copy.Image can concurrently pull all the layers. | ||
// | ||
////////////////////////////////////////////////////// | ||
layoutDir, err := os.MkdirTemp("", "oci-layout-*") |
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.
Interesting, rather than using the bundle or catalog name, you're using something random, which presumably will behave better if you do things multiple times?
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 was random before as well (the *
just lets the caller be explicit about where the random name piece should go). By default the randomness of the name is appended, so me adding this *
here is essentailly a no-op.
I notice that we used to include what is now the ownerID
in the name. I'll add that back in.
}); err != nil { | ||
return nil, nil, time.Time{}, fmt.Errorf("error copying image: %w", err) | ||
} | ||
l.Info("pulled image") |
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.
At first I thought this was missing the image ref, but it's done way up on lines 51/52.
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, this is a double-edged sword probably. We can:
- Incrementally add k/v pairs as the context fills in, meaning later
Info
andError
calls don't have to keep repeating that context over and over (and potentially differently) - Leave the incremental k/v pairs out, and repeat them every time (potentially forgetting important k/v pairs in certain places).
(1) has the problem that we may forget about the logger already being setup with k/v pairs, which might lead us to duplicating the same information with a slightly different key. (e.g. ref
and reference
both showing up in a log line with the same value)
I'm almost convincing myself that the tests should include tests of the structured fields of the logger, but maybe we can add that later if we discover that we aren't being consistent with our logging.
fsys, modTime, err = p.applyImage(ctx, ownerID, dockerRef, canonicalRef, layoutImgRef, cache, srcCtx) | ||
if err != nil { | ||
return nil, nil, time.Time{}, fmt.Errorf("error applying image: %w", err) | ||
} |
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 seems like another stage, where a large comment might be useful; the old code indicated:
//////////////////////////////////////////////////////
//
// Mount the image we just pulled
//
//////////////////////////////////////////////////////
@@ -273,14 +274,16 @@ func main() { | |||
os.Exit(1) | |||
} | |||
|
|||
unpackCacheBasePath := filepath.Join(cacheDir, source.UnpackCacheDir) | |||
unpackCacheBasePath := filepath.Join(cacheDir, "unpack") |
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 looks odd. we are moving from using a variable to a hard coded string.
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 the only place that constant was used. Nothing in the source
package used it, nothing in the new imageutil package uses it.
err := reconcile.TerminalError(fmt.Errorf("unknown source type %q", catalog.Spec.Source.Type)) | ||
updateStatusProgressing(&catalog.Status, catalog.GetGeneration(), err) | ||
return ctrl.Result{}, err | ||
} |
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.
So we never checked the catalog.Spec.Source.Type
or did we move this from other place?
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 used to be part of the source.ContainersImage
unpacker code:
operator-controller/internal/catalogd/source/containers_image.go
Lines 43 to 49 in becde51
if catalog.Spec.Source.Type != catalogdv1.SourceTypeImage { | |
panic(fmt.Sprintf("programmer error: source type %q is unable to handle specified catalog source type %q", catalogdv1.SourceTypeImage, catalog.Spec.Source.Type)) | |
} | |
if catalog.Spec.Source.Image == nil { | |
return nil, reconcile.TerminalError(fmt.Errorf("error parsing catalog, catalog %s has a nil image source", catalog.Name)) | |
} |
unpacker := &source.ContainersImageRegistry{ | ||
BaseCachePath: filepath.Join(cachePath, "unpack"), | ||
SourceContextFunc: func(logger logr.Logger) (*types.SystemContext, error) { | ||
imageCache := imageutil.BundleCache(filepath.Join(cachePath, "unpack")) |
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 are using unpackCacheBasePath := filepath.Join(cacheDir, "unpack")
also in catalogd main.go . we should use a constant or variable for unpack
.
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.
These unpack paths are decided/defined/used purely in the context of each main.go
. It's just a coincidence/convention that they are the same. But they don't have to be.
/hold cancel |
d9682c0
to
aea6012
Compare
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
/test e2e-kind |
@LalatenduMohanty: No presubmit jobs available for operator-framework/operator-controller@main In response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. |
Description
This PR is a follow-up from #1690. It completely removes the
source
packages that existed in bothcatalogd
andoperator-controller
, eliminates theUnpacker
abstraction that is unused and leftover from rukpak's support for multiple remote fetch protocols, adds new common abstractions for an image puller and an image cache, adds a newcontainers/image
puller implementation, and adds a new disk-based cache implementation.This is still a work-in-progress because the unit tests need to be consolidated and de-duplicated. Because the tests were written with the unpacker interface in mind, they also couple the pull and cache concepts together, so more work can be done to tease those tests apart and make them more appropriate for the new interfaces and implementations.
Reviewer Checklist