github.com/lalkh/containerd@v1.4.3/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 }