0
Fork 0
mirror of https://github.com/project-zot/zot.git synced 2024-12-23 22:27:35 -05:00
zot/pkg/test/images.go

503 lines
14 KiB
Go
Raw Normal View History

package test
import (
"crypto/rand"
"encoding/json"
mathRand "math/rand"
"strconv"
"time"
godigest "github.com/opencontainers/go-digest"
"github.com/opencontainers/image-spec/specs-go"
ispec "github.com/opencontainers/image-spec/specs-go/v1"
storageConstants "zotregistry.io/zot/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
// 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 vulnerage 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
ConfigDescriptor ispec.Descriptor
ManifestDescriptor ispec.Descriptor
}
func (img *Image) Digest() godigest.Digest {
blob, err := json.Marshal(img.Manifest)
if err != nil {
panic("unreachable: ispec.Manifest should always be marshable")
}
// when we'll migrate all code to the new format of creating images we can replace this with
// the value from manifestDescriptor
return godigest.FromBytes(blob)
}
func (img *Image) DigestStr() string {
return img.Digest().String()
}
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,
}
}
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{}
}
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()
}
// CreateFakeTestSignature returns a test signature that is used to mark a image as signed
// when creating a test Repo. It won't be recognized as a signature if uploaded to the repository directly.
func CreateFakeTestSignature(subject *ispec.Descriptor) Image {
return CreateImageWith().RandomLayers(1, 10).DefaultConfig().
ArtifactType(TestFakeSignatureArtType).Subject(subject).Build()
}
type BaseImageBuilder struct {
layers []Layer
config ispec.Image
configDescriptor ispec.Descriptor
annotations map[string]string
subject *ispec.Descriptor
artifactType string
}
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: godigest.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: godigest.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: godigest.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: godigest.FromBytes(configBlob),
}
return ib
}
func (ib *BaseImageBuilder) DefaultConfig() ManifestBuilder {
return ib.ImageConfig(GetDefaultConfig())
}
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: godigest.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: godigest.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: godigest.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: godigest.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: godigest.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,
},
}
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: godigest.FromBytes(manifestBlob),
Size: int64(len(manifestBlob)),
Data: manifestBlob,
}
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
}