github.com/khulnasoft-lab/khulnasoft@v26.0.1-0.20240328202558-330a6f959fe0+incompatible/integration/image/save_test.go (about) 1 package image 2 3 import ( 4 "archive/tar" 5 "encoding/json" 6 "io" 7 "io/fs" 8 "os" 9 "path/filepath" 10 "reflect" 11 "sort" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/cpuguy83/tar2go" 17 containertypes "github.com/docker/docker/api/types/container" 18 "github.com/docker/docker/api/types/versions" 19 "github.com/docker/docker/integration/internal/build" 20 "github.com/docker/docker/integration/internal/container" 21 "github.com/docker/docker/internal/testutils" 22 "github.com/docker/docker/internal/testutils/specialimage" 23 "github.com/docker/docker/pkg/archive" 24 "github.com/docker/docker/testutil/fakecontext" 25 "github.com/opencontainers/go-digest" 26 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 27 "gotest.tools/v3/assert" 28 "gotest.tools/v3/assert/cmp" 29 is "gotest.tools/v3/assert/cmp" 30 "gotest.tools/v3/skip" 31 ) 32 33 type imageSaveManifestEntry struct { 34 Config string 35 RepoTags []string 36 Layers []string 37 } 38 39 func tarIndexFS(t *testing.T, rdr io.Reader) fs.FS { 40 t.Helper() 41 42 dir := t.TempDir() 43 44 f, err := os.Create(filepath.Join(dir, "image.tar")) 45 assert.NilError(t, err) 46 47 // Do not close at the end of this function otherwise the indexer won't work 48 t.Cleanup(func() { f.Close() }) 49 50 _, err = io.Copy(f, rdr) 51 assert.NilError(t, err) 52 53 return tar2go.NewIndex(f).FS() 54 } 55 56 func TestSaveCheckTimes(t *testing.T) { 57 ctx := setupTest(t) 58 59 t.Parallel() 60 client := testEnv.APIClient() 61 62 const repoName = "busybox:latest" 63 img, _, err := client.ImageInspectWithRaw(ctx, repoName) 64 assert.NilError(t, err) 65 66 rdr, err := client.ImageSave(ctx, []string{repoName}) 67 assert.NilError(t, err) 68 69 tarfs := tarIndexFS(t, rdr) 70 71 dt, err := fs.ReadFile(tarfs, "manifest.json") 72 assert.NilError(t, err) 73 74 var ls []imageSaveManifestEntry 75 assert.NilError(t, json.Unmarshal(dt, &ls)) 76 assert.Assert(t, cmp.Len(ls, 1)) 77 78 info, err := fs.Stat(tarfs, ls[0].Config) 79 assert.NilError(t, err) 80 81 created, err := time.Parse(time.RFC3339, img.Created) 82 assert.NilError(t, err) 83 84 if testEnv.UsingSnapshotter() { 85 // containerd archive export sets the mod time to zero. 86 assert.Check(t, is.Equal(info.ModTime(), time.Unix(0, 0))) 87 } else { 88 assert.Check(t, is.Equal(info.ModTime().Format(time.RFC3339), created.Format(time.RFC3339))) 89 } 90 } 91 92 // Regression test for https://github.com/moby/moby/issues/47065 93 func TestSaveOCI(t *testing.T) { 94 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.44"), "OCI layout support was introduced in v25") 95 96 ctx := setupTest(t) 97 client := testEnv.APIClient() 98 99 const busybox = "busybox:latest" 100 inspectBusybox, _, err := client.ImageInspectWithRaw(ctx, busybox) 101 assert.NilError(t, err) 102 103 type testCase struct { 104 image string 105 expectedOCIRef string 106 expectedContainerdRef string 107 } 108 109 testCases := []testCase{ 110 // Busybox by tagged name 111 testCase{image: busybox, expectedContainerdRef: "docker.io/library/busybox:latest", expectedOCIRef: "latest"}, 112 113 // Busybox by ID 114 testCase{image: inspectBusybox.ID}, 115 } 116 117 if testEnv.DaemonInfo.OSType != "windows" { 118 multiLayerImage := specialimage.Load(ctx, t, client, specialimage.MultiLayer) 119 // Multi-layer image 120 testCases = append(testCases, testCase{image: multiLayerImage, expectedContainerdRef: "docker.io/library/multilayer:latest", expectedOCIRef: "latest"}) 121 122 } 123 124 // Busybox frozen image will have empty RepoDigests when loaded into the 125 // graphdriver image store so we can't use it. 126 // This will work with the containerd image store though. 127 if len(inspectBusybox.RepoDigests) > 0 { 128 // Digested reference 129 testCases = append(testCases, testCase{ 130 image: inspectBusybox.RepoDigests[0], 131 }) 132 } 133 134 for _, tc := range testCases { 135 tc := tc 136 t.Run(tc.image, func(t *testing.T) { 137 // Get information about the original image. 138 inspect, _, err := client.ImageInspectWithRaw(ctx, tc.image) 139 assert.NilError(t, err) 140 141 rdr, err := client.ImageSave(ctx, []string{tc.image}) 142 assert.NilError(t, err) 143 defer rdr.Close() 144 145 tarfs := tarIndexFS(t, rdr) 146 147 indexData, err := fs.ReadFile(tarfs, "index.json") 148 assert.NilError(t, err, "failed to read index.json") 149 150 var index ocispec.Index 151 assert.NilError(t, json.Unmarshal(indexData, &index), "failed to unmarshal index.json") 152 153 // All test images are single-platform, so they should have only one manifest. 154 assert.Assert(t, is.Len(index.Manifests, 1)) 155 156 manifestData, err := fs.ReadFile(tarfs, "blobs/sha256/"+index.Manifests[0].Digest.Encoded()) 157 assert.NilError(t, err) 158 159 var manifest ocispec.Manifest 160 assert.NilError(t, json.Unmarshal(manifestData, &manifest)) 161 162 t.Run("Manifest", func(t *testing.T) { 163 assert.Check(t, is.Len(manifest.Layers, len(inspect.RootFS.Layers))) 164 165 var digests []string 166 // Check if layers referenced by the manifest exist in the archive 167 // and match the layers from the original image. 168 for _, l := range manifest.Layers { 169 layerPath := "blobs/sha256/" + l.Digest.Encoded() 170 stat, err := fs.Stat(tarfs, layerPath) 171 assert.NilError(t, err) 172 173 assert.Check(t, is.Equal(l.Size, stat.Size())) 174 175 f, err := tarfs.Open(layerPath) 176 assert.NilError(t, err) 177 178 layerDigest, err := testutils.UncompressedTarDigest(f) 179 f.Close() 180 181 assert.NilError(t, err) 182 183 digests = append(digests, layerDigest.String()) 184 } 185 186 assert.Check(t, is.DeepEqual(digests, inspect.RootFS.Layers)) 187 }) 188 189 t.Run("Config", func(t *testing.T) { 190 configData, err := fs.ReadFile(tarfs, "blobs/sha256/"+manifest.Config.Digest.Encoded()) 191 assert.NilError(t, err) 192 193 var config ocispec.Image 194 assert.NilError(t, json.Unmarshal(configData, &config)) 195 196 var diffIDs []string 197 for _, l := range config.RootFS.DiffIDs { 198 diffIDs = append(diffIDs, l.String()) 199 } 200 201 assert.Check(t, is.DeepEqual(diffIDs, inspect.RootFS.Layers)) 202 }) 203 204 t.Run("Containerd image name", func(t *testing.T) { 205 assert.Check(t, is.Equal(index.Manifests[0].Annotations["io.containerd.image.name"], tc.expectedContainerdRef)) 206 }) 207 208 t.Run("OCI reference tag", func(t *testing.T) { 209 assert.Check(t, is.Equal(index.Manifests[0].Annotations["org.opencontainers.image.ref.name"], tc.expectedOCIRef)) 210 }) 211 212 }) 213 } 214 } 215 216 func TestSaveRepoWithMultipleImages(t *testing.T) { 217 ctx := setupTest(t) 218 client := testEnv.APIClient() 219 220 makeImage := func(from string, tag string) string { 221 id := container.Create(ctx, t, client, func(cfg *container.TestContainerConfig) { 222 cfg.Config.Image = from 223 cfg.Config.Cmd = []string{"true"} 224 }) 225 226 res, err := client.ContainerCommit(ctx, id, containertypes.CommitOptions{Reference: tag}) 227 assert.NilError(t, err) 228 229 err = client.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true}) 230 assert.NilError(t, err) 231 232 return res.ID 233 } 234 235 busyboxImg, _, err := client.ImageInspectWithRaw(ctx, "busybox:latest") 236 assert.NilError(t, err) 237 238 const repoName = "foobar-save-multi-images-test" 239 const tagFoo = repoName + ":foo" 240 const tagBar = repoName + ":bar" 241 242 idFoo := makeImage("busybox:latest", tagFoo) 243 idBar := makeImage("busybox:latest", tagBar) 244 idBusybox := busyboxImg.ID 245 246 rdr, err := client.ImageSave(ctx, []string{repoName, "busybox:latest"}) 247 assert.NilError(t, err) 248 defer rdr.Close() 249 250 tarfs := tarIndexFS(t, rdr) 251 252 dt, err := fs.ReadFile(tarfs, "manifest.json") 253 assert.NilError(t, err) 254 255 var mfstLs []imageSaveManifestEntry 256 assert.NilError(t, json.Unmarshal(dt, &mfstLs)) 257 258 actual := make([]string, 0, len(mfstLs)) 259 for _, m := range mfstLs { 260 actual = append(actual, strings.TrimPrefix(m.Config, "blobs/sha256/")) 261 // make sure the blob actually exists 262 _, err = fs.Stat(tarfs, m.Config) 263 assert.Check(t, err) 264 } 265 266 expected := []string{idBusybox, idFoo, idBar} 267 // prefixes are not in tar 268 for i := range expected { 269 expected[i] = digest.Digest(expected[i]).Encoded() 270 } 271 272 // With snapshotters, ID of the image is the ID of the manifest/index 273 // With graphdrivers, ID of the image is the ID of the image config 274 if testEnv.UsingSnapshotter() { 275 // ID of image won't match the Config ID from manifest.json 276 // Just check if manifests exist in blobs 277 for _, blob := range expected { 278 _, err = fs.Stat(tarfs, "blobs/sha256/"+blob) 279 assert.Check(t, err) 280 } 281 } else { 282 sort.Strings(actual) 283 sort.Strings(expected) 284 assert.Assert(t, cmp.DeepEqual(actual, expected), "archive does not contains the right layers: got %v, expected %v", actual, expected) 285 } 286 } 287 288 func TestSaveDirectoryPermissions(t *testing.T) { 289 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "Test is looking at linux specific details") 290 291 ctx := setupTest(t) 292 client := testEnv.APIClient() 293 294 layerEntries := []string{"opt/", "opt/a/", "opt/a/b/", "opt/a/b/c"} 295 layerEntriesAUFS := []string{"./", ".wh..wh.aufs", ".wh..wh.orph/", ".wh..wh.plnk/", "opt/", "opt/a/", "opt/a/b/", "opt/a/b/c"} 296 297 dockerfile := `FROM busybox 298 RUN adduser -D user && mkdir -p /opt/a/b && chown -R user:user /opt/a 299 RUN touch /opt/a/b/c && chown user:user /opt/a/b/c` 300 301 imgID := build.Do(ctx, t, client, fakecontext.New(t, t.TempDir(), fakecontext.WithDockerfile(dockerfile))) 302 303 rdr, err := client.ImageSave(ctx, []string{imgID}) 304 assert.NilError(t, err) 305 defer rdr.Close() 306 307 tarfs := tarIndexFS(t, rdr) 308 309 dt, err := fs.ReadFile(tarfs, "manifest.json") 310 assert.NilError(t, err) 311 312 var mfstLs []imageSaveManifestEntry 313 assert.NilError(t, json.Unmarshal(dt, &mfstLs)) 314 315 var found bool 316 317 for _, p := range mfstLs[0].Layers { 318 var entriesSansDev []string 319 320 f, err := tarfs.Open(p) 321 assert.NilError(t, err) 322 323 entries, err := listTar(f) 324 f.Close() 325 assert.NilError(t, err) 326 327 for _, e := range entries { 328 if !strings.Contains(e, "dev/") { 329 entriesSansDev = append(entriesSansDev, e) 330 } 331 } 332 assert.NilError(t, err, "encountered error while listing tar entries: %s", err) 333 334 if reflect.DeepEqual(entriesSansDev, layerEntries) || reflect.DeepEqual(entriesSansDev, layerEntriesAUFS) { 335 found = true 336 break 337 } 338 } 339 340 assert.Assert(t, found, "failed to find the layer with the right content listing") 341 } 342 343 func listTar(f io.Reader) ([]string, error) { 344 // If using the containerd snapshotter, the tar file may be compressed 345 dec, err := archive.DecompressStream(f) 346 if err != nil { 347 return nil, err 348 } 349 defer dec.Close() 350 351 tr := tar.NewReader(dec) 352 var entries []string 353 354 for { 355 th, err := tr.Next() 356 if err == io.EOF { 357 // end of tar archive 358 return entries, nil 359 } 360 if err != nil { 361 return entries, err 362 } 363 entries = append(entries, th.Name) 364 } 365 }