github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/libpod/image/pull_test.go (about) 1 package image 2 3 import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "path/filepath" 9 "strings" 10 "testing" 11 12 "github.com/containers/image/v5/transports" 13 "github.com/containers/image/v5/transports/alltransports" 14 "github.com/containers/image/v5/types" 15 "github.com/containers/storage" 16 "github.com/containers/storage/pkg/idtools" 17 "github.com/stretchr/testify/assert" 18 "github.com/stretchr/testify/require" 19 ) 20 21 // newTestRuntime returns a *Runtime implementation and a cleanup function which the caller is expected to call. 22 func newTestRuntime(t *testing.T) (*Runtime, func()) { 23 wd, err := ioutil.TempDir("", "testStorageRuntime") 24 require.NoError(t, err) 25 err = os.MkdirAll(wd, 0700) 26 require.NoError(t, err) 27 28 store, err := storage.GetStore(storage.StoreOptions{ 29 RunRoot: filepath.Join(wd, "run"), 30 GraphRoot: filepath.Join(wd, "root"), 31 GraphDriverName: "vfs", 32 GraphDriverOptions: []string{}, 33 UIDMap: []idtools.IDMap{{ 34 ContainerID: 0, 35 HostID: os.Getuid(), 36 Size: 1, 37 }}, 38 GIDMap: []idtools.IDMap{{ 39 ContainerID: 0, 40 HostID: os.Getgid(), 41 Size: 1, 42 }}, 43 }) 44 require.NoError(t, err) 45 46 ir := NewImageRuntimeFromStore(store) 47 cleanup := func() { _ = os.RemoveAll(wd) } 48 return ir, cleanup 49 } 50 51 // storageReferenceWithoutLocation returns ref.StringWithinTransport(), 52 // stripping the [store-specification] prefix from containers/image/storage reference format. 53 func storageReferenceWithoutLocation(ref types.ImageReference) string { 54 res := ref.StringWithinTransport() 55 if res[0] == '[' { 56 closeIndex := strings.IndexRune(res, ']') 57 if closeIndex > 0 { 58 res = res[closeIndex+1:] 59 } 60 } 61 return res 62 } 63 64 func TestGetPullRefPair(t *testing.T) { 65 const imageID = "@0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" 66 const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" 67 68 ir, cleanup := newTestRuntime(t) 69 defer cleanup() 70 71 for _, c := range []struct{ srcName, destName, expectedImage, expectedDstName string }{ 72 // == Source does not have a Docker reference (as is the case for docker-archive:, oci-archive, dir:); destination formats: 73 { // registry/name, no tag: 74 "dir:/dev/this-does-not-exist", "example.com/from-directory", 75 "example.com/from-directory", "example.com/from-directory:latest", 76 }, 77 { // name, no registry, no tag: 78 "dir:/dev/this-does-not-exist", "from-directory", 79 "localhost/from-directory", "localhost/from-directory:latest", 80 }, 81 { // registry/name:tag : 82 "dir:/dev/this-does-not-exist", "example.com/from-directory:notlatest", 83 "example.com/from-directory:notlatest", "example.com/from-directory:notlatest", 84 }, 85 { // name:tag, no registry: 86 "dir:/dev/this-does-not-exist", "from-directory:notlatest", 87 "localhost/from-directory:notlatest", "localhost/from-directory:notlatest", 88 }, 89 { // name@digest, no registry: 90 "dir:/dev/this-does-not-exist", "from-directory" + digestSuffix, 91 "localhost/from-directory" + digestSuffix, "localhost/from-directory" + digestSuffix, 92 }, 93 { // registry/name@digest: 94 "dir:/dev/this-does-not-exist", "example.com/from-directory" + digestSuffix, 95 "example.com/from-directory" + digestSuffix, "example.com/from-directory" + digestSuffix, 96 }, 97 { // ns/name:tag, no registry: 98 "dir:/dev/this-does-not-exist", "ns/from-directory:notlatest", 99 "localhost/ns/from-directory:notlatest", "localhost/ns/from-directory:notlatest", 100 }, 101 { // containers-storage image ID 102 "dir:/dev/this-does-not-exist", imageID, 103 imageID, imageID, 104 }, 105 // == Source does have a Docker reference. 106 // In that case getPullListFromRef uses the full transport:name input as a destName, 107 // which would be invalid in the returned dstName - but dstName is derived from the source, so it does not really matter _so_ much. 108 // Note that unlike real-world use we use different :source and :destination to verify the data flow in more detail. 109 { // registry/name:tag 110 "docker://example.com/busybox:source", "docker://example.com/busybox:destination", 111 "docker://example.com/busybox:destination", "example.com/busybox:source", 112 }, 113 { // Implied docker.io/library and :latest 114 "docker://busybox", "docker://busybox:destination", 115 "docker://busybox:destination", "docker.io/library/busybox:latest", 116 }, 117 // == Invalid destination format. 118 {"tarball:/dev/null", "tarball:/dev/null", "", ""}, 119 } { 120 testDescription := fmt.Sprintf("%#v %#v", c.srcName, c.destName) 121 srcRef, err := alltransports.ParseImageName(c.srcName) 122 require.NoError(t, err, testDescription) 123 124 res, err := ir.getPullRefPair(srcRef, c.destName) 125 if c.expectedDstName == "" { 126 assert.Error(t, err, testDescription) 127 } else { 128 require.NoError(t, err, testDescription) 129 assert.Equal(t, c.expectedImage, res.image, testDescription) 130 assert.Equal(t, srcRef, res.srcRef, testDescription) 131 assert.Equal(t, c.expectedDstName, storageReferenceWithoutLocation(res.dstRef), testDescription) 132 } 133 } 134 } 135 136 func TestPullGoalFromImageReference(t *testing.T) { 137 ir, cleanup := newTestRuntime(t) 138 defer cleanup() 139 140 type expected struct{ image, dstName string } 141 for _, c := range []struct { 142 srcName string 143 expected []expected 144 expectedPullAllPairs bool 145 }{ 146 // == docker-archive: 147 {"docker-archive:/dev/this-does-not-exist", nil, false}, // Input does not exist. 148 {"docker-archive:/dev/null", nil, false}, // Input exists but does not contain a manifest. 149 // FIXME: The implementation has extra code for len(manifest) == 0?! That will fail in getImageDigest anyway. 150 { // RepoTags is empty 151 "docker-archive:testdata/docker-unnamed.tar.xz", 152 []expected{{"@ec9293436c2e66da44edb9efb8d41f6b13baf62283ebe846468bc992d76d7951", "@ec9293436c2e66da44edb9efb8d41f6b13baf62283ebe846468bc992d76d7951"}}, 153 false, 154 }, 155 { // RepoTags is a [docker.io/library/]name:latest, normalized to the short format. 156 "docker-archive:testdata/docker-name-only.tar.xz", 157 []expected{{"localhost/pretty-empty:latest", "localhost/pretty-empty:latest"}}, 158 true, 159 }, 160 { // RepoTags is a registry/name:latest 161 "docker-archive:testdata/docker-registry-name.tar.xz", 162 []expected{{"example.com/empty:latest", "example.com/empty:latest"}}, 163 true, 164 }, 165 { // RepoTags has multiple items for a single image 166 "docker-archive:testdata/docker-two-names.tar.xz", 167 []expected{ 168 {"localhost/pretty-empty:latest", "localhost/pretty-empty:latest"}, 169 {"example.com/empty:latest", "example.com/empty:latest"}, 170 }, 171 true, 172 }, 173 { // FIXME: Two images in a single archive - only the "first" one (whichever it is) is returned 174 // (and docker-archive: then refuses to read anything when the manifest has more than 1 item) 175 "docker-archive:testdata/docker-two-images.tar.xz", 176 []expected{{"example.com/empty:latest", "example.com/empty:latest"}}, 177 // "example.com/empty/but:different" exists but is ignored 178 true, 179 }, 180 181 // == oci-archive: 182 {"oci-archive:/dev/this-does-not-exist", nil, false}, // Input does not exist. 183 {"oci-archive:/dev/null", nil, false}, // Input exists but does not contain a manifest. 184 // FIXME: The remaining tests are commented out for now, because oci-archive: does not work unprivileged. 185 // { // No name annotation 186 // "oci-archive:testdata/oci-unnamed.tar.gz", 187 // []expected{{"@5c8aca8137ac47e84c69ae93ce650ce967917cc001ba7aad5494073fac75b8b6", "@5c8aca8137ac47e84c69ae93ce650ce967917cc001ba7aad5494073fac75b8b6"}}, 188 // false, 189 // }, 190 // { // Name is a name:latest (no normalization is defined). 191 // "oci-archive:testdata/oci-name-only.tar.gz", 192 // []expected{{"localhost/pretty-empty:latest", "localhost/pretty-empty:latest"}}, 193 // false, 194 // }, 195 // { // Name is a registry/name:latest 196 // "oci-archive:testdata/oci-registry-name.tar.gz", 197 // []expected{{"example.com/empty:latest", "example.com/empty:latest"}}, 198 // false, 199 // }, 200 // // Name exists, but is an invalid Docker reference; such names will fail when creating dstReference. 201 // {"oci-archive:testdata/oci-non-docker-name.tar.gz", nil, false}, 202 // Maybe test support of two images in a single archive? It should be transparently handled by adding a reference to srcRef. 203 204 // == dir: 205 { // Absolute path 206 "dir:/dev/this-does-not-exist", 207 []expected{{"localhost/dev/this-does-not-exist", "localhost/dev/this-does-not-exist:latest"}}, 208 false, 209 }, 210 { // Relative path, single element. 211 "dir:this-does-not-exist", 212 []expected{{"localhost/this-does-not-exist", "localhost/this-does-not-exist:latest"}}, 213 false, 214 }, 215 { // Relative path, multiple elements. 216 "dir:testdata/this-does-not-exist", 217 []expected{{"localhost/testdata/this-does-not-exist", "localhost/testdata/this-does-not-exist:latest"}}, 218 false, 219 }, 220 221 // == Others, notably: 222 // === docker:// (has ImageReference.DockerReference) 223 { // Fully-specified input 224 "docker://docker.io/library/busybox:latest", 225 []expected{{"docker://docker.io/library/busybox:latest", "docker.io/library/busybox:latest"}}, 226 false, 227 }, 228 { // Minimal form of the input 229 "docker://busybox", 230 []expected{{"docker://busybox", "docker.io/library/busybox:latest"}}, 231 false, 232 }, 233 234 // === tarball: (as an example of what happens when ImageReference.DockerReference is nil). 235 // FIXME? This tries to parse "tarball:/dev/null" as a storageReference, and fails. 236 // (This is NOT an API promise that the results will continue to be this way.) 237 {"tarball:/dev/null", nil, false}, 238 } { 239 srcRef, err := alltransports.ParseImageName(c.srcName) 240 require.NoError(t, err, c.srcName) 241 242 res, err := ir.pullGoalFromImageReference(context.Background(), srcRef, c.srcName, nil) 243 if len(c.expected) == 0 { 244 assert.Error(t, err, c.srcName) 245 } else { 246 require.NoError(t, err, c.srcName) 247 require.Len(t, res.refPairs, len(c.expected), c.srcName) 248 for i, e := range c.expected { 249 testDescription := fmt.Sprintf("%s #%d", c.srcName, i) 250 assert.Equal(t, e.image, res.refPairs[i].image, testDescription) 251 assert.Equal(t, srcRef, res.refPairs[i].srcRef, testDescription) 252 assert.Equal(t, e.dstName, storageReferenceWithoutLocation(res.refPairs[i].dstRef), testDescription) 253 } 254 assert.Equal(t, c.expectedPullAllPairs, res.pullAllPairs, c.srcName) 255 assert.False(t, res.usedSearchRegistries, c.srcName) 256 assert.Nil(t, res.searchedRegistries, c.srcName) 257 } 258 } 259 } 260 261 const registriesConfWithSearch = `[registries.search] 262 registries = ['example.com', 'docker.io'] 263 ` 264 265 func TestPullGoalFromPossiblyUnqualifiedName(t *testing.T) { 266 const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" 267 type pullRefStrings struct{ image, srcRef, dstName string } // pullRefPair with string data only 268 269 registriesConf, err := ioutil.TempFile("", "TestPullGoalFromPossiblyUnqualifiedName") 270 require.NoError(t, err) 271 defer registriesConf.Close() 272 defer os.Remove(registriesConf.Name()) 273 274 err = ioutil.WriteFile(registriesConf.Name(), []byte(registriesConfWithSearch), 0600) 275 require.NoError(t, err) 276 277 ir, cleanup := newTestRuntime(t) 278 defer cleanup() 279 280 // Environment is per-process, so this looks very unsafe; actually it seems fine because tests are not 281 // run in parallel unless they opt in by calling t.Parallel(). So don’t do that. 282 oldRCP, hasRCP := os.LookupEnv("REGISTRIES_CONFIG_PATH") 283 defer func() { 284 if hasRCP { 285 os.Setenv("REGISTRIES_CONFIG_PATH", oldRCP) 286 } else { 287 os.Unsetenv("REGISTRIES_CONFIG_PATH") 288 } 289 }() 290 os.Setenv("REGISTRIES_CONFIG_PATH", registriesConf.Name()) 291 292 for _, c := range []struct { 293 input string 294 expected []pullRefStrings 295 expectedUsedSearchRegistries bool 296 }{ 297 {"#", nil, false}, // Clearly invalid. 298 { // Fully-explicit docker.io, name-only. 299 "docker.io/library/busybox", 300 // (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".) 301 []pullRefStrings{{"docker.io/library/busybox", "docker://busybox:latest", "docker.io/library/busybox:latest"}}, 302 false, 303 }, 304 { // docker.io with implied /library/, name-only. 305 "docker.io/busybox", 306 // (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".) 307 []pullRefStrings{{"docker.io/busybox", "docker://busybox:latest", "docker.io/library/busybox:latest"}}, 308 false, 309 }, 310 { // Qualified example.com, name-only. 311 "example.com/ns/busybox", 312 []pullRefStrings{{"example.com/ns/busybox", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox:latest"}}, 313 false, 314 }, 315 { // Qualified example.com, name:tag. 316 "example.com/ns/busybox:notlatest", 317 []pullRefStrings{{"example.com/ns/busybox:notlatest", "docker://example.com/ns/busybox:notlatest", "example.com/ns/busybox:notlatest"}}, 318 false, 319 }, 320 { // Qualified example.com, name@digest. 321 "example.com/ns/busybox" + digestSuffix, 322 []pullRefStrings{{"example.com/ns/busybox" + digestSuffix, "docker://example.com/ns/busybox" + digestSuffix, 323 "example.com/ns/busybox" + digestSuffix}}, 324 false, 325 }, 326 // Qualified example.com, name:tag@digest. This code is happy to try, but .srcRef parsing currently rejects such input. 327 {"example.com/ns/busybox:notlatest" + digestSuffix, nil, false}, 328 { // Unqualified, single-name, name-only 329 "busybox", 330 []pullRefStrings{ 331 {"example.com/busybox", "docker://example.com/busybox:latest", "example.com/busybox:latest"}, 332 // (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".) 333 {"docker.io/library/busybox", "docker://busybox:latest", "docker.io/library/busybox:latest"}, 334 }, 335 true, 336 }, 337 { // Unqualified, namespaced, name-only 338 "ns/busybox", 339 []pullRefStrings{ 340 {"example.com/ns/busybox", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox:latest"}, 341 }, 342 true, 343 }, 344 { // Unqualified, name:tag 345 "busybox:notlatest", 346 []pullRefStrings{ 347 {"example.com/busybox:notlatest", "docker://example.com/busybox:notlatest", "example.com/busybox:notlatest"}, 348 // (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".) 349 {"docker.io/library/busybox:notlatest", "docker://busybox:notlatest", "docker.io/library/busybox:notlatest"}, 350 }, 351 true, 352 }, 353 { // Unqualified, name@digest 354 "busybox" + digestSuffix, 355 []pullRefStrings{ 356 {"example.com/busybox" + digestSuffix, "docker://example.com/busybox" + digestSuffix, "example.com/busybox" + digestSuffix}, 357 // (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".) 358 {"docker.io/library/busybox" + digestSuffix, "docker://busybox" + digestSuffix, "docker.io/library/busybox" + digestSuffix}, 359 }, 360 true, 361 }, 362 // Unqualified, name:tag@digest. This code is happy to try, but .srcRef parsing currently rejects such input. 363 {"busybox:notlatest" + digestSuffix, nil, false}, 364 } { 365 res, err := ir.pullGoalFromPossiblyUnqualifiedName(c.input) 366 if len(c.expected) == 0 { 367 assert.Error(t, err, c.input) 368 } else { 369 assert.NoError(t, err, c.input) 370 for i, e := range c.expected { 371 testDescription := fmt.Sprintf("%s #%d", c.input, i) 372 assert.Equal(t, e.image, res.refPairs[i].image, testDescription) 373 assert.Equal(t, e.srcRef, transports.ImageName(res.refPairs[i].srcRef), testDescription) 374 assert.Equal(t, e.dstName, storageReferenceWithoutLocation(res.refPairs[i].dstRef), testDescription) 375 } 376 assert.False(t, res.pullAllPairs, c.input) 377 assert.Equal(t, c.expectedUsedSearchRegistries, res.usedSearchRegistries, c.input) 378 if !c.expectedUsedSearchRegistries { 379 assert.Nil(t, res.searchedRegistries, c.input) 380 } else { 381 assert.Equal(t, []string{"example.com", "docker.io"}, res.searchedRegistries, c.input) 382 } 383 } 384 } 385 }