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

DRAFT: Improve architecture selection #187

Closed
wants to merge 4 commits into from
Closed
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
48 changes: 44 additions & 4 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package stereoscope
import (
"context"
"fmt"
"os"
"runtime"

"github.com/wagoodman/go-partybus"
Expand Down Expand Up @@ -103,15 +104,12 @@ func selectImageProvider(imgStr string, source image.Source, cfg config) (image.
var provider image.Provider
tempDirGenerator := rootTempDirGenerator.NewGenerator()

if err := setPlatform(source, &cfg, runtime.GOARCH); err != nil {
return nil, err
}

switch source {
case image.DockerTarballSource:
// note: the imgStr is the path on disk to the tar file
provider = docker.NewProviderFromTarball(imgStr, tempDirGenerator)
case image.DockerDaemonSource:
// TODO: only specify platform if --platform was passed
c, err := dockerClient.GetClient()
if err != nil {
return nil, err
Expand All @@ -121,6 +119,7 @@ func selectImageProvider(imgStr string, source image.Source, cfg config) (image.
return nil, err
}
case image.PodmanDaemonSource:
// TODO: only specify platform if --platform was passed
c, err := podman.GetClient()
if err != nil {
return nil, err
Expand All @@ -134,6 +133,16 @@ func selectImageProvider(imgStr string, source image.Source, cfg config) (image.
case image.OciTarballSource:
provider = oci.NewProviderFromTarball(imgStr, tempDirGenerator)
case image.OciRegistrySource:
// TODO: download manifest, and then do:
// specific platform requested: do that
// no specific platform requested:
// if image is multi-arch:
// try to match host, specify host platform on call to lib
// if not multi-arch, pull the single arch regardless.

if err := setPlatform(source, &cfg, runtime.GOARCH); err != nil {
return nil, err
}
provider = oci.NewProviderFromRegistry(imgStr, tempDirGenerator, cfg.Registry, cfg.Platform)
case image.SingularitySource:
provider = sif.NewProviderFromPath(imgStr, tempDirGenerator)
Expand Down Expand Up @@ -179,6 +188,37 @@ func GetImage(ctx context.Context, userStr string, options ...Option) (*image.Im
return GetImageFromSource(ctx, imgStr, source, options...)
}

func GetImageIndex(ctx context.Context, userStr string, options ...Option) ([]byte, error) {
log.Debugf("userStr is %q", userStr)
_, imgStr, err := image.DetectSource(userStr)
if err != nil {
return nil, err
}

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)
}
}
log.Debugf("image str is %s", imgStr)
tempDirGenerator := rootTempDirGenerator.NewGenerator()
provider := oci.NewProviderFromRegistry(imgStr, tempDirGenerator, cfg.Registry, cfg.Platform)
index, err := provider.ProvideIndex(ctx, cfg.AdditionalMetadata...)
if err != nil {
return nil, err
}
m, err := index.IndexManifest()
for _, man := range m.Manifests {
fmt.Fprintf(os.Stderr, "manifest was: %+v", man)
}

return index.RawManifest()
}

func SetLogger(logger logger.Logger) {
log.Log = logger
}
Expand Down
48 changes: 48 additions & 0 deletions pkg/image/oci/registry_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,23 @@ func NewProviderFromRegistry(imgStr string, tmpDirGen *file.TempDirGenerator, re
}
}

func (p *RegistryImageProvider) ProvideIndex(ctx context.Context, userMetadata ...image.AdditionalMetadata) (containerregistryV1.ImageIndex, error) {
log.Debugf("pulling index directly from registry image=%q", p.imageStr)
ref, err := name.ParseReference(p.imageStr, prepareReferenceOptions(p.registryOptions)...)
if err != nil {
return nil, err
}

index, err := remote.Index(ref, prepareRemoteOptions(ctx, ref, p.registryOptions, p.platform)...)
if err != nil {
return nil, err
}
return index, nil
}

// Provide an image object that represents the cached docker image tar fetched a registry.
func (p *RegistryImageProvider) Provide(ctx context.Context, userMetadata ...image.AdditionalMetadata) (*image.Image, error) {
// TODO: at this point, do I know whether platform was requested? I don't think so.
log.Debugf("pulling image info directly from registry image=%q", p.imageStr)

imageTempDir, err := p.tmpDirGen.NewDirectory("oci-registry-image")
Expand All @@ -48,6 +63,19 @@ func (p *RegistryImageProvider) Provide(ctx context.Context, userMetadata ...ima
return nil, fmt.Errorf("unable to parse registry reference=%q: %+v", p.imageStr, err)
}

// TODO: change platform logic here.

// TODO: download index, and then do:
// specific platform requested: do that
// no specific platform requested:
// if image is multi-arch:
// try to match host, specify host platform on call to lib
// if not multi-arch, pull the single arch regardless.
// probably call https://github.com/google/go-containerregistry/blob/e61c5190e39a37cd78c8c6d71f50a709a9d7eab4/pkg/v1/remote/index.go#L47
// if arch requested, pull that or fail if unavailable.
// if multi-arch index, and host arch available, pull that
// if multi-arch index, and host arch not available, error and say "ambiguous request"; enumerate available architectures
// if single-arch image, pull that image, but log debug
descriptor, err := remote.Get(ref, prepareRemoteOptions(ctx, ref, p.registryOptions, p.platform)...)
if err != nil {
return nil, fmt.Errorf("failed to get image descriptor from registry: %+v", err)
Expand Down Expand Up @@ -84,6 +112,26 @@ func (p *RegistryImageProvider) Provide(ctx context.Context, userMetadata ...ima
return image.New(img, p.tmpDirGen, imageTempDir, metadata...), nil
}

func (p *RegistryImageProvider) archIsAvailable(arch string, ref name.Reference, options []remote.Option) (bool, error) {
index, err := remote.Index(ref, options...)
if err != nil {
return false, err
}
manifest, err := index.IndexManifest()
if err != nil {
return false, err
}
for _, m := range manifest.Manifests {
if m.Platform == nil {
continue
}
if m.Platform.Architecture == arch {
return true, nil
}
}
return false, nil
}

func prepareReferenceOptions(registryOptions image.RegistryOptions) []name.Option {
var options []name.Option
if registryOptions.InsecureUseHTTP {
Expand Down
81 changes: 81 additions & 0 deletions test/integration/oci_registry_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package integration
import (
"context"
"fmt"
"runtime"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -44,3 +45,83 @@ func TestOciRegistrySourceMetadata(t *testing.T) {
assert.Equal(t, "index.docker.io/"+ref, img.Metadata.RepoDigests[0])
assert.Equal(t, []byte(rawManifest), img.Metadata.RawManifest)
}

func TestOciRegistryArchHandling(t *testing.T) {
// possible platforms are at
// https://github.com/docker/cli/blob/1f6a1a438c4ae426e446f17848114e58072af2bb/cli/command/manifest/util.go#L22
tests := []struct {
name string
wantErr bool
userStr string
options []stereoscope.Option
wantArch string
}{
{
name: "arch requested, arch available",
userStr: "registry:alpine:3.18.0",
options: []stereoscope.Option{
stereoscope.WithPlatform("linux/amd64"),
},
},
{
name: "arch requested, arch unavailable",
userStr: "registry:alpine:3.18.0",
wantErr: true,
options: []stereoscope.Option{
stereoscope.WithPlatform("linux/mips64"),
},
},
{
name: "multi-arch index, no arch requested, host arch available",
userStr: "registry:alpine:3.18.0",
wantArch: runtime.GOARCH,
},
{
name: "multi-arch index, no arch requested, host arch unavailable",
wantErr: true,
userStr: fmt.Sprintf("registry:%s", getMultiArchImageNotContainingHostArch()),
// TODO: this is a really hard one
// maybe make some test images?
},
{
name: "single arch index",
wantErr: false,
userStr: fmt.Sprintf("registry:%s", getSingleArchImageNotMatchingHostArch()),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
img, err := stereoscope.GetImage(context.TODO(), tt.userStr, tt.options...)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
if tt.wantArch != "" {
assert.Equal(t, tt.wantArch, img.Metadata.Architecture)
}
if img != nil {
t.Logf("%s", img.Metadata.Architecture)
}
})
}
}

func getSingleArchImageNotMatchingHostArch() string {
// TODO: publish test images to anchore/test_images
// and use those
if runtime.GOARCH != "amd64" {
return "rancher/busybox:1.31.1"
}
return ""
}

func getMultiArchImageNotContainingHostArch() string {
// TODO: publish test images to anchore/test_images
// and use those
if runtime.GOARCH == "arm64" {
return "localhost:5001/learn-multi-arch:no-arm"
}
return "localhost:5001/learn-multi-arch:no-amd64"
}