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
WIP RFC: internal/external interface separation/adapters #1439
Conversation
6c919d7
to
ae4e000
Compare
On second thought, we will need basically all of this, both for So solving this is, one way or another, is not urgent but it’s going to have to happen soonish. |
@@ -30,6 +32,8 @@ import ( | |||
) | |||
|
|||
type dockerImageDestination struct { | |||
impl.Compat |
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 makes no sense to have a separate imagedestination/impl
package; a transport is a fully-connected graph of transport/reference/(source+destination), so importing any part of it is going to drag in all of that once we add new features to all most of these interfaces.
So we need something like internal/transport/compat
, with compat.Implement{Source,Reference,Destination}(…)
(many ways to name this…).
(OTOH the other adapter, imagedestination.FromPublic
, does conceptually make more sense to have split granularly, one subpackage per interface, because not every caller is going to use every interface — especially if we ever made this public, for callers like skopeo inspect
that only need one of the read/write directions.)
docker/docker_image_dest_test.go
Outdated
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
var _ internalTypes.ImageDestinationWithOptions = &dockerImageDestination{} |
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 of the type checks, we need to have some mechanisms ensure the conformances hold, but it would be nice to be able to do that without relying on manually writing tests like this. (I did break ImageDestinationPartial
in one version of this PR already.)
Hopefully a new internal Transport
(with ParseInternalReference
returning an internal Reference
, and NewInternalSource
returning an internal ImageSource
, will do that automatically, or something like that.
internal/imagedestination/wrapper.go
Outdated
// WARNING: The contents of stream are being verified on the fly. Until stream.Read() returns io.EOF, the contents of the data SHOULD NOT be available | ||
// to any other readers for download using the supplied digest. | ||
// If stream.Read() at any time, ESPECIALLY at end of input, returns an error, PutBlob MUST 1) fail, and 2) delete any data stored so far. | ||
func (w *wrapped) PutBlobWithOptions(ctx context.Context, stream io.Reader, inputInfo publicTypes.BlobInfo, options types.PutBlobOptions) (publicTypes.BlobInfo, 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.
It would be interesting to have the internal names clean, without the WithOptions
baggage. We can’t quite do that directly in the returned ImageDestination
objects, and doing it here would require wrapping the publicTypes.ImageDestination
in another non-zero-cost layer of indirection. Or, alternatively, we could have all of the internal code clean and work directly with internal objects and internal names, but always proxy external callers though the external types in separate objects (so that we can have direct access to internal PutBlob(new)
and external callers get a proxy object with PutBlob(old)
).
A cheap compromise would be to just name the internal methods PutBlob2
or PutBlobX
(with X
reserved for all internal names), admittedly that’s also pretty ugly.
copy/copy.go
Outdated
uploadedInfo, err = dest.PutBlobWithOptions(ctx, &errorAnnotationReader{destStream}, inputInfo, options) | ||
} else { | ||
uploadedInfo, err = c.dest.PutBlob(ctx, &errorAnnotationReader{destStream}, inputInfo, c.blobInfoCache, isConfig) | ||
options := internalTypes.PutBlobOptions{ |
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 almost seems clear that copyBlobFromStream
should have a PutBlobOptions
parameter, rather than individual ones; the large number of parameters to copyBlobFromStream
are not nice at all.
Except for Cache
, and for the fact that we might, in that case, want to pass PutBlobOptions
by reference instead of by value.
Oh well, we can always introduce a single-purpose struct just for naming parameters to copyBlobFromStream
. But that’s out of scope.
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.
Very clever! I am cool with the changes but I also begin to wonder whether the added complexity of indirection outweighs the "benefits" of breaking the API and redesigning the API; just a thought/feeling.
internal/imagedestination/wrapper.go
Outdated
publicTypes "github.com/containers/image/v5/types" | ||
) | ||
|
||
// FromPublic(dest) returns an object that provides the internaltypes.ImageDestinationWithOptions API |
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 really like this idea; very elegant!
One possible answer to that is to make the external API purely a utility/wrapper that we mostly don’t care about (everything internal uses an internal API), e.g. use E.g. if we, for SIF, add some kind of progress facility to There just aren’t any great options.
Of all those options, it seems to me that something very broadly like this PR is the least bead way to keep moving forward. But I think all of them are sad, and I might very well be wrong about preferring this one. Admittedly this PR does not feel productive, compared to actual features, and yes, those two interface wrappers are tedious and not elegant. OTOH it was a prerequisite for improving the call stack and naming in |
A reminder from earlier #751 : Probably ideally to move that feature inside this repo. Failing that, we would at least have a real-world test for all the compatibility infrastructure 😟 |
ae4e000
to
3e3d165
Compare
3e3d165
to
fa67f89
Compare
fa67f89
to
be7e6e7
Compare
be7e6e7
to
24c0b84
Compare
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
The purpose is to let a transport only implement these methods, and provide a generic fallback for the public methods. Signed-off-by: Miloslav Trmač <mitr@redhat.com>
…tion Signed-off-by: Miloslav Trmač <mitr@redhat.com>
For now, this does not change behavior; but it will allow using BlobInfoCache2 next. Signed-off-by: Miloslav Trmač <mitr@redhat.com>
This should not change behavior, but it changes the overhead: - Transports (like dockerImageDestination) now don't need to care about the old interface - ... it's now handled by internal/imagedestination/impl - whether the transport uses the cache or not. Overall, this should make the internal callers and internal implementations a bit simpler and faster, at the cost of moving the overhead to the compat infrastructure, and paying the cost for more external callers. Signed-off-by: Miloslav Trmač <mitr@redhat.com>
24c0b84
to
1498d01
Compare
Superseded by #1589. |
This is a RFC for a hopefully more manageable approach to evolving the internal interfaces, motivated by the progress discussion in #1438 .
I think this looks fairly clean, but boy it’s a lot of toil.
Major themes:
imagedestination.FromPublic
, which allows an internal caller to always use the freshinternal/types
interface, instead of dealing with the compat versions. See how this benefitscopy
.types.ImageDestination
is effectively frozen and deprecated: instead of havinginternal/imagedestination.FromPublic()
returning an internal interface, we could havestable/imagedestination.Destination()
returning a struct with newly-designed and newly-named public functions (e.g. using the "functional options" pattern, or anything else), while still letting us have an internal interface that changes over time.ImageDestinationPartial
. I guess we’ll want to foldImageDestinationPartial
intoImageDestinationWithOptions
and use aSupportsPutBlobPartial() bool
, but it’s not obviously the only possible approach. Maybe just robust self tests would be enough.imagedestination/impl.AddCompat
, which allows an internal transport to implement the newinternal/types
interface, and rely on provided stubs for to still implement the compat version. See how this benefitsstorage
(although that required a fair bit of code movement).internal/types
useBlobInfoCache2
instead of the public frozen version; thendocker
can just assume that version, and delegate dealing with old callers to the generic infrastructure. Note that this has some run-time cost.)@vrothberg RFC. There might be a much simpler design I’m missing.
I wanted to prototype this, using actually existing internal features and code, to see what it would take to have something similar for new features, e.g. have
NewImageSource(…options{Progress:…})
, or adding progress bars to thoseGetBlob
/PutBlob
implementations that create extra copies/compress/decompress and the like. I think this is broadly viable and correct — but it’s also a fair amount of somewhat tedious work I don’t think I want to commit to do in a rush before Podman 4.0.