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:
parent
180eae5748
commit
de0d8e04f8
4 changed files with 192 additions and 40 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
28
pkg/imagedataloader/utils.go
Normal file
28
pkg/imagedataloader/utils.go
Normal 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
|
||||
}
|
Loading…
Add table
Reference in a new issue