github.com/containerd/Containerd@v1.4.13/content/local/store_test.go (about)

     1  /*
     2     Copyright The containerd Authors.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package local
    18  
    19  import (
    20  	"bufio"
    21  	"bytes"
    22  	"context"
    23  	_ "crypto/sha256" // required for digest package
    24  	"fmt"
    25  	"io"
    26  	"io/ioutil"
    27  	"math/rand"
    28  	"os"
    29  	"path/filepath"
    30  	"reflect"
    31  	"runtime"
    32  	"sync"
    33  	"testing"
    34  	"time"
    35  
    36  	"github.com/containerd/containerd/content"
    37  	"github.com/containerd/containerd/content/testsuite"
    38  	"github.com/containerd/containerd/errdefs"
    39  	"github.com/containerd/containerd/pkg/testutil"
    40  
    41  	"github.com/opencontainers/go-digest"
    42  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    43  	"gotest.tools/v3/assert"
    44  )
    45  
    46  type memoryLabelStore struct {
    47  	l      sync.Mutex
    48  	labels map[digest.Digest]map[string]string
    49  }
    50  
    51  func newMemoryLabelStore() LabelStore {
    52  	return &memoryLabelStore{
    53  		labels: map[digest.Digest]map[string]string{},
    54  	}
    55  }
    56  
    57  func (mls *memoryLabelStore) Get(d digest.Digest) (map[string]string, error) {
    58  	mls.l.Lock()
    59  	labels := mls.labels[d]
    60  	mls.l.Unlock()
    61  
    62  	return labels, nil
    63  }
    64  
    65  func (mls *memoryLabelStore) Set(d digest.Digest, labels map[string]string) error {
    66  	mls.l.Lock()
    67  	mls.labels[d] = labels
    68  	mls.l.Unlock()
    69  
    70  	return nil
    71  }
    72  
    73  func (mls *memoryLabelStore) Update(d digest.Digest, update map[string]string) (map[string]string, error) {
    74  	mls.l.Lock()
    75  	labels, ok := mls.labels[d]
    76  	if !ok {
    77  		labels = map[string]string{}
    78  	}
    79  	for k, v := range update {
    80  		if v == "" {
    81  			delete(labels, k)
    82  		} else {
    83  			labels[k] = v
    84  		}
    85  	}
    86  	mls.labels[d] = labels
    87  	mls.l.Unlock()
    88  
    89  	return labels, nil
    90  }
    91  
    92  func TestContent(t *testing.T) {
    93  	testsuite.ContentSuite(t, "fs", func(ctx context.Context, root string) (context.Context, content.Store, func() error, error) {
    94  		cs, err := NewLabeledStore(root, newMemoryLabelStore())
    95  		if err != nil {
    96  			return nil, nil, nil, err
    97  		}
    98  		return ctx, cs, func() error {
    99  			return nil
   100  		}, nil
   101  	})
   102  }
   103  
   104  func TestContentWriter(t *testing.T) {
   105  	ctx, tmpdir, cs, cleanup := contentStoreEnv(t)
   106  	defer cleanup()
   107  	defer testutil.DumpDirOnFailure(t, tmpdir)
   108  
   109  	if _, err := os.Stat(filepath.Join(tmpdir, "ingest")); os.IsNotExist(err) {
   110  		t.Fatal("ingest dir should be created", err)
   111  	}
   112  
   113  	cw, err := cs.Writer(ctx, content.WithRef("myref"))
   114  	if err != nil {
   115  		t.Fatal(err)
   116  	}
   117  	if err := cw.Close(); err != nil {
   118  		t.Fatal(err)
   119  	}
   120  
   121  	// reopen, so we can test things
   122  	cw, err = cs.Writer(ctx, content.WithRef("myref"))
   123  	if err != nil {
   124  		t.Fatal(err)
   125  	}
   126  
   127  	// make sure that second resume also fails
   128  	if _, err = cs.Writer(ctx, content.WithRef("myref")); err == nil {
   129  		// TODO(stevvooe): This also works across processes. Need to find a way
   130  		// to test that, as well.
   131  		t.Fatal("no error on second resume")
   132  	}
   133  
   134  	// we should also see this as an active ingestion
   135  	ingestions, err := cs.ListStatuses(ctx, "")
   136  	if err != nil {
   137  		t.Fatal(err)
   138  	}
   139  
   140  	// clear out the time and meta cause we don't care for this test
   141  	for i := range ingestions {
   142  		ingestions[i].UpdatedAt = time.Time{}
   143  		ingestions[i].StartedAt = time.Time{}
   144  	}
   145  
   146  	if !reflect.DeepEqual(ingestions, []content.Status{
   147  		{
   148  			Ref:    "myref",
   149  			Offset: 0,
   150  		},
   151  	}) {
   152  		t.Fatalf("unexpected ingestion set: %v", ingestions)
   153  	}
   154  
   155  	p := make([]byte, 4<<20)
   156  	if _, err := rand.Read(p); err != nil {
   157  		t.Fatal(err)
   158  	}
   159  	expected := digest.FromBytes(p)
   160  
   161  	checkCopy(t, int64(len(p)), cw, bufio.NewReader(ioutil.NopCloser(bytes.NewReader(p))))
   162  
   163  	if err := cw.Commit(ctx, int64(len(p)), expected); err != nil {
   164  		t.Fatal(err)
   165  	}
   166  
   167  	if err := cw.Close(); err != nil {
   168  		t.Fatal(err)
   169  	}
   170  
   171  	cw, err = cs.Writer(ctx, content.WithRef("aref"))
   172  	if err != nil {
   173  		t.Fatal(err)
   174  	}
   175  
   176  	// now, attempt to write the same data again
   177  	checkCopy(t, int64(len(p)), cw, bufio.NewReader(ioutil.NopCloser(bytes.NewReader(p))))
   178  	if err := cw.Commit(ctx, int64(len(p)), expected); err == nil {
   179  		t.Fatal("expected already exists error")
   180  	} else if !errdefs.IsAlreadyExists(err) {
   181  		t.Fatal(err)
   182  	}
   183  
   184  	path := checkBlobPath(t, cs, expected)
   185  
   186  	// read the data back, make sure its the same
   187  	pp, err := ioutil.ReadFile(path)
   188  	if err != nil {
   189  		t.Fatal(err)
   190  	}
   191  
   192  	if !bytes.Equal(p, pp) {
   193  		t.Fatal("mismatched data written to disk")
   194  	}
   195  
   196  }
   197  
   198  func TestWalkBlobs(t *testing.T) {
   199  	ctx, _, cs, cleanup := contentStoreEnv(t)
   200  	defer cleanup()
   201  
   202  	const (
   203  		nblobs  = 79
   204  		maxsize = 4 << 10
   205  	)
   206  	var (
   207  		blobs    = populateBlobStore(ctx, t, cs, nblobs, maxsize)
   208  		expected = map[digest.Digest]struct{}{}
   209  		found    = map[digest.Digest]struct{}{}
   210  	)
   211  
   212  	for dgst := range blobs {
   213  		expected[dgst] = struct{}{}
   214  	}
   215  
   216  	if err := cs.Walk(ctx, func(bi content.Info) error {
   217  		found[bi.Digest] = struct{}{}
   218  		checkBlobPath(t, cs, bi.Digest)
   219  		return nil
   220  	}); err != nil {
   221  		t.Fatal(err)
   222  	}
   223  
   224  	if !reflect.DeepEqual(expected, found) {
   225  		t.Fatalf("expected did not match found: %v != %v", found, expected)
   226  	}
   227  }
   228  
   229  // BenchmarkIngests checks the insertion time over varying blob sizes.
   230  //
   231  // Note that at the time of writing there is roughly a 4ms insertion overhead
   232  // for blobs. This seems to be due to the number of syscalls and file io we do
   233  // coordinating the ingestion.
   234  func BenchmarkIngests(b *testing.B) {
   235  	ctx, _, cs, cleanup := contentStoreEnv(b)
   236  	defer cleanup()
   237  
   238  	for _, size := range []int64{
   239  		1 << 10,
   240  		4 << 10,
   241  		512 << 10,
   242  		1 << 20,
   243  	} {
   244  		size := size
   245  		b.Run(fmt.Sprint(size), func(b *testing.B) {
   246  			b.StopTimer()
   247  			blobs := generateBlobs(b, int64(b.N), size)
   248  
   249  			var bytes int64
   250  			for _, blob := range blobs {
   251  				bytes += int64(len(blob))
   252  			}
   253  			b.SetBytes(bytes)
   254  
   255  			b.StartTimer()
   256  
   257  			for dgst, p := range blobs {
   258  				checkWrite(ctx, b, cs, dgst, p)
   259  			}
   260  		})
   261  	}
   262  }
   263  
   264  type checker interface {
   265  	Fatal(args ...interface{})
   266  }
   267  
   268  func generateBlobs(t checker, nblobs, maxsize int64) map[digest.Digest][]byte {
   269  	blobs := map[digest.Digest][]byte{}
   270  
   271  	for i := int64(0); i < nblobs; i++ {
   272  		p := make([]byte, rand.Int63n(maxsize))
   273  
   274  		if _, err := rand.Read(p); err != nil {
   275  			t.Fatal(err)
   276  		}
   277  
   278  		dgst := digest.FromBytes(p)
   279  		blobs[dgst] = p
   280  	}
   281  
   282  	return blobs
   283  }
   284  
   285  func populateBlobStore(ctx context.Context, t checker, cs content.Store, nblobs, maxsize int64) map[digest.Digest][]byte {
   286  	blobs := generateBlobs(t, nblobs, maxsize)
   287  
   288  	for dgst, p := range blobs {
   289  		checkWrite(ctx, t, cs, dgst, p)
   290  	}
   291  
   292  	return blobs
   293  }
   294  
   295  func contentStoreEnv(t checker) (context.Context, string, content.Store, func()) {
   296  	pc, _, _, ok := runtime.Caller(1)
   297  	if !ok {
   298  		t.Fatal("failed to resolve caller")
   299  	}
   300  	fn := runtime.FuncForPC(pc)
   301  
   302  	tmpdir, err := ioutil.TempDir("", filepath.Base(fn.Name())+"-")
   303  	if err != nil {
   304  		t.Fatal(err)
   305  	}
   306  
   307  	cs, err := NewStore(tmpdir)
   308  	if err != nil {
   309  		os.RemoveAll(tmpdir)
   310  		t.Fatal(err)
   311  	}
   312  
   313  	ctx, cancel := context.WithCancel(context.Background())
   314  	return ctx, tmpdir, cs, func() {
   315  		cancel()
   316  		os.RemoveAll(tmpdir)
   317  	}
   318  }
   319  
   320  func checkCopy(t checker, size int64, dst io.Writer, src io.Reader) {
   321  	nn, err := io.Copy(dst, src)
   322  	if err != nil {
   323  		t.Fatal(err)
   324  	}
   325  
   326  	if nn != size {
   327  		t.Fatal("incorrect number of bytes copied")
   328  	}
   329  }
   330  
   331  func checkBlobPath(t *testing.T, cs content.Store, dgst digest.Digest) string {
   332  	path, err := cs.(*store).blobPath(dgst)
   333  	if err != nil {
   334  		t.Fatalf("failed to calculate blob path: %v", err)
   335  	}
   336  
   337  	if path != filepath.Join(cs.(*store).root, "blobs", dgst.Algorithm().String(), dgst.Hex()) {
   338  		t.Fatalf("unexpected path: %q", path)
   339  	}
   340  	fi, err := os.Stat(path)
   341  	if err != nil {
   342  		t.Fatalf("error stating blob path: %v", err)
   343  	}
   344  
   345  	if runtime.GOOS != "windows" {
   346  		// ensure that only read bits are set.
   347  		if ((fi.Mode() & os.ModePerm) & 0333) != 0 {
   348  			t.Fatalf("incorrect permissions: %v", fi.Mode())
   349  		}
   350  	}
   351  
   352  	return path
   353  }
   354  
   355  func checkWrite(ctx context.Context, t checker, cs content.Store, dgst digest.Digest, p []byte) digest.Digest {
   356  	if err := content.WriteBlob(ctx, cs, dgst.String(), bytes.NewReader(p),
   357  		ocispec.Descriptor{Size: int64(len(p)), Digest: dgst}); err != nil {
   358  		t.Fatal(err)
   359  	}
   360  
   361  	return dgst
   362  }
   363  
   364  func TestWriterTruncateRecoversFromIncompleteWrite(t *testing.T) {
   365  	tmpdir, err := ioutil.TempDir("", "test-local-content-store-recover")
   366  	assert.NilError(t, err)
   367  	defer os.RemoveAll(tmpdir)
   368  
   369  	cs, err := NewStore(tmpdir)
   370  	assert.NilError(t, err)
   371  
   372  	ctx, cancel := context.WithCancel(context.Background())
   373  	defer cancel()
   374  
   375  	ref := "ref"
   376  	contentB := []byte("this is the content")
   377  	total := int64(len(contentB))
   378  	setupIncompleteWrite(ctx, t, cs, ref, total)
   379  
   380  	writer, err := cs.Writer(ctx, content.WithRef(ref), content.WithDescriptor(ocispec.Descriptor{Size: total}))
   381  	assert.NilError(t, err)
   382  
   383  	assert.NilError(t, writer.Truncate(0))
   384  
   385  	_, err = writer.Write(contentB)
   386  	assert.NilError(t, err)
   387  
   388  	dgst := digest.FromBytes(contentB)
   389  	err = writer.Commit(ctx, total, dgst)
   390  	assert.NilError(t, err)
   391  }
   392  
   393  func setupIncompleteWrite(ctx context.Context, t *testing.T, cs content.Store, ref string, total int64) {
   394  	writer, err := cs.Writer(ctx, content.WithRef(ref), content.WithDescriptor(ocispec.Descriptor{Size: total}))
   395  	assert.NilError(t, err)
   396  
   397  	_, err = writer.Write([]byte("bad data"))
   398  	assert.NilError(t, err)
   399  
   400  	assert.NilError(t, writer.Close())
   401  }
   402  
   403  func TestWriteReadEmptyFileTimestamp(t *testing.T) {
   404  	root, err := ioutil.TempDir("", "test-write-read-file-timestamp")
   405  	if err != nil {
   406  		t.Errorf("failed to create a tmp dir: %v", err)
   407  	}
   408  	defer os.RemoveAll(root)
   409  
   410  	emptyFile := filepath.Join(root, "updatedat")
   411  	if err := writeTimestampFile(emptyFile, time.Time{}); err != nil {
   412  		t.Errorf("failed to write Zero Time to file: %v", err)
   413  	}
   414  
   415  	timestamp, err := readFileTimestamp(emptyFile)
   416  	if err != nil {
   417  		t.Errorf("read empty timestamp file should success, but got error: %v", err)
   418  	}
   419  	if !timestamp.IsZero() {
   420  		t.Errorf("read empty timestamp file should return time.Time{}, but got: %v", timestamp)
   421  	}
   422  }