github.com/containers/podman/v2@v2.2.2-0.20210501105131-c1e07d070c4c/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 true, 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 { // Reference image by name in multi-image archive 174 "docker-archive:testdata/docker-two-images.tar.xz:example.com/empty:latest", 175 []expected{ 176 {"example.com/empty:latest", "example.com/empty:latest"}, 177 }, 178 true, 179 }, 180 { // Reference image by name in multi-image archive 181 "docker-archive:testdata/docker-two-images.tar.xz:example.com/empty/but:different", 182 []expected{ 183 {"example.com/empty/but:different", "example.com/empty/but:different"}, 184 }, 185 true, 186 }, 187 { // Reference image by index in multi-image archive 188 "docker-archive:testdata/docker-two-images.tar.xz:@0", 189 []expected{ 190 {"example.com/empty:latest", "example.com/empty:latest"}, 191 }, 192 true, 193 }, 194 { // Reference image by index in multi-image archive 195 "docker-archive:testdata/docker-two-images.tar.xz:@1", 196 []expected{ 197 {"example.com/empty/but:different", "example.com/empty/but:different"}, 198 }, 199 true, 200 }, 201 { // Reference entire multi-image archive must fail (more than one manifest) 202 "docker-archive:testdata/docker-two-images.tar.xz", 203 []expected{}, 204 true, 205 }, 206 207 // == oci-archive: 208 {"oci-archive:/dev/this-does-not-exist", nil, false}, // Input does not exist. 209 {"oci-archive:/dev/null", nil, false}, // Input exists but does not contain a manifest. 210 // FIXME: The remaining tests are commented out for now, because oci-archive: does not work unprivileged. 211 // { // No name annotation 212 // "oci-archive:testdata/oci-unnamed.tar.gz", 213 // []expected{{"@5c8aca8137ac47e84c69ae93ce650ce967917cc001ba7aad5494073fac75b8b6", "@5c8aca8137ac47e84c69ae93ce650ce967917cc001ba7aad5494073fac75b8b6"}}, 214 // false, 215 // }, 216 // { // Name is a name:latest (no normalization is defined). 217 // "oci-archive:testdata/oci-name-only.tar.gz", 218 // []expected{{"localhost/pretty-empty:latest", "localhost/pretty-empty:latest"}}, 219 // false, 220 // }, 221 // { // Name is a registry/name:latest 222 // "oci-archive:testdata/oci-registry-name.tar.gz", 223 // []expected{{"example.com/empty:latest", "example.com/empty:latest"}}, 224 // false, 225 // }, 226 // // Name exists, but is an invalid Docker reference; such names will fail when creating dstReference. 227 // {"oci-archive:testdata/oci-non-docker-name.tar.gz", nil, false}, 228 // Maybe test support of two images in a single archive? It should be transparently handled by adding a reference to srcRef. 229 230 // == dir: 231 { // Absolute path 232 "dir:/dev/this-does-not-exist", 233 []expected{{"localhost/dev/this-does-not-exist", "localhost/dev/this-does-not-exist:latest"}}, 234 false, 235 }, 236 { // Relative path, single element. 237 "dir:this-does-not-exist", 238 []expected{{"localhost/this-does-not-exist", "localhost/this-does-not-exist:latest"}}, 239 false, 240 }, 241 { // Relative path, multiple elements. 242 "dir:testdata/this-does-not-exist", 243 []expected{{"localhost/testdata/this-does-not-exist", "localhost/testdata/this-does-not-exist:latest"}}, 244 false, 245 }, 246 247 // == Others, notably: 248 // === docker:// (has ImageReference.DockerReference) 249 { // Fully-specified input 250 "docker://docker.io/library/busybox:latest", 251 []expected{{"docker://docker.io/library/busybox:latest", "docker.io/library/busybox:latest"}}, 252 false, 253 }, 254 { // Minimal form of the input 255 "docker://busybox", 256 []expected{{"docker://busybox", "docker.io/library/busybox:latest"}}, 257 false, 258 }, 259 260 // === tarball: (as an example of what happens when ImageReference.DockerReference is nil). 261 // FIXME? This tries to parse "tarball:/dev/null" as a storageReference, and fails. 262 // (This is NOT an API promise that the results will continue to be this way.) 263 {"tarball:/dev/null", nil, false}, 264 } { 265 srcRef, err := alltransports.ParseImageName(c.srcName) 266 require.NoError(t, err, c.srcName) 267 268 res, err := ir.pullGoalFromImageReference(context.Background(), srcRef, c.srcName, nil) 269 if len(c.expected) == 0 { 270 assert.Error(t, err, c.srcName) 271 } else { 272 require.NoError(t, err, c.srcName) 273 require.Len(t, res.refPairs, len(c.expected), c.srcName) 274 for i, e := range c.expected { 275 testDescription := fmt.Sprintf("%s #%d", c.srcName, i) 276 assert.Equal(t, e.image, res.refPairs[i].image, testDescription) 277 assert.Equal(t, transports.ImageName(srcRef), transports.ImageName(res.refPairs[i].srcRef), testDescription) 278 assert.Equal(t, e.dstName, storageReferenceWithoutLocation(res.refPairs[i].dstRef), testDescription) 279 } 280 assert.Equal(t, c.expectedPullAllPairs, res.pullAllPairs, c.srcName) 281 } 282 } 283 } 284 285 const registriesConfWithSearch = `unqualified-search-registries = ['example.com', 'docker.io']` 286 287 func TestPullGoalFromPossiblyUnqualifiedName(t *testing.T) { 288 const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" 289 type pullRefStrings struct{ image, srcRef, dstName string } // pullRefPair with string data only 290 291 registriesConf, err := ioutil.TempFile("", "TestPullGoalFromPossiblyUnqualifiedName") 292 require.NoError(t, err) 293 defer registriesConf.Close() 294 defer os.Remove(registriesConf.Name()) 295 296 err = ioutil.WriteFile(registriesConf.Name(), []byte(registriesConfWithSearch), 0600) 297 require.NoError(t, err) 298 299 ir, cleanup := newTestRuntime(t) 300 defer cleanup() 301 302 sc := GetSystemContext("", "", false) 303 304 aliasesConf, err := ioutil.TempFile("", "short-name-aliases.conf") 305 require.NoError(t, err) 306 defer aliasesConf.Close() 307 defer os.Remove(aliasesConf.Name()) 308 sc.UserShortNameAliasConfPath = aliasesConf.Name() 309 sc.SystemRegistriesConfPath = registriesConf.Name() 310 311 for _, c := range []struct { 312 input string 313 expected []pullRefStrings 314 }{ 315 {"#", nil}, // Clearly invalid. 316 { // Fully-explicit docker.io, name-only. 317 "docker.io/library/busybox", 318 // (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".) 319 []pullRefStrings{{"docker.io/library/busybox:latest", "docker://busybox:latest", "docker.io/library/busybox:latest"}}, 320 }, 321 { // docker.io with implied /library/, name-only. 322 "docker.io/busybox", 323 // (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".) 324 []pullRefStrings{{"docker.io/library/busybox:latest", "docker://busybox:latest", "docker.io/library/busybox:latest"}}, 325 }, 326 { // Qualified example.com, name-only. 327 "example.com/ns/busybox", 328 []pullRefStrings{{"example.com/ns/busybox:latest", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox:latest"}}, 329 }, 330 { // Qualified example.com, name:tag. 331 "example.com/ns/busybox:notlatest", 332 []pullRefStrings{{"example.com/ns/busybox:notlatest", "docker://example.com/ns/busybox:notlatest", "example.com/ns/busybox:notlatest"}}, 333 }, 334 { // Qualified example.com, name@digest. 335 "example.com/ns/busybox" + digestSuffix, 336 []pullRefStrings{{"example.com/ns/busybox" + digestSuffix, "docker://example.com/ns/busybox" + digestSuffix, 337 "example.com/ns/busybox" + digestSuffix}}, 338 }, 339 // Qualified example.com, name:tag@digest. This code is happy to try, but .srcRef parsing currently rejects such input. 340 {"example.com/ns/busybox:notlatest" + digestSuffix, nil}, 341 { // Unqualified, single-name, name-only 342 "busybox", 343 []pullRefStrings{ 344 {"example.com/busybox:latest", "docker://example.com/busybox:latest", "example.com/busybox:latest"}, 345 // (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".) 346 {"docker.io/library/busybox:latest", "docker://busybox:latest", "docker.io/library/busybox:latest"}, 347 }, 348 }, 349 { // Unqualified, namespaced, name-only 350 "ns/busybox", 351 []pullRefStrings{ 352 {"example.com/ns/busybox:latest", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox:latest"}, 353 }, 354 }, 355 { // Unqualified, name:tag 356 "busybox:notlatest", 357 []pullRefStrings{ 358 {"example.com/busybox:notlatest", "docker://example.com/busybox:notlatest", "example.com/busybox:notlatest"}, 359 // (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".) 360 {"docker.io/library/busybox:notlatest", "docker://busybox:notlatest", "docker.io/library/busybox:notlatest"}, 361 }, 362 }, 363 { // Unqualified, name@digest 364 "busybox" + digestSuffix, 365 []pullRefStrings{ 366 {"example.com/busybox" + digestSuffix, "docker://example.com/busybox" + digestSuffix, "example.com/busybox" + digestSuffix}, 367 // (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".) 368 {"docker.io/library/busybox" + digestSuffix, "docker://busybox" + digestSuffix, "docker.io/library/busybox" + digestSuffix}, 369 }, 370 }, 371 // Unqualified, name:tag@digest. This code is happy to try, but .srcRef parsing currently rejects such input. 372 {"busybox:notlatest" + digestSuffix, nil}, 373 } { 374 res, err := ir.pullGoalFromPossiblyUnqualifiedName(sc, nil, c.input) 375 if len(c.expected) == 0 { 376 assert.Error(t, err, c.input) 377 } else { 378 assert.NoError(t, err, c.input) 379 for i, e := range c.expected { 380 testDescription := fmt.Sprintf("%s #%d (%v)", c.input, i, res.refPairs) 381 assert.Equal(t, e.image, res.refPairs[i].image, testDescription) 382 assert.Equal(t, e.srcRef, transports.ImageName(res.refPairs[i].srcRef), testDescription) 383 assert.Equal(t, e.dstName, storageReferenceWithoutLocation(res.refPairs[i].dstRef), testDescription) 384 } 385 assert.False(t, res.pullAllPairs, c.input) 386 } 387 } 388 }