github.com/rish1988/moby@v25.0.2+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/pkg/archive" 22 "github.com/docker/docker/testutil/fakecontext" 23 "github.com/opencontainers/go-digest" 24 ocispec "github.com/opencontainers/image-spec/specs-go/v1" 25 "gotest.tools/v3/assert" 26 "gotest.tools/v3/assert/cmp" 27 is "gotest.tools/v3/assert/cmp" 28 "gotest.tools/v3/skip" 29 ) 30 31 type imageSaveManifestEntry struct { 32 Config string 33 RepoTags []string 34 Layers []string 35 } 36 37 func tarIndexFS(t *testing.T, rdr io.Reader) fs.FS { 38 t.Helper() 39 40 dir := t.TempDir() 41 42 f, err := os.Create(filepath.Join(dir, "image.tar")) 43 assert.NilError(t, err) 44 45 // Do not close at the end of this function otherwise the indexer won't work 46 t.Cleanup(func() { f.Close() }) 47 48 _, err = io.Copy(f, rdr) 49 assert.NilError(t, err) 50 51 return tar2go.NewIndex(f).FS() 52 } 53 54 func TestSaveCheckTimes(t *testing.T) { 55 ctx := setupTest(t) 56 57 t.Parallel() 58 client := testEnv.APIClient() 59 60 const repoName = "busybox:latest" 61 img, _, err := client.ImageInspectWithRaw(ctx, repoName) 62 assert.NilError(t, err) 63 64 rdr, err := client.ImageSave(ctx, []string{repoName}) 65 assert.NilError(t, err) 66 67 tarfs := tarIndexFS(t, rdr) 68 69 dt, err := fs.ReadFile(tarfs, "manifest.json") 70 assert.NilError(t, err) 71 72 var ls []imageSaveManifestEntry 73 assert.NilError(t, json.Unmarshal(dt, &ls)) 74 assert.Assert(t, cmp.Len(ls, 1)) 75 76 info, err := fs.Stat(tarfs, ls[0].Config) 77 assert.NilError(t, err) 78 79 created, err := time.Parse(time.RFC3339, img.Created) 80 assert.NilError(t, err) 81 82 if testEnv.UsingSnapshotter() { 83 // containerd archive export sets the mod time to zero. 84 assert.Check(t, is.Equal(info.ModTime(), time.Unix(0, 0))) 85 } else { 86 assert.Check(t, is.Equal(info.ModTime().Format(time.RFC3339), created.Format(time.RFC3339))) 87 } 88 } 89 90 // Regression test for https://github.com/moby/moby/issues/47065 91 func TestSaveCheckManifestLayers(t *testing.T) { 92 skip.If(t, versions.LessThan(testEnv.DaemonAPIVersion(), "1.44"), "OCI layout support was introduced in v25") 93 94 ctx := setupTest(t) 95 client := testEnv.APIClient() 96 97 t.Parallel() 98 99 const repoName = "busybox:latest" 100 img, _, err := client.ImageInspectWithRaw(ctx, repoName) 101 assert.NilError(t, err) 102 103 rdr, err := client.ImageSave(ctx, []string{repoName}) 104 assert.NilError(t, err) 105 106 tarfs := tarIndexFS(t, rdr) 107 108 indexData, err := fs.ReadFile(tarfs, "index.json") 109 assert.NilError(t, err) 110 111 var index ocispec.Index 112 assert.NilError(t, json.Unmarshal(indexData, &index)) 113 114 assert.Assert(t, is.Len(index.Manifests, 1)) 115 116 manifestData, err := fs.ReadFile(tarfs, "blobs/sha256/"+index.Manifests[0].Digest.Encoded()) 117 assert.NilError(t, err) 118 119 var manifest ocispec.Manifest 120 assert.NilError(t, json.Unmarshal(manifestData, &manifest)) 121 122 assert.Check(t, is.Len(manifest.Layers, len(img.RootFS.Layers))) 123 for _, l := range manifest.Layers { 124 stat, err := fs.Stat(tarfs, "blobs/sha256/"+l.Digest.Encoded()) 125 if !assert.Check(t, err) { 126 continue 127 } 128 129 assert.Check(t, is.Equal(l.Size, stat.Size())) 130 } 131 } 132 133 func TestSaveRepoWithMultipleImages(t *testing.T) { 134 ctx := setupTest(t) 135 client := testEnv.APIClient() 136 137 makeImage := func(from string, tag string) string { 138 id := container.Create(ctx, t, client, func(cfg *container.TestContainerConfig) { 139 cfg.Config.Image = from 140 cfg.Config.Cmd = []string{"true"} 141 }) 142 143 res, err := client.ContainerCommit(ctx, id, containertypes.CommitOptions{Reference: tag}) 144 assert.NilError(t, err) 145 146 err = client.ContainerRemove(ctx, id, containertypes.RemoveOptions{Force: true}) 147 assert.NilError(t, err) 148 149 return res.ID 150 } 151 152 busyboxImg, _, err := client.ImageInspectWithRaw(ctx, "busybox:latest") 153 assert.NilError(t, err) 154 155 const repoName = "foobar-save-multi-images-test" 156 const tagFoo = repoName + ":foo" 157 const tagBar = repoName + ":bar" 158 159 idFoo := makeImage("busybox:latest", tagFoo) 160 idBar := makeImage("busybox:latest", tagBar) 161 idBusybox := busyboxImg.ID 162 163 rdr, err := client.ImageSave(ctx, []string{repoName, "busybox:latest"}) 164 assert.NilError(t, err) 165 defer rdr.Close() 166 167 tarfs := tarIndexFS(t, rdr) 168 169 dt, err := fs.ReadFile(tarfs, "manifest.json") 170 assert.NilError(t, err) 171 172 var mfstLs []imageSaveManifestEntry 173 assert.NilError(t, json.Unmarshal(dt, &mfstLs)) 174 175 actual := make([]string, 0, len(mfstLs)) 176 for _, m := range mfstLs { 177 actual = append(actual, strings.TrimPrefix(m.Config, "blobs/sha256/")) 178 // make sure the blob actually exists 179 _, err = fs.Stat(tarfs, m.Config) 180 assert.Check(t, err) 181 } 182 183 expected := []string{idBusybox, idFoo, idBar} 184 // prefixes are not in tar 185 for i := range expected { 186 expected[i] = digest.Digest(expected[i]).Encoded() 187 } 188 189 // With snapshotters, ID of the image is the ID of the manifest/index 190 // With graphdrivers, ID of the image is the ID of the image config 191 if testEnv.UsingSnapshotter() { 192 // ID of image won't match the Config ID from manifest.json 193 // Just check if manifests exist in blobs 194 for _, blob := range expected { 195 _, err = fs.Stat(tarfs, "blobs/sha256/"+blob) 196 assert.Check(t, err) 197 } 198 } else { 199 sort.Strings(actual) 200 sort.Strings(expected) 201 assert.Assert(t, cmp.DeepEqual(actual, expected), "archive does not contains the right layers: got %v, expected %v", actual, expected) 202 } 203 } 204 205 func TestSaveDirectoryPermissions(t *testing.T) { 206 skip.If(t, testEnv.DaemonInfo.OSType == "windows", "Test is looking at linux specific details") 207 208 ctx := setupTest(t) 209 client := testEnv.APIClient() 210 211 layerEntries := []string{"opt/", "opt/a/", "opt/a/b/", "opt/a/b/c"} 212 layerEntriesAUFS := []string{"./", ".wh..wh.aufs", ".wh..wh.orph/", ".wh..wh.plnk/", "opt/", "opt/a/", "opt/a/b/", "opt/a/b/c"} 213 214 dockerfile := `FROM busybox 215 RUN adduser -D user && mkdir -p /opt/a/b && chown -R user:user /opt/a 216 RUN touch /opt/a/b/c && chown user:user /opt/a/b/c` 217 218 imgID := build.Do(ctx, t, client, fakecontext.New(t, t.TempDir(), fakecontext.WithDockerfile(dockerfile))) 219 220 rdr, err := client.ImageSave(ctx, []string{imgID}) 221 assert.NilError(t, err) 222 defer rdr.Close() 223 224 tarfs := tarIndexFS(t, rdr) 225 226 dt, err := fs.ReadFile(tarfs, "manifest.json") 227 assert.NilError(t, err) 228 229 var mfstLs []imageSaveManifestEntry 230 assert.NilError(t, json.Unmarshal(dt, &mfstLs)) 231 232 var found bool 233 234 for _, p := range mfstLs[0].Layers { 235 var entriesSansDev []string 236 237 f, err := tarfs.Open(p) 238 assert.NilError(t, err) 239 240 entries, err := listTar(f) 241 f.Close() 242 assert.NilError(t, err) 243 244 for _, e := range entries { 245 if !strings.Contains(e, "dev/") { 246 entriesSansDev = append(entriesSansDev, e) 247 } 248 } 249 assert.NilError(t, err, "encountered error while listing tar entries: %s", err) 250 251 if reflect.DeepEqual(entriesSansDev, layerEntries) || reflect.DeepEqual(entriesSansDev, layerEntriesAUFS) { 252 found = true 253 break 254 } 255 } 256 257 assert.Assert(t, found, "failed to find the layer with the right content listing") 258 } 259 260 func listTar(f io.Reader) ([]string, error) { 261 // If using the containerd snapshotter, the tar file may be compressed 262 dec, err := archive.DecompressStream(f) 263 if err != nil { 264 return nil, err 265 } 266 defer dec.Close() 267 268 tr := tar.NewReader(dec) 269 var entries []string 270 271 for { 272 th, err := tr.Next() 273 if err == io.EOF { 274 // end of tar archive 275 return entries, nil 276 } 277 if err != nil { 278 return entries, err 279 } 280 entries = append(entries, th.Name) 281 } 282 }