mirror of
https://github.com/project-zot/zot.git
synced 2024-12-23 22:27:35 -05:00
8e18917b07
Signed-off-by: Laurentiu Niculae <niculae.laurentiu1@gmail.com>
502 lines
14 KiB
Go
502 lines
14 KiB
Go
package image
|
|
|
|
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 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
|
|
|
|
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
|
|
}
|