Files
zot/pkg/test/image-utils/images.go
T
Andrei Aaron b7ab9dab16 fix (metadb): make sure metadb statistics are initialized on image download, and minor metadb fixes for Docker v2 manifest compatibility (#3545)
fix: make sure metadb statistics are initialized on image download, and minor metadb fixes for Docker v2 manifest compatibility

Looking into potential causes of https://github.com/project-zot/zot/issues/3163

1. One possible reason is the statistics were not properly initialized in the first place because of (unknown and/or unavoidable) errors on image push.
To workaround this add logic to initialize the statistics on the call to download them.

2. Some images have the download statistics while others dont, one cause could be a bug in the logic handling manifest mediatypes in the search extension.
Add compatibility checks for Docker v2 manifest types in metadb convert functions, and more tests for covering the Docker mediatype use case.

Side fixes:
- Ensure PushedBy Statistics entries are properly initialized in SetRepoReference
- Fix and issue in the image upload test functions, they were uploading docker images with oci mediatypes in call headers

Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
2025-11-18 20:56:44 +02:00

609 lines
17 KiB
Go

package image
import (
"crypto/rand"
"encoding/json"
mathRand "math/rand"
"strconv"
"time"
docker "github.com/distribution/distribution/v3/manifest/schema2"
godigest "github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
"zotregistry.dev/zot/v2/pkg/common"
mTypes "zotregistry.dev/zot/v2/pkg/meta/types"
storageConstants "zotregistry.dev/zot/v2/pkg/storage/constants"
)
const (
TestFakeSignatureArtType = "application/test.fake.signature"
)
// LayerBuilder abstracts the first step in creating an OCI image, specifying the layers of the image.
type LayerBuilder interface {
// LayerBlobs sets the image layers from the gives blobs array, adding a default zipped layer media type.
LayerBlobs(layers [][]byte) ConfigBuilder
// Layers sets the given layers to the built image
Layers(layers []Layer) ConfigBuilder
// RandomLayers generates `count` layers with the given size and initialises them with random values
// and a default zipped layer media type.
RandomLayers(count, size int) ConfigBuilder
// EmptyLayer adds a single empty json layer semnifying no layers.
EmptyLayer() ConfigBuilder
// DefaultLayers adds predefined default layers.
DefaultLayers() ConfigBuilder
// VulnerableLayers adds layers that contains known CVE's.
VulnerableLayers() VulnerableConfigBuilder
}
// ConfigBuilder abstracts the second step in creating an OCI image, specifying the config content of the image.
type ConfigBuilder interface {
// ImageConfig sets the given image config. It updates the "config" field of the image manifest with
// values corresponding to the given image.
ImageConfig(config ispec.Image) ManifestBuilder
// ImageConfig sets an empty json as the images config. It updates the "config" field of the image manifest with
// values corresponding to the empty descriptor.
EmptyConfig() ManifestBuilder
// ArtifactConfig sets an empty json as the content of the image config and sets it's media type (described by
// the Config field of the image manifest) to the given artifact type. This will make the created image
// an OCI artifact.
// (see: https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidelines-for-artifact-usage)
ArtifactConfig(artifactType string) ManifestBuilder
// PlatformConfig is used when we're interesting in specifying only the platform of a manifest.
// Other fields of the config are random.
PlatformConfig(architecture, os string) ManifestBuilder
// DefaultConfig sets the default config, platform linux/amd64.
DefaultConfig() ManifestBuilder
// CustomConfigBlob will set a custom blob as the image config without other checks.
CustomConfigBlob(configBlob []byte, mediaType string) ManifestBuilder
// RandomConfig sets a randomly generated config.
RandomConfig() ManifestBuilder
}
// VulnerableConfigBuilder abstracts specifying the config of an vulnerable OCI image.
// Keeping the RootFS field consistent with the vulnerable layers.
type VulnerableConfigBuilder interface {
// VulnerableConfig sets the given config while keeping the correct RootFS values for the
// vulnerable layer set earlier. This allows scan tools to find CVE's
VulnerableConfig(config ispec.Image) ManifestBuilder
// DefaultVulnConfig sets default config of the vulnerable image
DefaultVulnConfig() ManifestBuilder
// RandomVulnConfig sets the keeping the correct RootFS values for the
// vulnerable layer set earlier. This allows scan tools to find CVE's
RandomVulnConfig() ManifestBuilder
}
// ManifestBuilder abstracts creating the manifest of the image.
type ManifestBuilder interface {
// Subject sets the subject of the image manifest.
Subject(subject *ispec.Descriptor) ManifestBuilder
// ArtifactType sets the artifact type field on the image manifest,
// (see: https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidelines-for-artifact-usage)
ArtifactType(artifactType string) ManifestBuilder
// Annotations sets the annotations field on the image manifest.
Annotations(annotations map[string]string) ManifestBuilder
Build() Image
}
type Image struct {
Manifest ispec.Manifest
Config ispec.Image
Layers [][]byte
digestAlgorithm godigest.Algorithm
ConfigDescriptor ispec.Descriptor
ManifestDescriptor ispec.Descriptor
}
func (img *Image) Digest() godigest.Digest {
if img.ManifestDescriptor.Digest != "" {
return img.ManifestDescriptor.Digest
}
// when we'll migrate all code to the new format of creating images we can replace this with
// the value from manifestDescriptor
blob, err := json.Marshal(img.Manifest)
if err != nil {
panic("unreachable: ispec.Manifest should always be marshable")
}
digestAlgorithm := img.digestAlgorithm
if digestAlgorithm == "" {
digestAlgorithm = godigest.Canonical
}
return digestAlgorithm.FromBytes(blob)
}
func (img *Image) DigestStr() string {
return img.Digest().String()
}
func (img *Image) DigestForAlgorithm(digestAlgorithm godigest.Algorithm) godigest.Digest {
blob, err := json.Marshal(img.Manifest)
if err != nil {
panic("unreachable: ispec.Manifest should always be marshable")
}
return digestAlgorithm.FromBytes(blob)
}
func (img *Image) Size() int {
size := img.ConfigDescriptor.Size + img.ManifestDescriptor.Size
for _, layer := range img.Manifest.Layers {
size += layer.Size
}
return int(size)
}
func (img Image) Descriptor() ispec.Descriptor {
return ispec.Descriptor{
MediaType: img.ManifestDescriptor.MediaType,
Digest: img.ManifestDescriptor.Digest,
Size: img.ManifestDescriptor.Size,
}
}
func (img Image) DescriptorRef() *ispec.Descriptor {
return &ispec.Descriptor{
MediaType: img.ManifestDescriptor.MediaType,
Digest: img.ManifestDescriptor.Digest,
Size: img.ManifestDescriptor.Size,
}
}
func (img Image) AsImageMeta() mTypes.ImageMeta {
return mTypes.ImageMeta{
MediaType: img.Manifest.MediaType,
Digest: img.ManifestDescriptor.Digest,
Size: img.ManifestDescriptor.Size,
Manifests: []mTypes.ManifestMeta{
{
Size: img.ManifestDescriptor.Size,
Digest: img.ManifestDescriptor.Digest,
Manifest: img.Manifest,
Config: img.Config,
},
},
}
}
func (img Image) AsDockerImage() Image {
// Convert manifest media type
img.Manifest.MediaType = docker.MediaTypeManifest
// Convert config media type
img.Manifest.Config.MediaType = docker.MediaTypeImageConfig
img.ConfigDescriptor.MediaType = docker.MediaTypeImageConfig
// Convert layer media types
for i := range img.Manifest.Layers {
img.Manifest.Layers[i].MediaType = docker.MediaTypeLayer
}
manifestBlob, err := json.Marshal(img.Manifest)
if err != nil {
panic("unreachable: ispec.Manifest should always be marshable")
}
img.ManifestDescriptor = ispec.Descriptor{
MediaType: docker.MediaTypeManifest,
Digest: img.digestAlgorithm.FromBytes(manifestBlob),
Size: int64(len(manifestBlob)),
Data: manifestBlob,
Platform: img.ConfigDescriptor.Platform,
}
return img
}
type Layer struct {
Blob []byte
MediaType string
Digest godigest.Digest
}
// CreateImageWith initiates the creation of an OCI image. The creation process starts with
// specifying the layers of the image.
func CreateImageWith() LayerBuilder {
// set default values here
return &BaseImageBuilder{
digestAlgorithm: godigest.Canonical,
}
}
func CreateImageWithDigestAlgorithm(digestAlgorithm godigest.Algorithm) LayerBuilder {
return &BaseImageBuilder{
digestAlgorithm: digestAlgorithm,
}
}
func CreateDefaultImage() Image {
return CreateImageWith().DefaultLayers().DefaultConfig().Build()
}
func CreateDefaultImageWith() ManifestBuilder {
return CreateImageWith().DefaultLayers().DefaultConfig()
}
const (
layerCount = 1
layerSize = 10
)
func CreateRandomImage() Image {
return CreateImageWith().RandomLayers(layerCount, layerSize).RandomConfig().Build()
}
func CreateRandomImageWith() ManifestBuilder {
return CreateImageWith().RandomLayers(layerCount, layerSize).RandomConfig()
}
// CreateDefaultVulnerableImage creates a vulnerable image with the default config.
func CreateDefaultVulnerableImage() Image {
return CreateImageWith().VulnerableLayers().DefaultVulnConfig().Build()
}
func CreateRandomVulnerableImage() Image {
return CreateImageWith().VulnerableLayers().RandomVulnConfig().Build()
}
func CreateRandomVulnerableImageWith() ManifestBuilder {
return CreateImageWith().VulnerableLayers().RandomVulnConfig()
}
func CreateMockNotationSignature(subject *ispec.Descriptor) Image {
return CreateImageWith().RandomLayers(1, 10).EmptyConfig().Subject(subject).
ArtifactType(common.ArtifactTypeNotation).Build()
}
func CreateMockCosignSignature(subject *ispec.Descriptor) Image {
return CreateImageWith().RandomLayers(1, 10).EmptyConfig().Subject(subject).
ArtifactType(common.ArtifactTypeCosign).Build()
}
type BaseImageBuilder struct {
layers []Layer
config ispec.Image
configDescriptor ispec.Descriptor
annotations map[string]string
subject *ispec.Descriptor
artifactType string
digestAlgorithm godigest.Algorithm
}
func (ib *BaseImageBuilder) Layers(layers []Layer) ConfigBuilder {
ib.layers = layers
return ib
}
func (ib *BaseImageBuilder) LayerBlobs(layers [][]byte) ConfigBuilder {
for _, layer := range layers {
ib.layers = append(ib.layers, Layer{
Blob: layer,
MediaType: ispec.MediaTypeImageLayerGzip,
Digest: ib.digestAlgorithm.FromBytes(layer),
})
}
return ib
}
func (ib *BaseImageBuilder) EmptyLayer() ConfigBuilder {
ib.layers = []Layer{
{
Blob: ispec.DescriptorEmptyJSON.Data,
MediaType: ispec.DescriptorEmptyJSON.MediaType,
Digest: ispec.DescriptorEmptyJSON.Digest,
},
}
return ib
}
func (ib *BaseImageBuilder) RandomLayers(count, size int) ConfigBuilder {
for i := 0; i < count; i++ {
layer := make([]byte, size)
_, err := rand.Read(layer)
if err != nil {
panic("unexpected error while reading random bytes")
}
ib.layers = append(ib.layers, Layer{
Blob: layer,
MediaType: ispec.MediaTypeImageLayerGzip,
Digest: ib.digestAlgorithm.FromBytes(layer),
})
}
return ib
}
func (ib *BaseImageBuilder) DefaultLayers() ConfigBuilder {
ib.layers = GetDefaultLayers()
return ib
}
func (ib *BaseImageBuilder) VulnerableLayers() VulnerableConfigBuilder {
layer, err := GetLayerWithVulnerability()
if err != nil {
panic("unable to read vulnerable layers from test data: " + err.Error())
}
ib.layers = []Layer{
{
Blob: layer,
MediaType: ispec.MediaTypeImageLayerGzip,
Digest: ib.digestAlgorithm.FromBytes(layer),
},
}
return ib
}
func (ib *BaseImageBuilder) ImageConfig(config ispec.Image) ManifestBuilder {
ib.config = config
configBlob, err := json.Marshal(config)
if err != nil {
panic("unreachable: ispec.Image should always be marshable")
}
ib.configDescriptor = ispec.Descriptor{
MediaType: ispec.MediaTypeImageConfig,
Size: int64(len(configBlob)),
Data: configBlob,
Digest: ib.digestAlgorithm.FromBytes(configBlob),
}
return ib
}
func (ib *BaseImageBuilder) DefaultConfig() ManifestBuilder {
return ib.ImageConfig(GetDefaultConfig())
}
func (ib *BaseImageBuilder) PlatformConfig(arch, os string) ManifestBuilder {
conf := GetDefaultConfig()
conf.Created = RandomDateRef(time.UTC)
conf.Author = getRandomAuthor()
conf.Platform = ispec.Platform{Architecture: arch, OS: os}
return ib.ImageConfig(conf)
}
func (ib *BaseImageBuilder) EmptyConfig() ManifestBuilder {
ib.configDescriptor = ispec.DescriptorEmptyJSON
return ib
}
func (ib *BaseImageBuilder) ArtifactConfig(artifactType string) ManifestBuilder {
configDescriptor := ispec.DescriptorEmptyJSON
configDescriptor.MediaType = artifactType
ib.configDescriptor = configDescriptor
return ib
}
func (ib *BaseImageBuilder) CustomConfigBlob(configBlob []byte, mediaType string) ManifestBuilder {
ib.config = ispec.Image{}
ib.configDescriptor = ispec.Descriptor{
MediaType: mediaType,
Size: int64(len(configBlob)),
Data: configBlob,
Digest: ib.digestAlgorithm.FromBytes(configBlob),
}
return ib
}
func (ib *BaseImageBuilder) RandomConfig() ManifestBuilder {
config := GetDefaultConfig()
config.Author = getRandomAuthor()
config.Platform = getRandomPlatform()
config.Created = RandomDateRef(time.UTC)
ib.config = config
configBlob, err := json.Marshal(config)
if err != nil {
panic("unreachable: ispec.Image should always be marshable")
}
ib.configDescriptor = ispec.Descriptor{
MediaType: ispec.MediaTypeImageConfig,
Digest: ib.digestAlgorithm.FromBytes(configBlob),
Size: int64(len(configBlob)),
Data: configBlob,
}
return ib
}
func (ib *BaseImageBuilder) DefaultVulnConfig() ManifestBuilder {
vulnerableConfig := GetDefaultVulnConfig()
configBlob, err := json.Marshal(vulnerableConfig)
if err != nil {
panic("unreachable: ispec.Image should always be marshable")
}
vulnConfigDescriptor := ispec.Descriptor{
MediaType: ispec.MediaTypeImageConfig,
Digest: ib.digestAlgorithm.FromBytes(configBlob),
Size: int64(len(configBlob)),
Data: configBlob,
}
ib.config = vulnerableConfig
ib.configDescriptor = vulnConfigDescriptor
return ib
}
func (ib *BaseImageBuilder) VulnerableConfig(config ispec.Image) ManifestBuilder {
vulnerableConfig := ispec.Image{
Created: config.Created,
Platform: config.Platform,
Config: config.Config,
RootFS: ispec.RootFS{
Type: "layers",
DiffIDs: []godigest.Digest{"sha256:f1417ff83b319fbdae6dd9cd6d8c9c88002dcd75ecf6ec201c8c6894681cf2b5"},
},
Author: config.Author,
History: config.History,
}
configBlob, err := json.Marshal(vulnerableConfig)
if err != nil {
panic("unreachable: ispec.Image should always be marshable")
}
vulnConfigDescriptor := ispec.Descriptor{
MediaType: ispec.MediaTypeImageConfig,
Digest: ib.digestAlgorithm.FromBytes(configBlob),
Size: int64(len(configBlob)),
Data: configBlob,
}
ib.config = vulnerableConfig
ib.configDescriptor = vulnConfigDescriptor
return ib
}
func (ib *BaseImageBuilder) RandomVulnConfig() ManifestBuilder {
vulnerableConfig := GetDefaultVulnConfig()
vulnerableConfig.Author = getRandomAuthor()
vulnerableConfig.Platform = getRandomPlatform()
vulnerableConfig.Created = RandomDateRef(time.UTC)
configBlob, err := json.Marshal(vulnerableConfig)
if err != nil {
panic("unreachable: ispec.Image should always be marshable")
}
vulnConfigDescriptor := ispec.Descriptor{
MediaType: ispec.MediaTypeImageConfig,
Digest: ib.digestAlgorithm.FromBytes(configBlob),
Size: int64(len(configBlob)),
Data: configBlob,
}
ib.config = vulnerableConfig
ib.configDescriptor = vulnConfigDescriptor
return ib
}
func (ib *BaseImageBuilder) Subject(subject *ispec.Descriptor) ManifestBuilder {
ib.subject = subject
return ib
}
func (ib *BaseImageBuilder) ArtifactType(artifactType string) ManifestBuilder {
ib.artifactType = artifactType
return ib
}
func (ib *BaseImageBuilder) Annotations(annotations map[string]string) ManifestBuilder {
ib.annotations = annotations
return ib
}
func (ib *BaseImageBuilder) Build() Image {
img := Image{
Layers: getLayerBlobs(ib.layers),
Config: ib.config,
ConfigDescriptor: ib.configDescriptor,
Manifest: ispec.Manifest{
Versioned: specs.Versioned{SchemaVersion: storageConstants.SchemaVersion},
MediaType: ispec.MediaTypeImageManifest,
Config: ispec.Descriptor{
MediaType: ib.configDescriptor.MediaType,
Digest: ib.configDescriptor.Digest,
Size: ib.configDescriptor.Size,
},
Layers: getLayersDescriptors(ib.layers),
ArtifactType: ib.artifactType,
Subject: ib.subject,
Annotations: ib.annotations,
},
digestAlgorithm: ib.digestAlgorithm,
}
manifestBlob, err := json.Marshal(img.Manifest)
if err != nil {
panic("unreachable: ispec.Manifest should always be marshable")
}
img.ManifestDescriptor = ispec.Descriptor{
MediaType: ispec.MediaTypeImageManifest,
Digest: ib.digestAlgorithm.FromBytes(manifestBlob),
Size: int64(len(manifestBlob)),
Data: manifestBlob,
Platform: &ib.config.Platform,
}
return img
}
func getRandomAuthor() string {
const n = 100000
return "ZotUser-" + strconv.Itoa(mathRand.Intn(n)) //nolint: gosec
}
func getRandomPlatform() ispec.Platform {
const n = 100000
return ispec.Platform{
OS: "linux-" + strconv.Itoa(mathRand.Intn(n)), //nolint: gosec
Architecture: "amd64-" + strconv.Itoa(mathRand.Intn(n)), //nolint: gosec
}
}
func getLayerBlobs(layers []Layer) [][]byte {
blobs := make([][]byte, len(layers))
for i := range layers {
blobs[i] = layers[i].Blob
}
return blobs
}
func getLayersDescriptors(layers []Layer) []ispec.Descriptor {
descriptors := make([]ispec.Descriptor, len(layers))
for i := range layers {
descriptors[i] = ispec.Descriptor{
Digest: layers[i].Digest,
MediaType: layers[i].MediaType,
Size: int64(len(layers[i].Blob)),
}
}
return descriptors
}