save_test.go 10 KB

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