Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 33 additions & 9 deletions pkg/image/containerd/daemon_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import (
const Daemon image.Source = image.ContainerdDaemonSource

// NewDaemonProvider creates a new provider instance for a specific image that will later be cached to the given directory.
func NewDaemonProvider(tmpDirGen *file.TempDirGenerator, registryOptions image.RegistryOptions, namespace string, imageStr string, platform *image.Platform) image.Provider {
func NewDaemonProvider(tmpDirGen *file.TempDirGenerator, registryOptions image.RegistryOptions, namespace string, imageStr string, platform *image.Platform, imageCleanup bool) image.Provider {
if namespace == "" {
namespace = namespaces.Default
}
Expand All @@ -46,6 +46,7 @@ func NewDaemonProvider(tmpDirGen *file.TempDirGenerator, registryOptions image.R
platform: platform,
namespace: namespace,
registryOptions: registryOptions,
imageCleanup: imageCleanup,
}
}

Expand All @@ -58,6 +59,7 @@ type daemonImageProvider struct {
platform *image.Platform
namespace string
registryOptions image.RegistryOptions
imageCleanup bool
}

func (p *daemonImageProvider) Name() string {
Expand Down Expand Up @@ -85,7 +87,7 @@ func (p *daemonImageProvider) Provide(ctx context.Context) (*image.Image, error)

ctx = namespaces.WithNamespace(ctx, p.namespace)

resolvedImage, resolvedPlatform, err := p.pullImageIfMissing(ctx, client)
resolvedImage, resolvedPlatform, imageCleanupFunc, err := p.pullImageIfMissing(ctx, client)
if err != nil {
return nil, err
}
Expand All @@ -101,7 +103,7 @@ func (p *daemonImageProvider) Provide(ctx context.Context) (*image.Image, error)
log.WithFields("image", p.imageStr, "time", time.Since(startTime)).Info("containerd saved image")

// use the existing tarball provider to process what was pulled from the containerd daemon
return stereoscopeDocker.NewArchiveProvider(p.tmpDirGen, tarFileName, withMetadata(resolvedPlatform, p.imageStr)...).
return stereoscopeDocker.NewArchiveProvider(p.tmpDirGen, tarFileName, append(withMetadata(resolvedPlatform, p.imageStr), withCleanupFunc(p.imageCleanup, imageCleanupFunc)...)...).
Provide(ctx)
}

Expand Down Expand Up @@ -324,7 +326,8 @@ func (p *daemonImageProvider) fetchPlatformFromConfig(ctx context.Context, clien
return &cfg, nil
}

func (p *daemonImageProvider) pullImageIfMissing(ctx context.Context, client *containerd.Client) (string, *platforms.Platform, error) {
func (p *daemonImageProvider) pullImageIfMissing(ctx context.Context, client *containerd.Client) (string, *platforms.Platform, func() error, error) {
var cleanupFunc func() error
p.imageStr = ensureRegistryHostPrefix(p.imageStr)

// try to get the image first before pulling
Expand All @@ -338,20 +341,23 @@ func (p *daemonImageProvider) pullImageIfMissing(ctx context.Context, client *co
if err != nil {
_, err := p.pull(ctx, client, imageStr)
if err != nil {
return "", nil, err
return "", nil, cleanupFunc, err
}
// Image Pulled Create cleanupFunc
cleanupFunc = func() error {
return p.deleteImage(ctx, client, resolvedImage)
}

resolvedImage, resolvedPlatform, err = p.resolveImage(ctx, client, imageStr)
if err != nil {
return "", nil, fmt.Errorf("unable to resolve image after pull: %w", err)
return "", nil, cleanupFunc, fmt.Errorf("unable to resolve image after pull: %w", err)
}
}

if err := validatePlatform(p.platform, resolvedPlatform); err != nil {
return "", nil, fmt.Errorf("platform validation failed: %w", err)
return "", nil, cleanupFunc, fmt.Errorf("platform validation failed: %w", err)
}

return resolvedImage, resolvedPlatform, nil
return resolvedImage, resolvedPlatform, cleanupFunc, nil
}

func validatePlatform(expected *image.Platform, given *platforms.Platform) error {
Expand Down Expand Up @@ -446,6 +452,16 @@ func (p *daemonImageProvider) saveImage(ctx context.Context, client *containerd.
return tempTarFile.Name(), nil
}

func (p *daemonImageProvider) deleteImage(ctx context.Context, client *containerd.Client, resolvedImage string) error {
// Remove the image
if err := client.ImageService().Delete(ctx, resolvedImage, nil); err != nil {
return fmt.Errorf("failed to delete image %s: %w", resolvedImage, err)
}

log.Infof("deleted image %s from containerd", p.Name())
return nil
}

func exportPlatformComparer(platform *image.Platform) (platforms.MatchComparer, error) {
// it is important to only export a single architecture. Default to linux/amd64. Without specifying a specific
// architecture then the export may include multiple architectures (if the tag points to a manifest list)
Expand Down Expand Up @@ -517,6 +533,14 @@ func withMetadata(platform *platforms.Platform, ref string) (metadata []image.Ad
return metadata
}

func withCleanupFunc(imageCleanup bool, cleanupFunc func() error) []image.AdditionalMetadata {
// with image.WithCleanupFunc we can ensure that the image is cleaned up after use
if cleanupFunc == nil || !imageCleanup {
return nil // Or return []image.AdditionalMetadata{} for an empty slice
}
return []image.AdditionalMetadata{image.WithCleanupFunc(cleanupFunc)}
}

// if imageName doesn't have an identifiable hostname prefix set,
// add docker hub by default
func ensureRegistryHostPrefix(imageName string) string {
Expand Down
55 changes: 43 additions & 12 deletions pkg/image/docker/daemon_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,21 @@ import (
const Daemon image.Source = image.DockerDaemonSource

// NewDaemonProvider creates a new provider instance for a specific image that will later be cached to the given directory
func NewDaemonProvider(tmpDirGen *file.TempDirGenerator, imageStr string, platform *image.Platform) image.Provider {
return NewAPIClientProvider(Daemon, tmpDirGen, imageStr, platform, func() (client.APIClient, error) {
func NewDaemonProvider(tmpDirGen *file.TempDirGenerator, imageStr string, platform *image.Platform, imageCleanup bool) image.Provider {
return NewAPIClientProvider(Daemon, tmpDirGen, imageStr, platform, imageCleanup, func() (client.APIClient, error) {
return docker.GetClient()
})
}

// NewAPIClientProvider creates a new provider for the provided Docker client.APIClient
func NewAPIClientProvider(name string, tmpDirGen *file.TempDirGenerator, imageStr string, platform *image.Platform, newClient apiClientCreator) image.Provider {
func NewAPIClientProvider(name string, tmpDirGen *file.TempDirGenerator, imageStr string, platform *image.Platform, imageCleanup bool, newClient apiClientCreator) image.Provider {
return &daemonImageProvider{
name: name,
tmpDirGen: tmpDirGen,
newAPIClient: newClient,
imageStr: imageStr,
platform: platform,
imageCleanup: imageCleanup,
}
}

Expand All @@ -59,6 +60,7 @@ type daemonImageProvider struct {
newAPIClient apiClientCreator
imageStr string
platform *image.Platform
imageCleanup bool
}

func (p *daemonImageProvider) Name() string {
Expand Down Expand Up @@ -277,7 +279,7 @@ func (p *daemonImageProvider) Provide(ctx context.Context) (*image.Image, error)
}

log.WithFields("image", p.imageStr).Info("docker pulling image")
imageRef, err := p.pullImageIfMissing(ctx, apiClient)
imageRef, imageCleanupFunc, err := p.pullImageIfMissing(ctx, apiClient)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -307,8 +309,9 @@ func (p *daemonImageProvider) Provide(ctx context.Context) (*image.Image, error)
log.WithFields("image", imageRef, "time", time.Since(startTime), "path", tarFileName).Info("docker saved image")

// use the existing tarball provider to process what was pulled from the docker daemon
return NewArchiveProvider(p.tmpDirGen, tarFileName, withInspectMetadata(inspectResult)...).
Provide(ctx)
return NewArchiveProvider(p.tmpDirGen, tarFileName,
append(withInspectMetadata(inspectResult), withCleanupFunc(p.imageCleanup, imageCleanupFunc)...)...,
).Provide(ctx)
}

func (p *daemonImageProvider) saveImage(ctx context.Context, apiClient client.APIClient, imageRef string) (string, error) {
Expand Down Expand Up @@ -373,10 +376,22 @@ func (p *daemonImageProvider) saveImage(ctx context.Context, apiClient client.AP
return tempTarFile.Name(), nil
}

func (p *daemonImageProvider) pullImageIfMissing(ctx context.Context, apiClient client.APIClient) (imageRef string, err error) {
func (p *daemonImageProvider) deleteImage(ctx context.Context, apiClient client.APIClient, imageRef string) error {
// delete the image from the docker daemon
_, err := apiClient.ImageRemove(ctx, imageRef, dockerImage.RemoveOptions{})
if err != nil {
return fmt.Errorf("unable to delete image: %w", err)
}

log.Infof("deleted image %q from %s", imageRef, p.name)

return nil
}

func (p *daemonImageProvider) pullImageIfMissing(ctx context.Context, apiClient client.APIClient) (imageRef string, cleanupFunc func() error, err error) {
imageRef, originalImageRef, err := image.ParseReference(p.imageStr)
if err != nil {
return "", err
return "", cleanupFunc, err
}

// check if the image exists locally
Expand All @@ -387,24 +402,33 @@ func (p *daemonImageProvider) pullImageIfMissing(ctx context.Context, apiClient
imageRef = strings.TrimSuffix(imageRef, ":latest")
}
}

if err != nil {
if client.IsErrNotFound(err) {
if err = p.pull(ctx, apiClient, imageRef); err != nil {
return imageRef, err
return imageRef, cleanupFunc, err
}
// Image Pulled Create cleanupFunc
cleanupFunc = func() error {
return p.deleteImage(ctx, apiClient, imageRef)
}
} else {
return imageRef, fmt.Errorf("unable to inspect existing image: %w", err)
return imageRef, cleanupFunc, fmt.Errorf("unable to inspect existing image: %w", err)
}
} else {
// looks like the image exists, but if the platform doesn't match what the user specified, we may need to
// pull the image again with the correct platform specifier, which will override the local tag.
if err = p.validatePlatform(inspectResult); err != nil {
if err = p.pull(ctx, apiClient, imageRef); err != nil {
return imageRef, err
return imageRef, cleanupFunc, err
}
// Image Pulled Create cleanupFunc
cleanupFunc = func() error {
return p.deleteImage(ctx, apiClient, imageRef)
}
}
}
return imageRef, nil
return imageRef, cleanupFunc, nil
}

func (p *daemonImageProvider) validatePlatform(i dockerImage.InspectResponse) error {
Expand Down Expand Up @@ -436,6 +460,13 @@ func withInspectMetadata(i dockerImage.InspectResponse) (metadata []image.Additi
return metadata
}

func withCleanupFunc(imageCleanup bool, cleanupFunc func() error) []image.AdditionalMetadata {
if cleanupFunc == nil || !imageCleanup {
return nil // Or return []image.AdditionalMetadata{} for an empty slice
}
return []image.AdditionalMetadata{image.WithCleanupFunc(cleanupFunc)}
}

func encodeCredentials(authConfig configTypes.AuthConfig) (string, error) {
buffer := &bytes.Buffer{}
encoder := json.NewEncoder(buffer)
Expand Down
19 changes: 19 additions & 0 deletions pkg/image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ type Image struct {
SquashedSearchContext filetree.Searcher

overrideMetadata []AdditionalMetadata

imageCleanupFunc func() error
}

type AdditionalMetadata func(*Image) error
Expand Down Expand Up @@ -73,6 +75,16 @@ func WithTags(tags ...string) AdditionalMetadata {
}
}

func WithCleanupFunc(fn func() error) AdditionalMetadata {
return func(image *Image) error {
if image.imageCleanupFunc != nil {
return fmt.Errorf("image cleanup function already set, cannot override")
}
image.imageCleanupFunc = fn
return nil
}
}

func WithManifest(manifest []byte) AdditionalMetadata {
return func(image *Image) error {
image.Metadata.RawManifest = manifest
Expand Down Expand Up @@ -379,5 +391,12 @@ func (i *Image) Cleanup() error {
}
}
}

if i.imageCleanupFunc != nil {
if err := i.imageCleanupFunc(); err != nil {
errs = multierror.Append(errs, fmt.Errorf("image cleanup function failed: %w", err))
}
}

return errs
}
4 changes: 2 additions & 2 deletions pkg/image/podman/daemon_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (

const Daemon image.Source = image.PodmanDaemonSource

func NewDaemonProvider(tmpDirGen *file.TempDirGenerator, imageStr string, platform *image.Platform) image.Provider {
return docker.NewAPIClientProvider(Daemon, tmpDirGen, imageStr, platform, func() (client.APIClient, error) {
func NewDaemonProvider(tmpDirGen *file.TempDirGenerator, imageStr string, platform *image.Platform, imageCleanup bool) image.Provider {
return docker.NewAPIClientProvider(Daemon, tmpDirGen, imageStr, platform, imageCleanup, func() (client.APIClient, error) {
return podman.GetClient()
})
}
13 changes: 7 additions & 6 deletions providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ const (

// ImageProviderConfig is the user-configuration containing all configuration needed by stereoscope image providers
type ImageProviderConfig struct {
UserInput string
Platform *image.Platform
Registry image.RegistryOptions
UserInput string
Platform *image.Platform
Registry image.RegistryOptions
ImageCleanup bool
}

func ImageProviders(cfg ImageProviderConfig) []collections.TaggedValue[image.Provider] {
Expand All @@ -36,9 +37,9 @@ func ImageProviders(cfg ImageProviderConfig) []collections.TaggedValue[image.Pro
taggedProvider(sif.NewArchiveProvider(tempDirGenerator, cfg.UserInput), FileTag),

// daemon providers
taggedProvider(docker.NewDaemonProvider(tempDirGenerator, cfg.UserInput, cfg.Platform), DaemonTag, PullTag),
taggedProvider(podman.NewDaemonProvider(tempDirGenerator, cfg.UserInput, cfg.Platform), DaemonTag, PullTag),
taggedProvider(containerd.NewDaemonProvider(tempDirGenerator, cfg.Registry, containerdClient.Namespace(), cfg.UserInput, cfg.Platform), DaemonTag, PullTag),
taggedProvider(docker.NewDaemonProvider(tempDirGenerator, cfg.UserInput, cfg.Platform, cfg.ImageCleanup), DaemonTag, PullTag),
taggedProvider(podman.NewDaemonProvider(tempDirGenerator, cfg.UserInput, cfg.Platform, cfg.ImageCleanup), DaemonTag, PullTag),
taggedProvider(containerd.NewDaemonProvider(tempDirGenerator, cfg.Registry, containerdClient.Namespace(), cfg.UserInput, cfg.Platform, cfg.ImageCleanup), DaemonTag, PullTag),

// registry providers
taggedProvider(oci.NewRegistryProvider(tempDirGenerator, cfg.Registry, cfg.UserInput, cfg.Platform), RegistryTag, PullTag),
Expand Down
Loading