github.com/containerd/Containerd@v1.4.13/metadata/images_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 metadata
    18  
    19  import (
    20  	"fmt"
    21  	"reflect"
    22  	"testing"
    23  	"time"
    24  
    25  	"github.com/containerd/containerd/errdefs"
    26  	"github.com/containerd/containerd/filters"
    27  	"github.com/containerd/containerd/images"
    28  	digest "github.com/opencontainers/go-digest"
    29  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    30  	"github.com/pkg/errors"
    31  )
    32  
    33  func TestImagesList(t *testing.T) {
    34  	ctx, db, cancel := testEnv(t)
    35  	defer cancel()
    36  	store := NewImageStore(NewDB(db, nil, nil))
    37  
    38  	testset := map[string]*images.Image{}
    39  	for i := 0; i < 4; i++ {
    40  		id := "image-" + fmt.Sprint(i)
    41  		testset[id] = &images.Image{
    42  			Name: id,
    43  			Labels: map[string]string{
    44  				"namelabel": id,
    45  				"even":      fmt.Sprint(i%2 == 0),
    46  				"odd":       fmt.Sprint(i%2 != 0),
    47  			},
    48  			Target: ocispec.Descriptor{
    49  				Size:      10,
    50  				MediaType: "application/vnd.containerd.test",
    51  				Digest:    digest.FromString(id),
    52  				Annotations: map[string]string{
    53  					"foo": "bar",
    54  				},
    55  			},
    56  		}
    57  
    58  		now := time.Now()
    59  		result, err := store.Create(ctx, *testset[id])
    60  		if err != nil {
    61  			t.Fatal(err)
    62  		}
    63  
    64  		checkImageTimestamps(t, &result, now, true)
    65  		testset[id].UpdatedAt, testset[id].CreatedAt = result.UpdatedAt, result.CreatedAt
    66  		checkImagesEqual(t, &result, testset[id], "ensure that containers were created as expected for list")
    67  	}
    68  
    69  	for _, testcase := range []struct {
    70  		name    string
    71  		filters []string
    72  	}{
    73  		{
    74  			name: "FullSet",
    75  		},
    76  		{
    77  			name:    "FullSetFiltered", // full set, but because we have OR filter
    78  			filters: []string{"labels.even==true", "labels.odd==true"},
    79  		},
    80  		{
    81  			name:    "Even",
    82  			filters: []string{"labels.even==true"},
    83  		},
    84  		{
    85  			name:    "Odd",
    86  			filters: []string{"labels.odd==true"},
    87  		},
    88  		{
    89  			name:    "ByName",
    90  			filters: []string{"name==image-0"},
    91  		},
    92  		{
    93  			name:    "ByNameLabelEven",
    94  			filters: []string{"labels.namelabel==image-0,labels.even==true"},
    95  		},
    96  		{
    97  			name:    "ByMediaType",
    98  			filters: []string{"target.mediatype~=application/vnd.*"},
    99  		},
   100  	} {
   101  		t.Run(testcase.name, func(t *testing.T) {
   102  			testset := testset
   103  			if len(testcase.filters) > 0 {
   104  				fs, err := filters.ParseAll(testcase.filters...)
   105  				if err != nil {
   106  					t.Fatal(err)
   107  				}
   108  
   109  				newtestset := make(map[string]*images.Image, len(testset))
   110  				for k, v := range testset {
   111  					if fs.Match(adaptImage(*v)) {
   112  						newtestset[k] = v
   113  					}
   114  				}
   115  				testset = newtestset
   116  			}
   117  
   118  			results, err := store.List(ctx, testcase.filters...)
   119  			if err != nil {
   120  				t.Fatal(err)
   121  			}
   122  
   123  			if len(results) == 0 { // all tests return a non-empty result set
   124  				t.Fatalf("no results returned")
   125  			}
   126  
   127  			if len(results) != len(testset) {
   128  				t.Fatalf("length of result does not match testset: %v != %v", len(results), len(testset))
   129  			}
   130  
   131  			for _, result := range results {
   132  				checkImagesEqual(t, &result, testset[result.Name], "list results did not match")
   133  			}
   134  		})
   135  	}
   136  
   137  	// delete everything to test it
   138  	for id := range testset {
   139  		if err := store.Delete(ctx, id); err != nil {
   140  			t.Fatal(err)
   141  		}
   142  
   143  		// try it again, get NotFound
   144  		if err := store.Delete(ctx, id); !errdefs.IsNotFound(err) {
   145  			t.Fatalf("unexpected error %v", err)
   146  		}
   147  	}
   148  }
   149  func TestImagesCreateUpdateDelete(t *testing.T) {
   150  	ctx, db, cancel := testEnv(t)
   151  	defer cancel()
   152  	store := NewImageStore(NewDB(db, nil, nil))
   153  
   154  	for _, testcase := range []struct {
   155  		name       string
   156  		original   images.Image
   157  		createerr  error
   158  		input      images.Image
   159  		fieldpaths []string
   160  		expected   images.Image
   161  		cause      error
   162  	}{
   163  		{
   164  			name:     "Touch",
   165  			original: imageBase(),
   166  			input: images.Image{
   167  				Labels: map[string]string{
   168  					"foo": "bar",
   169  					"baz": "boo",
   170  				},
   171  				Target: ocispec.Descriptor{
   172  					Size:      10,
   173  					MediaType: "application/vnd.oci.blab",
   174  					Annotations: map[string]string{
   175  						"foo": "bar",
   176  					},
   177  				},
   178  			},
   179  			expected: images.Image{
   180  				Labels: map[string]string{
   181  					"foo": "bar",
   182  					"baz": "boo",
   183  				},
   184  				Target: ocispec.Descriptor{
   185  					Size:      10,
   186  					MediaType: "application/vnd.oci.blab",
   187  					Annotations: map[string]string{
   188  						"foo": "bar",
   189  					},
   190  				},
   191  			},
   192  		},
   193  		{
   194  			name: "NoTarget",
   195  			original: images.Image{
   196  				Labels: map[string]string{
   197  					"foo": "bar",
   198  					"baz": "boo",
   199  				},
   200  				Target: ocispec.Descriptor{},
   201  			},
   202  			createerr: errdefs.ErrInvalidArgument,
   203  		},
   204  		{
   205  			name:     "ReplaceLabels",
   206  			original: imageBase(),
   207  			input: images.Image{
   208  				Labels: map[string]string{
   209  					"for": "bar",
   210  					"boo": "boo",
   211  				},
   212  				Target: ocispec.Descriptor{
   213  					Size:      10,
   214  					MediaType: "application/vnd.oci.blab",
   215  					Annotations: map[string]string{
   216  						"foo": "bar",
   217  					},
   218  				},
   219  			},
   220  			expected: images.Image{
   221  				Labels: map[string]string{
   222  					"for": "bar",
   223  					"boo": "boo",
   224  				},
   225  				Target: ocispec.Descriptor{
   226  					Size:      10,
   227  					MediaType: "application/vnd.oci.blab",
   228  					Annotations: map[string]string{
   229  						"foo": "bar",
   230  					},
   231  				},
   232  			},
   233  		},
   234  		{
   235  			name:     "ReplaceLabelsFieldPath",
   236  			original: imageBase(),
   237  			input: images.Image{
   238  				Labels: map[string]string{
   239  					"for": "bar",
   240  					"boo": "boo",
   241  				},
   242  				Target: ocispec.Descriptor{
   243  					Size:      20,                                 // ignored
   244  					MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
   245  					Annotations: map[string]string{
   246  						"not": "bar",
   247  						"new": "boo",
   248  					},
   249  				},
   250  			},
   251  			fieldpaths: []string{"labels"},
   252  			expected: images.Image{
   253  				Labels: map[string]string{
   254  					"for": "bar",
   255  					"boo": "boo",
   256  				},
   257  				Target: ocispec.Descriptor{
   258  					Size:      10,
   259  					MediaType: "application/vnd.oci.blab",
   260  					Annotations: map[string]string{
   261  						"foo": "bar",
   262  						"baz": "boo",
   263  					},
   264  				},
   265  			},
   266  		},
   267  		{
   268  			name:     "ReplaceLabelsAnnotationsFieldPath",
   269  			original: imageBase(),
   270  			input: images.Image{
   271  				Labels: map[string]string{
   272  					"for": "bar",
   273  					"boo": "boo",
   274  				},
   275  				Target: ocispec.Descriptor{
   276  					Size:      20,                                 // ignored
   277  					MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
   278  					Annotations: map[string]string{
   279  						"foo": "boo",
   280  					},
   281  				},
   282  			},
   283  			fieldpaths: []string{"annotations", "labels"},
   284  			expected: images.Image{
   285  				Labels: map[string]string{
   286  					"for": "bar",
   287  					"boo": "boo",
   288  				},
   289  				Target: ocispec.Descriptor{
   290  					Size:      10,
   291  					MediaType: "application/vnd.oci.blab",
   292  					Annotations: map[string]string{
   293  						"foo": "boo",
   294  					},
   295  				},
   296  			},
   297  		},
   298  		{
   299  			name:     "ReplaceLabel",
   300  			original: imageBase(),
   301  			input: images.Image{
   302  				Labels: map[string]string{
   303  					"foo": "baz",
   304  					"baz": "bunk",
   305  				},
   306  				Target: ocispec.Descriptor{
   307  					Size:      20,                                 // ignored
   308  					MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
   309  					Annotations: map[string]string{
   310  						"foo": "bar",
   311  					},
   312  				},
   313  			},
   314  			fieldpaths: []string{"labels.foo"},
   315  			expected: images.Image{
   316  				Labels: map[string]string{
   317  					"foo": "baz",
   318  					"baz": "boo",
   319  				},
   320  				Target: ocispec.Descriptor{
   321  					Size:      10,
   322  					MediaType: "application/vnd.oci.blab",
   323  					Annotations: map[string]string{
   324  						"foo": "bar",
   325  						"baz": "boo",
   326  					},
   327  				},
   328  			},
   329  		},
   330  		{
   331  			name:     "ReplaceAnnotation",
   332  			original: imageBase(),
   333  			input: images.Image{
   334  				Labels: map[string]string{
   335  					"foo": "baz",
   336  					"baz": "bunk",
   337  				},
   338  				Target: ocispec.Descriptor{
   339  					Size:      20,                                 // ignored
   340  					MediaType: "application/vnd.oci.blab+ignored", // make sure other stuff is ignored
   341  					Annotations: map[string]string{
   342  						"foo": "baz",
   343  						"baz": "bunk",
   344  					},
   345  				},
   346  			},
   347  			fieldpaths: []string{"annotations.foo"},
   348  			expected: images.Image{
   349  				Labels: map[string]string{
   350  					"foo": "bar",
   351  					"baz": "boo",
   352  				},
   353  				Target: ocispec.Descriptor{
   354  					Size:      10,
   355  					MediaType: "application/vnd.oci.blab",
   356  					Annotations: map[string]string{
   357  						"foo": "baz",
   358  						"baz": "boo",
   359  					},
   360  				},
   361  			},
   362  		},
   363  		{
   364  			name:     "ReplaceTarget", // target must be updated as a unit
   365  			original: imageBase(),
   366  			input: images.Image{
   367  				Target: ocispec.Descriptor{
   368  					Size:      10,
   369  					MediaType: "application/vnd.oci.blab+replaced",
   370  					Annotations: map[string]string{
   371  						"fox": "dog",
   372  					},
   373  				},
   374  			},
   375  			fieldpaths: []string{"target"},
   376  			expected: images.Image{
   377  				Labels: map[string]string{
   378  					"foo": "bar",
   379  					"baz": "boo",
   380  				},
   381  				Target: ocispec.Descriptor{
   382  					Size:      10,
   383  					MediaType: "application/vnd.oci.blab+replaced",
   384  					Annotations: map[string]string{
   385  						"fox": "dog",
   386  					},
   387  				},
   388  			},
   389  		},
   390  		{
   391  			name:     "EmptySize",
   392  			original: imageBase(),
   393  			input: images.Image{
   394  				Labels: map[string]string{
   395  					"foo": "bar",
   396  					"baz": "boo",
   397  				},
   398  				Target: ocispec.Descriptor{
   399  					Size:      0,
   400  					MediaType: "application/vnd.oci.blab",
   401  					Annotations: map[string]string{
   402  						"foo": "bar",
   403  					},
   404  				},
   405  			},
   406  			cause: errdefs.ErrInvalidArgument,
   407  		},
   408  		{
   409  			name: "EmptySizeOnCreate",
   410  			original: images.Image{
   411  				Labels: map[string]string{
   412  					"foo": "bar",
   413  					"baz": "boo",
   414  				},
   415  				Target: ocispec.Descriptor{
   416  					MediaType: "application/vnd.oci.blab",
   417  					Annotations: map[string]string{
   418  						"foo": "bar",
   419  					},
   420  				},
   421  			},
   422  			createerr: errdefs.ErrInvalidArgument,
   423  		},
   424  		{
   425  			name:     "EmptyMediaType",
   426  			original: imageBase(),
   427  			input: images.Image{
   428  				Labels: map[string]string{
   429  					"foo": "bar",
   430  					"baz": "boo",
   431  				},
   432  				Target: ocispec.Descriptor{
   433  					Size: 10,
   434  				},
   435  			},
   436  			cause: errdefs.ErrInvalidArgument,
   437  		},
   438  		{
   439  			name: "EmptySizeOnCreate",
   440  			original: images.Image{
   441  				Labels: map[string]string{
   442  					"foo": "bar",
   443  					"baz": "boo",
   444  				},
   445  				Target: ocispec.Descriptor{
   446  					Size: 10,
   447  				},
   448  			},
   449  			createerr: errdefs.ErrInvalidArgument,
   450  		},
   451  		{
   452  			name: "TryUpdateNameFail",
   453  			original: images.Image{
   454  				Labels: map[string]string{
   455  					"foo": "bar",
   456  					"baz": "boo",
   457  				},
   458  				Target: ocispec.Descriptor{
   459  					Size:      10,
   460  					MediaType: "application/vnd.oci.blab",
   461  					Annotations: map[string]string{
   462  						"foo": "bar",
   463  					},
   464  				},
   465  			},
   466  			input: images.Image{
   467  				Name: "test should fail",
   468  				Labels: map[string]string{
   469  					"foo": "bar",
   470  					"baz": "boo",
   471  				},
   472  				Target: ocispec.Descriptor{
   473  					Size:      10,
   474  					MediaType: "application/vnd.oci.blab",
   475  					Annotations: map[string]string{
   476  						"foo": "bar",
   477  					},
   478  				},
   479  			},
   480  			cause: errdefs.ErrNotFound,
   481  		},
   482  	} {
   483  		t.Run(testcase.name, func(t *testing.T) {
   484  			testcase.original.Name = testcase.name
   485  			if testcase.input.Name == "" {
   486  				testcase.input.Name = testcase.name
   487  			}
   488  			testcase.expected.Name = testcase.name
   489  
   490  			if testcase.original.Target.Digest == "" {
   491  				testcase.original.Target.Digest = digest.FromString(testcase.name)
   492  				testcase.input.Target.Digest = testcase.original.Target.Digest
   493  				testcase.expected.Target.Digest = testcase.original.Target.Digest
   494  			}
   495  
   496  			// Create
   497  			now := time.Now()
   498  			created, err := store.Create(ctx, testcase.original)
   499  			if !errors.Is(err, testcase.createerr) {
   500  				if testcase.createerr == nil {
   501  					t.Fatalf("unexpected error: %v", err)
   502  				} else {
   503  					t.Fatalf("cause of %v (cause: %v) != %v", err, errors.Cause(err), testcase.createerr)
   504  				}
   505  			} else if testcase.createerr != nil {
   506  				return
   507  			}
   508  
   509  			checkImageTimestamps(t, &created, now, true)
   510  
   511  			testcase.original.CreatedAt = created.CreatedAt
   512  			testcase.expected.CreatedAt = created.CreatedAt
   513  			testcase.original.UpdatedAt = created.UpdatedAt
   514  			testcase.expected.UpdatedAt = created.UpdatedAt
   515  
   516  			checkImagesEqual(t, &created, &testcase.original, "unexpected image on creation")
   517  
   518  			// Update
   519  			now = time.Now()
   520  			updated, err := store.Update(ctx, testcase.input, testcase.fieldpaths...)
   521  			if !errors.Is(err, testcase.cause) {
   522  				if testcase.cause == nil {
   523  					t.Fatalf("unexpected error: %v", err)
   524  				} else {
   525  					t.Fatalf("cause of %v (cause: %v) != %v", err, errors.Cause(err), testcase.cause)
   526  				}
   527  			} else if testcase.cause != nil {
   528  				return
   529  			}
   530  
   531  			checkImageTimestamps(t, &updated, now, false)
   532  			testcase.expected.UpdatedAt = updated.UpdatedAt
   533  			checkImagesEqual(t, &updated, &testcase.expected, "updated failed to get expected result")
   534  
   535  			// Get
   536  			result, err := store.Get(ctx, testcase.original.Name)
   537  			if err != nil {
   538  				t.Fatal(err)
   539  			}
   540  
   541  			checkImagesEqual(t, &result, &testcase.expected, "get after failed to get expected result")
   542  		})
   543  	}
   544  }
   545  
   546  func imageBase() images.Image {
   547  	return images.Image{
   548  		Labels: map[string]string{
   549  			"foo": "bar",
   550  			"baz": "boo",
   551  		},
   552  		Target: ocispec.Descriptor{
   553  			Size:      10,
   554  			MediaType: "application/vnd.oci.blab",
   555  			Annotations: map[string]string{
   556  				"foo": "bar",
   557  				"baz": "boo",
   558  			},
   559  		},
   560  	}
   561  }
   562  
   563  func checkImageTimestamps(t *testing.T, im *images.Image, now time.Time, oncreate bool) {
   564  	t.Helper()
   565  	if im.UpdatedAt.IsZero() || im.CreatedAt.IsZero() {
   566  		t.Fatalf("timestamps not set")
   567  	}
   568  
   569  	if oncreate {
   570  		if !im.CreatedAt.Equal(im.UpdatedAt) {
   571  			t.Fatal("timestamps should be equal on create")
   572  		}
   573  
   574  	} else {
   575  		// ensure that updatedat is always after createdat
   576  		if !im.UpdatedAt.After(im.CreatedAt) {
   577  			t.Fatalf("timestamp for updatedat not after createdat: %v <= %v", im.UpdatedAt, im.CreatedAt)
   578  		}
   579  	}
   580  
   581  	if im.UpdatedAt.Before(now) {
   582  		t.Fatal("createdat time incorrect should be after the start of the operation")
   583  	}
   584  }
   585  
   586  func checkImagesEqual(t *testing.T, a, b *images.Image, format string, args ...interface{}) {
   587  	t.Helper()
   588  	if !reflect.DeepEqual(a, b) {
   589  		t.Fatalf("images not equal \n\t%v != \n\t%v: "+format, append([]interface{}{a, b}, args...)...)
   590  	}
   591  }