Merge pull request #9120 from dmcgowan/image-usage-test
Image usage test
This commit is contained in:
commit
5444dae0d4
276
images/imagetest/content.go
Normal file
276
images/imagetest/content.go
Normal file
@ -0,0 +1,276 @@
|
||||
/*
|
||||
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
|
||||
}
|
58
images/imagetest/size.go
Normal file
58
images/imagetest/size.go
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
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
|
||||
|
||||
// Size represents the size of an object from different methods of counting, in bytes
|
||||
type Size struct {
|
||||
// Manifest is the total Manifest reported size of current object and children
|
||||
Manifest int64
|
||||
|
||||
// Content is the total of store Content of current object and children
|
||||
Content int64
|
||||
|
||||
// Unpacked is the total size of Unpacked data for the given snapshotter
|
||||
// The key is the snapshotter name
|
||||
// The value is the usage reported by the snapshotter for the image
|
||||
Unpacked map[string]int64
|
||||
|
||||
// Uncompressed is the total Uncompressed tar stream data (used for size estimate)
|
||||
Uncompressed int64
|
||||
}
|
||||
|
||||
// ContentSizeCalculator calculates a size property for the test content
|
||||
type ContentSizeCalculator func(Content) int64
|
||||
|
||||
// SizeOfManifest recursively calculates the manifest reported size,
|
||||
// not accounting for duplicate entries.
|
||||
func SizeOfManifest(t Content) int64 {
|
||||
accumulated := t.Size.Manifest
|
||||
for _, child := range t.Children {
|
||||
accumulated = accumulated + SizeOfManifest(child)
|
||||
}
|
||||
return accumulated
|
||||
}
|
||||
|
||||
// SizeOfContent recursively calculates the size of all stored content
|
||||
// The calculation is a simple accumulation based on the structure,
|
||||
// duplicate blobs are not accounted for.
|
||||
func SizeOfContent(t Content) int64 {
|
||||
accumulated := t.Size.Content
|
||||
for _, child := range t.Children {
|
||||
accumulated = accumulated + SizeOfContent(child)
|
||||
}
|
||||
return accumulated
|
||||
}
|
100
images/usage/calculator_test.go
Normal file
100
images/usage/calculator_test.go
Normal file
@ -0,0 +1,100 @@
|
||||
/*
|
||||
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 usage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/images/imagetest"
|
||||
"github.com/containerd/containerd/platforms"
|
||||
"github.com/containerd/log/logtest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
func TestUsageCalculation(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
target imagetest.ContentCreator
|
||||
expected imagetest.ContentSizeCalculator
|
||||
opts []Opt
|
||||
}{
|
||||
{
|
||||
name: "simple",
|
||||
target: imagetest.SimpleManifest(50),
|
||||
expected: imagetest.SizeOfManifest,
|
||||
opts: []Opt{WithManifestUsage()},
|
||||
},
|
||||
{
|
||||
name: "simpleIndex",
|
||||
target: imagetest.SimpleIndex(5, 50),
|
||||
expected: imagetest.SizeOfContent,
|
||||
},
|
||||
{
|
||||
name: "stripLayersManifestOnly",
|
||||
target: imagetest.StripLayers(imagetest.SimpleIndex(3, 51)),
|
||||
expected: imagetest.SizeOfManifest,
|
||||
opts: []Opt{WithManifestUsage()},
|
||||
},
|
||||
{
|
||||
name: "stripLayers",
|
||||
target: imagetest.StripLayers(imagetest.SimpleIndex(4, 60)),
|
||||
expected: imagetest.SizeOfContent,
|
||||
},
|
||||
{
|
||||
name: "manifestlimit",
|
||||
target: func(tc imagetest.ContentStore) imagetest.Content {
|
||||
return tc.Index(
|
||||
imagetest.AddPlatform(imagetest.SimpleManifest(5)(tc), ocispec.Platform{Architecture: "amd64", OS: "linux"}),
|
||||
imagetest.AddPlatform(imagetest.SimpleManifest(10)(tc), ocispec.Platform{Architecture: "arm64", OS: "linux"}),
|
||||
)
|
||||
},
|
||||
expected: func(t imagetest.Content) int64 { return imagetest.SizeOfManifest(imagetest.LimitChildren(t, 1)) },
|
||||
opts: []Opt{
|
||||
WithManifestLimit(platforms.Only(ocispec.Platform{Architecture: "amd64", OS: "linux"}), 1),
|
||||
WithManifestUsage(),
|
||||
},
|
||||
},
|
||||
// TODO: Add test with snapshot
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx := logtest.WithT(context.Background(), t)
|
||||
|
||||
cs := imagetest.NewContentStore(ctx, t)
|
||||
content := tc.target(cs)
|
||||
img := images.Image{
|
||||
Name: tc.name,
|
||||
Target: content.Descriptor,
|
||||
Labels: content.Labels,
|
||||
}
|
||||
|
||||
usage, err := CalculateImageUsage(ctx, img, cs, tc.opts...)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := tc.expected(content)
|
||||
|
||||
if expected != usage {
|
||||
t.Fatalf("unexpected usage: %d, expected %d", usage, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user