277 lines
7.3 KiB
Go
277 lines
7.3 KiB
Go
/*
|
|
Copyright The containerd Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package imagetest
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"math/rand"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/containerd/containerd/content"
|
|
"github.com/containerd/containerd/content/local"
|
|
"github.com/containerd/containerd/images"
|
|
"github.com/opencontainers/go-digest"
|
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
)
|
|
|
|
// Content represents a piece of image content in the content store along with
|
|
// its relevant children and size.
|
|
type Content struct {
|
|
Descriptor ocispec.Descriptor
|
|
Labels map[string]string
|
|
Size Size
|
|
Children []Content
|
|
}
|
|
|
|
// ContentStore is a temporary content store which provides simple
|
|
// helper functions for quickly altering content in the store.
|
|
// Directly modifying the content store without using the helper functions
|
|
// may result in out of sync test content values, be careful of this
|
|
// when writing tests.
|
|
type ContentStore struct {
|
|
content.Store
|
|
|
|
ctx context.Context
|
|
t *testing.T
|
|
}
|
|
|
|
// NewContentStore creates a new content store in the testing's temporary directory
|
|
func NewContentStore(ctx context.Context, t *testing.T) ContentStore {
|
|
cs, err := local.NewLabeledStore(t.TempDir(), newMemoryLabelStore())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return ContentStore{
|
|
Store: cs,
|
|
ctx: ctx,
|
|
t: t,
|
|
}
|
|
}
|
|
|
|
// Index creates an index with the provided manifests and stores it
|
|
// in the content store.
|
|
func (tc ContentStore) Index(manifests ...Content) Content {
|
|
var index ocispec.Index
|
|
for _, m := range manifests {
|
|
index.Manifests = append(index.Manifests, m.Descriptor)
|
|
}
|
|
|
|
idx := tc.JSONObject(ocispec.MediaTypeImageIndex, index)
|
|
idx.Children = manifests
|
|
return idx
|
|
|
|
}
|
|
|
|
// Manifest creates a manifest with the given config and layers then
|
|
// stores it in the content store.
|
|
func (tc ContentStore) Manifest(config Content, layers ...Content) Content {
|
|
var manifest ocispec.Manifest
|
|
manifest.Config = config.Descriptor
|
|
for _, l := range layers {
|
|
manifest.Layers = append(manifest.Layers, l.Descriptor)
|
|
}
|
|
m := tc.JSONObject(ocispec.MediaTypeImageManifest, manifest)
|
|
m.Children = append(m.Children, config)
|
|
m.Children = append(m.Children, layers...)
|
|
return m
|
|
}
|
|
|
|
// Blob creates a generic blob with the given data and media type
|
|
// and stores the data in the content store.
|
|
func (tc ContentStore) Blob(mediaType string, data []byte) Content {
|
|
tc.t.Helper()
|
|
|
|
descriptor := ocispec.Descriptor{
|
|
MediaType: mediaType,
|
|
Digest: digest.SHA256.FromBytes(data),
|
|
Size: int64(len(data)),
|
|
}
|
|
|
|
ref := string(descriptor.Digest) // TODO: Add random component?
|
|
if err := content.WriteBlob(tc.ctx, tc.Store, ref, bytes.NewReader(data), descriptor); err != nil {
|
|
tc.t.Fatal(err)
|
|
}
|
|
|
|
return Content{
|
|
Descriptor: descriptor,
|
|
Size: Size{
|
|
Manifest: descriptor.Size,
|
|
Content: descriptor.Size,
|
|
},
|
|
}
|
|
|
|
}
|
|
|
|
// RandomBlob creates a blob object in the content store with random data.
|
|
func (tc ContentStore) RandomBlob(mediaType string, n int) Content {
|
|
tc.t.Helper()
|
|
|
|
data := make([]byte, int64(n))
|
|
rand.New(rand.NewSource(int64(n))).Read(data)
|
|
|
|
return tc.Blob(mediaType, data)
|
|
}
|
|
|
|
// JSONObject creates an object in the content store by first marshaling
|
|
// to JSON and then storing the data.
|
|
func (tc ContentStore) JSONObject(mediaType string, i interface{}) Content {
|
|
tc.t.Helper()
|
|
|
|
data, err := json.Marshal(i)
|
|
if err != nil {
|
|
tc.t.Fatal(err)
|
|
}
|
|
|
|
return tc.Blob(mediaType, data)
|
|
}
|
|
|
|
// Walk walks all the children of the provided content and calls the provided
|
|
// function with the associated content store.
|
|
// Walk can be used to update an object and reflect that change in the content
|
|
// store.
|
|
func (tc ContentStore) Walk(c Content, fn func(context.Context, *Content, content.Store) error) Content {
|
|
tc.t.Helper()
|
|
|
|
if err := fn(tc.ctx, &c, tc.Store); err != nil {
|
|
tc.t.Fatal(err)
|
|
}
|
|
|
|
if len(c.Children) > 0 {
|
|
children := make([]Content, len(c.Children))
|
|
for i, child := range c.Children {
|
|
children[i] = tc.Walk(child, fn)
|
|
}
|
|
c.Children = children
|
|
}
|
|
|
|
return c
|
|
}
|
|
|
|
// AddPlatform alters the content desciptor by setting the platform
|
|
func AddPlatform(c Content, p ocispec.Platform) Content {
|
|
c.Descriptor.Platform = &p
|
|
return c
|
|
}
|
|
|
|
// LimitChildren limits the amount of children in the content.
|
|
// This function is non-recursive and uses the natural ordering.
|
|
func LimitChildren(c Content, limit int) Content {
|
|
if images.IsIndexType(c.Descriptor.MediaType) {
|
|
if len(c.Children) > limit {
|
|
c.Children = c.Children[0:limit]
|
|
}
|
|
}
|
|
return c
|
|
}
|
|
|
|
// ContentCreator is a simple interface for generating content for tests
|
|
type ContentCreator func(ContentStore) Content
|
|
|
|
// SimpleManifest generates a simple manifest with small config and random layer
|
|
// The layer produced is not a valid compressed tar, do not unpack it.
|
|
func SimpleManifest(layerSize int) ContentCreator {
|
|
return func(tc ContentStore) Content {
|
|
config := ocispec.ImageConfig{
|
|
Env: []string{"random"}, // TODO: Make random string
|
|
}
|
|
return tc.Manifest(
|
|
tc.JSONObject(ocispec.MediaTypeImageConfig, config),
|
|
tc.RandomBlob(ocispec.MediaTypeImageLayerGzip, layerSize))
|
|
}
|
|
}
|
|
|
|
// SimpleIndex generates a simple index with the number of simple
|
|
// manifests specified.
|
|
func SimpleIndex(manifests, layerSize int) ContentCreator {
|
|
manifestFn := SimpleManifest(layerSize)
|
|
return func(tc ContentStore) Content {
|
|
var m []Content
|
|
for i := 0; i < manifests; i++ {
|
|
m = append(m, manifestFn(tc))
|
|
}
|
|
return tc.Index(m...)
|
|
}
|
|
}
|
|
|
|
// StripLayers deletes all layer content from the content store
|
|
// and updates the content size to 0.
|
|
func StripLayers(cc ContentCreator) ContentCreator {
|
|
return func(tc ContentStore) Content {
|
|
return tc.Walk(cc(tc), func(ctx context.Context, c *Content, store content.Store) error {
|
|
if images.IsLayerType(c.Descriptor.MediaType) {
|
|
if err := store.Delete(tc.ctx, c.Descriptor.Digest); err != nil {
|
|
return err
|
|
}
|
|
c.Size.Content = 0
|
|
}
|
|
return nil
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
type memoryLabelStore struct {
|
|
l sync.Mutex
|
|
labels map[digest.Digest]map[string]string
|
|
}
|
|
|
|
// newMemoryLabelStore creates an inmemory label store for testing
|
|
func newMemoryLabelStore() local.LabelStore {
|
|
return &memoryLabelStore{
|
|
labels: map[digest.Digest]map[string]string{},
|
|
}
|
|
}
|
|
|
|
func (mls *memoryLabelStore) Get(d digest.Digest) (map[string]string, error) {
|
|
mls.l.Lock()
|
|
labels := mls.labels[d]
|
|
mls.l.Unlock()
|
|
|
|
return labels, nil
|
|
}
|
|
|
|
func (mls *memoryLabelStore) Set(d digest.Digest, labels map[string]string) error {
|
|
mls.l.Lock()
|
|
mls.labels[d] = labels
|
|
mls.l.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (mls *memoryLabelStore) Update(d digest.Digest, update map[string]string) (map[string]string, error) {
|
|
mls.l.Lock()
|
|
labels, ok := mls.labels[d]
|
|
if !ok {
|
|
labels = map[string]string{}
|
|
}
|
|
for k, v := range update {
|
|
if v == "" {
|
|
delete(labels, k)
|
|
} else {
|
|
labels[k] = v
|
|
}
|
|
}
|
|
mls.labels[d] = labels
|
|
mls.l.Unlock()
|
|
|
|
return labels, nil
|
|
}
|