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

feat: separate tagged image providers #219

Merged
merged 26 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8b711dd
feat: separate tagged image providers
kzantow Feb 6, 2024
e4fbc22
chore: bump go version in CI
kzantow Feb 6, 2024
c03e9e6
chore: update tests
kzantow Feb 7, 2024
4d7aa0c
chore: update tests
kzantow Feb 7, 2024
0b04138
chore: update tests
kzantow Feb 7, 2024
122e3e8
chore: minor cleanup
kzantow Feb 7, 2024
31a31bc
chore: improve error messaging
kzantow Feb 7, 2024
f9a46ae
chore: fix tests
kzantow Feb 8, 2024
bdedd0d
Merge remote-tracking branch 'upstream/main' into feat/tagged-image-p…
kzantow Feb 8, 2024
9f408b6
chore: fix tests
kzantow Feb 8, 2024
4ea370f
chore: address some PR feedback
kzantow Feb 9, 2024
d809cb4
chore: address more PR feedback
kzantow Feb 9, 2024
860923e
chore: update lint settings, cleanup
kzantow Feb 9, 2024
38fa7ce
chore: adjust provide call to include platform
kzantow Feb 9, 2024
36f33d3
chore: update tests
kzantow Feb 9, 2024
d86a529
chore: revert to more traditional provider pattern
kzantow Feb 13, 2024
1858416
chore: update unit test
kzantow Feb 13, 2024
707581e
chore: reconfigure providers when source scheme provided
kzantow Feb 13, 2024
9958a84
chore: cleanup
kzantow Feb 13, 2024
6c05703
chore: migrate to collections.TaggedValue
kzantow Feb 16, 2024
85e937b
chore: remove static image.Read
kzantow Feb 16, 2024
b137bdf
Merge remote-tracking branch 'upstream/main' into feat/tagged-image-p…
kzantow Feb 16, 2024
93690a8
chore: restore WithAdditionalMetadata
kzantow Feb 16, 2024
b696721
chore: return nil, err
kzantow Feb 16, 2024
935c9c8
chore: minor cleanup
kzantow Feb 16, 2024
050c8d5
Merge remote-tracking branch 'upstream/main' into feat/tagged-image-p…
kzantow Feb 16, 2024
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
2 changes: 1 addition & 1 deletion .github/actions/bootstrap/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ inputs:
go-version:
description: "Go version to install"
required: true
default: "1.19.x"
default: "1.20.x"
use-go-cache:
description: "Restore go cache"
required: true
Expand Down
171 changes: 32 additions & 139 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,17 @@ package stereoscope

import (
"context"
"errors"
"fmt"
"runtime"
"strings"

"github.com/wagoodman/go-partybus"

"github.com/anchore/go-logger"
"github.com/anchore/stereoscope/internal/bus"
containerdClient "github.com/anchore/stereoscope/internal/containerd"
dockerClient "github.com/anchore/stereoscope/internal/docker"
"github.com/anchore/stereoscope/internal/log"
"github.com/anchore/stereoscope/internal/podman"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/image"
"github.com/anchore/stereoscope/pkg/image/containerd"
"github.com/anchore/stereoscope/pkg/image/docker"
"github.com/anchore/stereoscope/pkg/image/oci"
"github.com/anchore/stereoscope/pkg/image/sif"
)

var rootTempDirGenerator = file.NewTempDirGenerator("stereoscope")
Expand Down Expand Up @@ -69,151 +63,50 @@ func WithPlatform(platform string) Option {
}
}

// GetImage parses the user provided image string and provides an image object;
// note: the source where the image should be referenced from is automatically inferred.
func GetImage(ctx context.Context, imgStr string, options ...Option) (*image.Image, error) {
return GetImageFromSource(ctx, imgStr, "", options...)
kzantow marked this conversation as resolved.
Show resolved Hide resolved
}

// GetImageFromSource returns an image from the explicitly provided source.
func GetImageFromSource(ctx context.Context, imgStr string, source image.Source, options ...Option) (*image.Image, error) {
log.Debugf("image: source=%+v location=%+v", source, imgStr)

var cfg config
for _, option := range options {
if option == nil {
continue
}
if err := option(&cfg); err != nil {
return nil, fmt.Errorf("unable to parse option: %w", err)
}
}

provider, cleanup, err := selectImageProvider(imgStr, source, cfg)
if cleanup != nil {
defer cleanup()
}
if err != nil {
// apply ImageProviderConfig config
cfg := config{}
if err := applyOptions(&cfg, options...); err != nil {
return nil, err
}

img, err := provider.Provide(ctx, cfg.AdditionalMetadata...)
if err != nil {
return nil, fmt.Errorf("unable to use %s source: %w", source, err)
// select image provider
providers := ImageProviders(ImageProviderConfig{
Registry: cfg.Registry,
Platform: cfg.Platform,
})
source = strings.ToLower(strings.TrimSpace(source))
if source == "" {
// if no source is explicitly specified, look for a known scheme like docker:
source, imgStr = ExtractSchemeSource(providers, imgStr)
}

err = img.Read()
if err != nil {
return nil, fmt.Errorf("could not read image: %+v", err)
}

return img, nil
}

// nolint:funlen
func selectImageProvider(imgStr string, source image.Source, cfg config) (image.Provider, func(), error) {
var provider image.Provider
tempDirGenerator := rootTempDirGenerator.NewGenerator()
platformSelectionUnsupported := fmt.Errorf("specified platform=%q however image source=%q does not support selecting platform", cfg.Platform.String(), source.String())

cleanup := func() {}

switch source {
case image.DockerTarballSource:
if cfg.Platform != nil {
return nil, cleanup, platformSelectionUnsupported
}
// note: the imgStr is the path on disk to the tar file
provider = docker.NewProviderFromTarball(imgStr, tempDirGenerator)
case image.ContainerdDaemonSource:
c, err := containerdClient.GetClient()
if err != nil {
return nil, cleanup, err
}

cleanup = func() {
if err := c.Close(); err != nil {
log.Errorf("unable to close docker client: %+v", err)
}
}

provider, err = containerd.NewProviderFromDaemon(imgStr, tempDirGenerator, c, containerdClient.Namespace(), cfg.Registry, cfg.Platform)
if err != nil {
return nil, cleanup, err
}
case image.DockerDaemonSource:
c, err := dockerClient.GetClient()
if err != nil {
return nil, cleanup, err
}

cleanup = func() {
if err := c.Close(); err != nil {
log.Errorf("unable to close docker client: %+v", err)
}
}

provider, err = docker.NewProviderFromDaemon(imgStr, tempDirGenerator, c, cfg.Platform)
if err != nil {
return nil, cleanup, err
}
case image.PodmanDaemonSource:
c, err := podman.GetClient()
if err != nil {
return nil, cleanup, err
}

cleanup = func() {
if err := c.Close(); err != nil {
log.Errorf("unable to close docker client: %+v", err)
}
if source != "" {
providers = providers.Select(source)
if len(providers) == 0 {
return nil, fmt.Errorf("unable to find image providers matching: '%s'", source)
}
}

provider, err = docker.NewProviderFromDaemon(imgStr, tempDirGenerator, c, cfg.Platform)
var errs []error
for _, provider := range providers.Collect() {
img, err := provider.Provide(ctx, imgStr, cfg.AdditionalMetadata...)
if err != nil {
return nil, cleanup, err
}
case image.OciDirectorySource:
if cfg.Platform != nil {
return nil, cleanup, platformSelectionUnsupported
errs = append(errs, err)
}
provider = oci.NewProviderFromPath(imgStr, tempDirGenerator)
case image.OciTarballSource:
if cfg.Platform != nil {
return nil, cleanup, platformSelectionUnsupported
if img != nil {
return img, nil
}
provider = oci.NewProviderFromTarball(imgStr, tempDirGenerator)
case image.OciRegistrySource:
defaultPlatformIfNil(&cfg)
provider = oci.NewProviderFromRegistry(imgStr, tempDirGenerator, cfg.Registry, cfg.Platform)
case image.SingularitySource:
if cfg.Platform != nil {
return nil, cleanup, platformSelectionUnsupported
}
provider = sif.NewProviderFromPath(imgStr, tempDirGenerator)
default:
return nil, cleanup, fmt.Errorf("unable to determine image source")
}
return provider, cleanup, nil
}

// defaultPlatformIfNil sets the platform to use the host's architecture
// if no platform was specified. The OCI registry provider uses "linux/amd64"
// as a hard-coded default platform, which has surprised customers
// running stereoscope on non-amd64 hosts. If platform is already
// set on the config, or the code can't generate a matching platform,
// do nothing.
func defaultPlatformIfNil(cfg *config) {
if cfg.Platform == nil {
p, err := image.NewPlatform(fmt.Sprintf("linux/%s", runtime.GOARCH))
if err == nil {
cfg.Platform = p
}
}
}

// GetImage parses the user provided image string and provides an image object;
// note: the source where the image should be referenced from is automatically inferred.
func GetImage(ctx context.Context, userStr string, options ...Option) (*image.Image, error) {
source, imgStr, err := image.DetectSource(userStr)
if err != nil {
return nil, err
}
return GetImageFromSource(ctx, imgStr, source, options...)
return nil, fmt.Errorf("unable to detect input for '%s', errs: %w", imgStr, errors.Join(errs...))
}

func SetLogger(logger logger.Logger) {
Expand Down
11 changes: 0 additions & 11 deletions config.go

This file was deleted.

31 changes: 31 additions & 0 deletions deprecated.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package stereoscope

import (
"strings"

"github.com/anchore/stereoscope/tagged"
)

// ExtractSchemeSource parses a string with any colon-delimited prefix and validates it against the set
// of known provider tags, returning a valid source name and input string to use for GetImageFromSource
//
// NOTE: since it is now possible to select which providers to use, using schemes
// in the user input text is not necessary and should be avoided due to some ambiguity this introduces
func ExtractSchemeSource[T comparable](schemeTags tagged.Values[T], userInput string) (scheme, newInput string) {
const SchemeSeparator = ":"

parts := strings.SplitN(userInput, SchemeSeparator, 2)
if len(parts) < 2 {
return "", userInput
}
// the user may have provided a source hint (or this is a split from a path or docker image reference, we aren't certain yet)
sourceHint := parts[0]
sourceHint = strings.TrimSpace(strings.ToLower(sourceHint))
// validate the hint against the possible tags
if !schemeTags.HasTag(sourceHint) {
// did not have any matching tags, scheme is not a valid provider scheme
return "", userInput
}

return sourceHint, parts[1]
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/anchore/stereoscope

go 1.19
go 1.20
kzantow marked this conversation as resolved.
Show resolved Hide resolved

require (
github.com/GoogleCloudPlatform/docker-credential-gcr v2.0.5+incompatible
Expand Down
24 changes: 24 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
package stereoscope

import (
"fmt"

"github.com/anchore/stereoscope/pkg/image"
)

type Option func(*config) error

type config struct {
Registry image.RegistryOptions
AdditionalMetadata []image.AdditionalMetadata
Platform *image.Platform
}

func applyOptions(cfg *config, options ...Option) error {
for _, option := range options {
if option == nil {
continue
}
if err := option(cfg); err != nil {
return fmt.Errorf("unable to parse option: %w", err)
}
}
return nil
}
Loading
Loading