1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-03-06 07:57:07 +00:00

feat: add image data fetching support (#12134)

This commit is contained in:
Vishal Choudhary 2025-02-10 18:33:01 +05:30 committed by GitHub
parent 180eae5748
commit de0d8e04f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 192 additions and 40 deletions

View file

@ -127,4 +127,7 @@ func Test_impl_get_imagedata_string(t *testing.T) {
img := out.Value().(*imagedataloader.ImageData)
assert.Equal(t, img.Tag, "latest")
assert.True(t, strings.HasPrefix(img.ResolvedImage, "ghcr.io/kyverno/kyverno:latest@sha256:"))
assert.True(t, img.ConfigData != nil)
assert.True(t, img.Manifest != nil)
assert.True(t, img.ImageIndex != nil)
}

View file

@ -4,12 +4,20 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"github.com/google/go-containerregistry/pkg/name"
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/remote"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
k8scorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
var (
maxReferrersCount = 50
maxPayloadSize = int64(10 * 1000 * 1000) // 10 MB
)
type imagedatafetcher struct {
// TODO: Add caching and prefetching
@ -33,34 +41,24 @@ func New(lister k8scorev1.SecretInterface, opts ...Option) (*imagedatafetcher, e
}, nil
}
type ImageData struct {
Image string `json:"image,omitempty"`
ResolvedImage string `json:"resolvedImage,omitempty"`
Registry string `json:"registry,omitempty"`
Repository string `json:"repository,omitempty"`
Tag string `json:"tag,omitempty"`
Digest string `json:"digest,omitempty"`
ImageIndex interface{} `json:"imageIndex,omitempty"`
Manifest interface{} `json:"manifest,omitempty"`
ConfigData interface{} `json:"config,omitempty"`
}
func (i *imagedatafetcher) FetchImageData(ctx context.Context, image string, options ...Option) (*ImageData, error) {
img := ImageData{
Image: image,
}
remoteOpts, err := i.remoteOptions(ctx, i.lister, options...)
var err error
img.remoteOpts, err = i.remoteOptions(ctx, i.lister, options...)
if err != nil {
return nil, err
}
nameOpts := nameOptions(options...)
ref, err := name.ParseReference(image, nameOpts...)
img.nameOpts = nameOptions(options...)
ref, err := name.ParseReference(image, img.nameOpts...)
if err != nil {
return nil, err
}
img.nameRef = ref
img.Registry = ref.Context().RegistryStr()
img.Repository = ref.Context().RepositoryStr()
@ -70,33 +68,26 @@ func (i *imagedatafetcher) FetchImageData(ctx context.Context, image string, opt
img.Digest = ref.Identifier()
}
remoteImg, err := remote.Image(ref, remoteOpts...)
remoteImg, err := remote.Image(ref, img.remoteOpts...)
if err != nil {
return nil, err
}
manifest, err := remoteImg.RawManifest()
img.Manifest, err = remoteImg.Manifest()
if err != nil {
return nil, err
}
if err := json.Unmarshal(manifest, &img.Manifest); err != nil {
return nil, err
}
config, err := remoteImg.RawConfigFile()
img.ConfigData, err = remoteImg.ConfigFile()
if err != nil {
return nil, err
}
if err := json.Unmarshal(config, &img.ConfigData); err != nil {
return nil, err
}
desc, err := remote.Get(ref, remoteOpts...)
desc, err := remote.Get(ref, img.remoteOpts...)
if err != nil {
return nil, err
}
img.desc = desc
if len(img.Digest) == 0 {
img.Digest = desc.Digest.String()
@ -137,3 +128,110 @@ func (i *imagedatafetcher) remoteOptions(ctx context.Context, lister k8scorev1.S
return opts, nil
}
type ImageData struct {
remoteOpts []remote.Option
nameOpts []name.Option
Image string `json:"image,omitempty"`
ResolvedImage string `json:"resolvedImage,omitempty"`
Registry string `json:"registry,omitempty"`
Repository string `json:"repository,omitempty"`
Tag string `json:"tag,omitempty"`
Digest string `json:"digest,omitempty"`
ImageIndex interface{} `json:"imageIndex,omitempty"`
Manifest *gcrv1.Manifest `json:"manifest,omitempty"`
ConfigData *gcrv1.ConfigFile `json:"config,omitempty"`
nameRef name.Reference
desc *remote.Descriptor
referrersManifest *gcrv1.IndexManifest
}
func (i *ImageData) Descriptor() ocispec.Descriptor {
return GCRtoOCISpecDesc(i.desc.Descriptor)
}
func (i *ImageData) loadReferrers() error {
if i.referrersManifest != nil {
return nil
}
referrers, err := remote.Referrers(i.nameRef.Context().Digest(i.Digest), i.remoteOpts...)
if err != nil {
return err
}
referrersDescs, err := referrers.IndexManifest()
if err != nil {
return err
}
// This check ensures that the manifest does not have an abnormal amount of referrers attached to it to protect against compromised images
if len(referrersDescs.Manifests) > maxReferrersCount {
return fmt.Errorf("failed to fetch referrers: to many referrers found, max limit is %d", maxReferrersCount)
}
i.referrersManifest = referrersDescs
return nil
}
func (i *ImageData) FetchRefererrs(artifactType string) ([]gcrv1.Descriptor, error) {
if err := i.loadReferrers(); err != nil {
return nil, err
}
refList := make([]gcrv1.Descriptor, 0)
for _, ref := range i.referrersManifest.Manifests {
if ref.ArtifactType == artifactType {
refList = append(refList, ref)
}
}
return refList, nil
}
func (i *ImageData) FetchReferrerData(desc gcrv1.Descriptor) ([]byte, *gcrv1.Descriptor, error) {
img, err := remote.Image(i.nameRef.Context().Digest(desc.Digest.String()), i.remoteOpts...)
if err != nil {
return nil, nil, err
}
layers, err := img.Layers()
if err != nil {
return nil, nil, err
}
if len(layers) != 1 {
return nil, nil, fmt.Errorf("invalid referrer descriptor, must have only one layer")
}
layer := layers[0]
size, err := layer.Size()
if err != nil {
return nil, nil, err
}
digest, err := layer.Digest()
if err != nil {
return nil, nil, err
}
mediaType, err := layer.MediaType()
if err != nil {
return nil, nil, err
}
layerDesc := &gcrv1.Descriptor{
MediaType: mediaType,
Digest: digest,
Size: size,
}
reader, err := layer.Uncompressed()
if err != nil {
return nil, nil, err
}
b, err := io.ReadAll(io.LimitReader(reader, maxPayloadSize))
return b, layerDesc, err
}

View file

@ -2,35 +2,58 @@ package imagedataloader
import (
"context"
"fmt"
"strings"
"testing"
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/stretchr/testify/assert"
)
var (
image = "ghcr.io/kyverno/test-verify-image:signed"
ctx = context.Background()
)
func Test_ImageDataLoader(t *testing.T) {
idf, err := New(nil)
assert.NoError(t, err)
img, err := idf.FetchImageData(context.TODO(), "ghcr.io/kyverno/kyverno:latest")
img, err := idf.FetchImageData(context.TODO(), image)
assert.NoError(t, err)
assert.Equal(t, img.Image, "ghcr.io/kyverno/kyverno:latest")
assert.Equal(t, img.Image, image)
assert.Equal(t, img.Registry, "ghcr.io")
assert.Equal(t, img.Repository, "kyverno/kyverno")
assert.Equal(t, img.Tag, "latest")
assert.True(t, strings.HasPrefix(img.Digest, "sha256:"))
assert.True(t, strings.HasPrefix(img.ResolvedImage, "ghcr.io/kyverno/kyverno:latest@sha256:"))
assert.Equal(t, img.Repository, "kyverno/test-verify-image")
assert.Equal(t, img.Tag, "signed")
assert.Equal(t, img.Digest, "sha256:b31bfb4d0213f254d361e0079deaaebefa4f82ba7aa76ef82e90b4935ad5b105")
assert.Equal(t, img.ResolvedImage, "ghcr.io/kyverno/test-verify-image:signed@sha256:b31bfb4d0213f254d361e0079deaaebefa4f82ba7aa76ef82e90b4935ad5b105")
img, err = idf.FetchImageData(context.TODO(), "nginx")
assert.NoError(t, err)
indexMediaType := img.ImageIndex.(map[string]interface{})["mediaType"].(string)
assert.Equal(t, indexMediaType, string(types.OCIImageIndex))
fmt.Println(img.ConfigData)
_, ok := img.ConfigData.(map[string]interface{})["architecture"]
assert.True(t, ok)
arch := img.ConfigData.Architecture
assert.True(t, len(arch) > 0)
manifestMediaType := img.Manifest.(map[string]interface{})["mediaType"].(string)
assert.Equal(t, manifestMediaType, string(types.OCIManifestSchema1))
manifestMediaType := img.Manifest.MediaType
assert.Equal(t, manifestMediaType, types.OCIManifestSchema1)
}
func Test_Referrers(t *testing.T) {
idf, err := New(nil)
assert.NoError(t, err)
img, err := idf.FetchImageData(context.TODO(), image)
assert.NoError(t, err)
refList, err := img.FetchRefererrs("application/vnd.cncf.notary.signature")
assert.NoError(t, err)
assert.Equal(t, len(refList), 2)
assert.Equal(t, refList[0].ArtifactType, "application/vnd.cncf.notary.signature")
assert.Equal(t, string(refList[0].MediaType), "application/vnd.oci.image.manifest.v1+json")
data, desc, err := img.FetchReferrerData(refList[0])
assert.NoError(t, err)
assert.True(t, len(data) > 0)
assert.Equal(t, string(desc.MediaType), "application/jose+json")
}

View file

@ -0,0 +1,28 @@
package imagedataloader
import (
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
func GCRtoOCISpecDesc(v1desc gcrv1.Descriptor) ocispec.Descriptor {
ociDesc := ocispec.Descriptor{
MediaType: string(v1desc.MediaType),
Digest: digest.Digest(v1desc.Digest.String()),
Size: v1desc.Size,
URLs: v1desc.URLs,
Annotations: v1desc.Annotations,
Data: v1desc.Data,
ArtifactType: v1desc.ArtifactType,
}
if v1desc.Platform != nil {
ociDesc.Platform = &ocispec.Platform{
Architecture: v1desc.Platform.Architecture,
OS: v1desc.Platform.OS,
OSVersion: v1desc.Platform.OSVersion,
}
}
return ociDesc
}