Skip to content

Commit 506005c

Browse files
authored
fix(search): use index descriptor platform when config platform is empty (#4221)
Multi-arch image indexes (including ORAS artifact indexes) could carry OS/arch on index manifest descriptors rather than in the config blob. GraphQL search omitted platform for those manifests, leaving the UI OS/Arch dropdown empty. Fall back to the index descriptor when config platform is unset, while still preferring config when both are present. Apply the same resolution when resolving index tags by platform or digest in resolveImageData. Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
1 parent efd1dc0 commit 506005c

5 files changed

Lines changed: 413 additions & 4 deletions

File tree

pkg/extensions/search/convert/convert_internal_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"testing"
99

1010
"github.com/99designs/gqlgen/graphql"
11+
godigest "github.com/opencontainers/go-digest"
1112
ispec "github.com/opencontainers/image-spec/specs-go/v1"
1213
. "github.com/smartystreets/goconvey/convey"
1314

@@ -331,3 +332,57 @@ func TestCVEConvert(t *testing.T) {
331332
})
332333
})
333334
}
335+
336+
func TestManifestPlatformHelpers(t *testing.T) {
337+
Convey("manifest platform helpers", t, func() {
338+
Convey("IspecPlatformEmpty", func() {
339+
So(IspecPlatformEmpty(ispec.Platform{}), ShouldBeTrue)
340+
So(IspecPlatformEmpty(ispec.Platform{OS: "linux"}), ShouldBeFalse)
341+
So(IspecPlatformEmpty(ispec.Platform{Architecture: "amd64"}), ShouldBeFalse)
342+
So(IspecPlatformEmpty(ispec.Platform{OS: "linux", Architecture: "amd64"}), ShouldBeFalse)
343+
So(IspecPlatformEmpty(ispec.Platform{Variant: "v8"}), ShouldBeTrue)
344+
})
345+
346+
Convey("IndexPlatformByDigest", func() {
347+
amd64Digest := godigest.FromString("sha256:amd64manifest")
348+
arm64Digest := godigest.FromString("sha256:arm64manifest")
349+
350+
So(IndexPlatformByDigest(nil), ShouldResemble, map[string]ispec.Platform{})
351+
So(IndexPlatformByDigest(&ispec.Index{}), ShouldResemble, map[string]ispec.Platform{})
352+
353+
index := &ispec.Index{
354+
Manifests: []ispec.Descriptor{
355+
{Digest: amd64Digest, Platform: &ispec.Platform{OS: "linux", Architecture: "amd64"}},
356+
{Digest: arm64Digest},
357+
{Digest: godigest.FromString("sha256:noplatform")},
358+
},
359+
}
360+
361+
platforms := IndexPlatformByDigest(index)
362+
So(platforms, ShouldHaveLength, 1)
363+
So(platforms[amd64Digest.String()], ShouldResemble, ispec.Platform{OS: "linux", Architecture: "amd64"})
364+
})
365+
366+
Convey("ResolveManifestPlatform", func() {
367+
configPlatform := ispec.Platform{OS: "windows", Architecture: "amd64"}
368+
indexPlatform := ispec.Platform{OS: "linux", Architecture: "arm64"}
369+
indexPlatforms := map[string]ispec.Platform{
370+
"sha256:manifest": indexPlatform,
371+
}
372+
373+
So(ResolveManifestPlatform(configPlatform, "sha256:manifest", indexPlatforms), ShouldResemble, configPlatform)
374+
So(ResolveManifestPlatform(ispec.Platform{}, "sha256:manifest", indexPlatforms), ShouldResemble, indexPlatform)
375+
So(ResolveManifestPlatform(ispec.Platform{}, "sha256:missing", indexPlatforms), ShouldResemble, ispec.Platform{})
376+
So(ResolveManifestPlatform(ispec.Platform{}, "sha256:missing", nil), ShouldResemble, ispec.Platform{})
377+
})
378+
379+
Convey("platformSummaryIsEmpty", func() {
380+
So(platformSummaryIsEmpty(nil), ShouldBeTrue)
381+
So(platformSummaryIsEmpty(&gql_generated.Platform{}), ShouldBeTrue)
382+
So(platformSummaryIsEmpty(&gql_generated.Platform{Os: ref(""), Arch: ref("")}), ShouldBeTrue)
383+
So(platformSummaryIsEmpty(&gql_generated.Platform{Os: ref("linux")}), ShouldBeFalse)
384+
So(platformSummaryIsEmpty(&gql_generated.Platform{Arch: ref("amd64")}), ShouldBeFalse)
385+
So(platformSummaryIsEmpty(&gql_generated.Platform{Os: ref("linux"), Arch: ref("amd64")}), ShouldBeFalse)
386+
})
387+
})
388+
}

pkg/extensions/search/convert/convert_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,143 @@ func TestTaggedTimestamp(t *testing.T) {
462462
So(imageSummary.TaggedTimestamp, ShouldNotBeNil)
463463
So(*imageSummary.TaggedTimestamp, ShouldEqual, pushTime)
464464
})
465+
466+
Convey("ImageIndex uses index descriptor platform when config platform is empty", func() {
467+
amd64Digest := godigest.FromString("sha256:amd64manifest")
468+
arm64Digest := godigest.FromString("sha256:arm64manifest")
469+
indexDigest := godigest.FromString("sha256:indexdigest")
470+
471+
imageMeta := mTypes.ImageMeta{
472+
MediaType: ispec.MediaTypeImageIndex,
473+
Digest: indexDigest,
474+
Index: &ispec.Index{
475+
MediaType: ispec.MediaTypeImageIndex,
476+
Manifests: []ispec.Descriptor{
477+
{
478+
MediaType: ispec.MediaTypeImageManifest,
479+
Digest: amd64Digest,
480+
Size: 572,
481+
Platform: &ispec.Platform{OS: "linux", Architecture: "amd64"},
482+
},
483+
{
484+
MediaType: ispec.MediaTypeImageManifest,
485+
Digest: arm64Digest,
486+
Size: 572,
487+
Platform: &ispec.Platform{OS: "linux", Architecture: "arm64"},
488+
},
489+
},
490+
},
491+
Manifests: []mTypes.ManifestMeta{
492+
{
493+
Digest: amd64Digest,
494+
Size: 572,
495+
Config: ispec.Image{},
496+
Manifest: ispec.Manifest{MediaType: ispec.MediaTypeImageManifest},
497+
},
498+
{
499+
Digest: arm64Digest,
500+
Size: 572,
501+
Config: ispec.Image{},
502+
Manifest: ispec.Manifest{MediaType: ispec.MediaTypeImageManifest},
503+
},
504+
},
505+
}
506+
507+
repoMeta := mTypes.RepoMeta{
508+
Name: "hello-artifact",
509+
Tags: map[string]mTypes.Descriptor{
510+
"v2": {
511+
Digest: indexDigest.String(),
512+
MediaType: ispec.MediaTypeImageIndex,
513+
},
514+
},
515+
}
516+
517+
fullImageMeta := convert.GetFullImageMeta("v2", repoMeta, imageMeta)
518+
519+
imageSummary, _, err := convert.ImageIndex2ImageSummary(ctx, fullImageMeta)
520+
So(err, ShouldBeNil)
521+
So(len(imageSummary.Manifests), ShouldEqual, 2)
522+
So(*imageSummary.Manifests[0].Platform.Os, ShouldEqual, "linux")
523+
So(*imageSummary.Manifests[0].Platform.Arch, ShouldEqual, "amd64")
524+
So(*imageSummary.Manifests[1].Platform.Os, ShouldEqual, "linux")
525+
So(*imageSummary.Manifests[1].Platform.Arch, ShouldEqual, "arm64")
526+
})
527+
528+
Convey("ImageIndex prefers config platform over index descriptor", func() {
529+
manifestDigest := godigest.FromString("sha256:manifest")
530+
531+
imageMeta := mTypes.ImageMeta{
532+
MediaType: ispec.MediaTypeImageIndex,
533+
Digest: godigest.FromString("sha256:indexdigest"),
534+
Index: &ispec.Index{
535+
MediaType: ispec.MediaTypeImageIndex,
536+
Manifests: []ispec.Descriptor{
537+
{
538+
MediaType: ispec.MediaTypeImageManifest,
539+
Digest: manifestDigest,
540+
Platform: &ispec.Platform{OS: "linux", Architecture: "arm64"},
541+
},
542+
},
543+
},
544+
Manifests: []mTypes.ManifestMeta{
545+
{
546+
Digest: manifestDigest,
547+
Config: ispec.Image{Platform: ispec.Platform{OS: "linux", Architecture: "amd64"}},
548+
Manifest: ispec.Manifest{
549+
MediaType: ispec.MediaTypeImageManifest,
550+
Config: ispec.Descriptor{Digest: godigest.FromString("sha256:config"), Size: 1},
551+
},
552+
},
553+
},
554+
}
555+
556+
repoMeta := mTypes.RepoMeta{
557+
Name: "repo",
558+
Tags: map[string]mTypes.Descriptor{
559+
"v2": {Digest: imageMeta.Digest.String(), MediaType: ispec.MediaTypeImageIndex},
560+
},
561+
}
562+
563+
imageSummary, _, err := convert.ImageIndex2ImageSummary(ctx, convert.GetFullImageMeta("v2", repoMeta, imageMeta))
564+
So(err, ShouldBeNil)
565+
So(*imageSummary.Manifests[0].Platform.Os, ShouldEqual, "linux")
566+
So(*imageSummary.Manifests[0].Platform.Arch, ShouldEqual, "amd64")
567+
})
568+
569+
Convey("ImageIndex leaves platform empty without index descriptor platform", func() {
570+
manifestDigest := godigest.FromString("sha256:manifest")
571+
572+
imageMeta := mTypes.ImageMeta{
573+
MediaType: ispec.MediaTypeImageIndex,
574+
Digest: godigest.FromString("sha256:indexdigest"),
575+
Index: &ispec.Index{
576+
MediaType: ispec.MediaTypeImageIndex,
577+
Manifests: []ispec.Descriptor{
578+
{MediaType: ispec.MediaTypeImageManifest, Digest: manifestDigest},
579+
},
580+
},
581+
Manifests: []mTypes.ManifestMeta{
582+
{
583+
Digest: manifestDigest,
584+
Config: ispec.Image{},
585+
Manifest: ispec.Manifest{MediaType: ispec.MediaTypeImageManifest},
586+
},
587+
},
588+
}
589+
590+
repoMeta := mTypes.RepoMeta{
591+
Name: "repo",
592+
Tags: map[string]mTypes.Descriptor{
593+
"v2": {Digest: imageMeta.Digest.String(), MediaType: ispec.MediaTypeImageIndex},
594+
},
595+
}
596+
597+
imageSummary, _, err := convert.ImageIndex2ImageSummary(ctx, convert.GetFullImageMeta("v2", repoMeta, imageMeta))
598+
So(err, ShouldBeNil)
599+
So(*imageSummary.Manifests[0].Platform.Os, ShouldEqual, "")
600+
So(*imageSummary.Manifests[0].Platform.Arch, ShouldEqual, "")
601+
})
465602
})
466603
}
467604

pkg/extensions/search/convert/metadb.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,8 @@ func ImageIndex2ImageSummary(ctx context.Context, fullImageMeta mTypes.FullImage
424424
taggedTimestamp = pushTimestamp
425425
}
426426

427+
indexPlatforms := IndexPlatformByDigest(fullImageMeta.Index)
428+
427429
for _, imageManifest := range fullImageMeta.Manifests {
428430
imageManifestSummary, manifestBlobs, err := ImageManifest2ImageSummary(ctx, mTypes.FullImageMeta{
429431
Repo: fullImageMeta.Repo,
@@ -459,7 +461,16 @@ func ImageIndex2ImageSummary(ctx context.Context, fullImageMeta mTypes.FullImage
459461

460462
indexSize += manifestSize
461463

462-
manifestSummaries = append(manifestSummaries, imageManifestSummary.Manifests[0])
464+
manifestSummary := imageManifestSummary.Manifests[0]
465+
if platformSummaryIsEmpty(manifestSummary.Platform) {
466+
resolved := ResolveManifestPlatform(imageManifest.Config.Platform, imageManifest.Digest.String(), indexPlatforms)
467+
if !IspecPlatformEmpty(resolved) {
468+
gqlPlatform := getPlatform(resolved)
469+
manifestSummary.Platform = &gqlPlatform
470+
}
471+
}
472+
473+
manifestSummaries = append(manifestSummaries, manifestSummary)
463474
}
464475

465476
signaturesInfo := GetSignaturesInfo(isSigned, fullImageMeta.Signatures)
@@ -620,6 +631,49 @@ func getPlatform(platform ispec.Platform) gql_generated.Platform {
620631
}
621632
}
622633

634+
func IspecPlatformEmpty(platform ispec.Platform) bool {
635+
return platform.OS == "" && platform.Architecture == ""
636+
}
637+
638+
func IndexPlatformByDigest(index *ispec.Index) map[string]ispec.Platform {
639+
result := make(map[string]ispec.Platform)
640+
if index == nil {
641+
return result
642+
}
643+
644+
for i := range index.Manifests {
645+
desc := index.Manifests[i]
646+
if desc.Platform == nil {
647+
continue
648+
}
649+
650+
result[desc.Digest.String()] = *desc.Platform
651+
}
652+
653+
return result
654+
}
655+
656+
func ResolveManifestPlatform(configPlatform ispec.Platform, digest string, indexPlatforms map[string]ispec.Platform,
657+
) ispec.Platform {
658+
if !IspecPlatformEmpty(configPlatform) {
659+
return configPlatform
660+
}
661+
662+
if platform, ok := indexPlatforms[digest]; ok {
663+
return platform
664+
}
665+
666+
return configPlatform
667+
}
668+
669+
func platformSummaryIsEmpty(platform *gql_generated.Platform) bool {
670+
if platform == nil {
671+
return true
672+
}
673+
674+
return deref(platform.Os, "") == "" && deref(platform.Arch, "") == ""
675+
}
676+
623677
func getArch(arch string, variant string) string {
624678
if variant != "" {
625679
arch = arch + "/" + variant

pkg/extensions/search/resolver.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -479,13 +479,21 @@ func resolveImageData(ctx context.Context, imageInput gql_generated.ImageInput,
479479
return gql_generated.ImageInput{}, err
480480
}
481481

482+
indexPlatforms := convert.IndexPlatformByDigest(imageMeta.Index)
483+
482484
for _, manifest := range imageMeta.Manifests {
485+
platform := convert.ResolveManifestPlatform(
486+
manifest.Config.Platform,
487+
manifest.Digest.String(),
488+
indexPlatforms,
489+
)
490+
483491
if manifest.Digest.String() == dderef(imageInput.Digest) ||
484-
isMatchingPlatform(manifest.Config.Platform, dderef(imageInput.Platform)) {
492+
isMatchingPlatform(platform, dderef(imageInput.Platform)) {
485493
imageInput.Digest = ref(manifest.Digest.String())
486494
imageInput.Platform = &gql_generated.PlatformInput{
487-
Os: ref(manifest.Config.OS),
488-
Arch: ref(getArch(manifest.Config.Platform)),
495+
Os: ref(platform.OS),
496+
Arch: ref(getArch(platform)),
489497
}
490498

491499
return imageInput, nil

0 commit comments

Comments
 (0)