hack: Load special images on demand

Rewrite `.build-empty-images` shell script that produced special images
(emptyfs with no layers, and empty danglign image) to a Go functions
that construct the same archives in a temporary directory.

Use them to load these images on demand only in the tests that need
them.

Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
This commit is contained in:
Paweł Gronowski 2023-12-05 11:59:19 +01:00
parent 2a38569337
commit bc94dfc7d2
No known key found for this signature in database
GPG key ID: B85EFCFE26DEF92A
14 changed files with 234 additions and 89 deletions

View file

@ -1,55 +0,0 @@
#!/usr/bin/env bash
set -e
function imageNotPresent {
local img="$1"
! docker image inspect "$img" > /dev/null 2> /dev/null
}
if imageNotPresent "emptyfs"; then
# build a "docker save" tarball for "emptyfs"
# see https://github.com/docker/docker/pull/5262
# and also https://github.com/docker/docker/issues/4242
dir="$DEST/emptyfs"
uuid=511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158
mkdir -p "$dir/$uuid"
(
echo '[{"Config":"11f64303f0f7ffdc71f001788132bca5346831939a956e3e975c93267d89a16d.json","RepoTags":["emptyfs:latest"],"Layers":["511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158/layer.tar"]}]' > "$dir/manifest.json"
echo -n '{"architecture":"x86_64","comment":"Imported from -","container_config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":null,"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"created":"2013-06-13T14:03:50.821769-07:00","docker_version":"0.4.0","history":[{"created":"2013-06-13T14:03:50.821769-07:00","comment":"Imported from -"}],"rootfs":{"type":"layers","diff_ids":["sha256:84ff92691f909a05b224e1c56abb4864f01b4f8e3c854e4bb4c7baf1d3f6d652"]}}' > "$dir/11f64303f0f7ffdc71f001788132bca5346831939a956e3e975c93267d89a16d.json"
echo '{"emptyfs":{"latest":"511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158"}}' > "$dir/repositories"
cd "$dir/$uuid"
echo '{"id":"511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158","comment":"Imported from -","created":"2013-06-13T14:03:50.821769-07:00","container_config":{"Hostname":"","Domainname":"","User":"","Memory":0,"MemorySwap":0,"CpuShares":0,"AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"PortSpecs":null,"ExposedPorts":null,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":null,"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"NetworkDisabled":false,"OnBuild":null},"docker_version":"0.4.0","architecture":"x86_64","Size":0}' > json
echo '1.0' > VERSION
tar -cf layer.tar --files-from /dev/null
)
(
[ -n "$TESTDEBUG" ] && set -x
tar -cC "$dir" . | docker load
)
rm -rf "$dir"
fi
# without c8d image store, image id is the config's id
dangling_cfg=0df1207206e5288f4a989a2f13d1f5b3c4e70467702c1d5d21dfc9f002b7bd43
# with c8d image store, image id is the id of manifest/manifest list.
dangling_mfst=16d365089e5c10e1673ee82ab5bba38ade9b763296ad918bd24b42a1156c5456
if imageNotPresent "$dangling_cfg" && imageNotPresent "$dangling_mfst"; then
dir="$DEST/dangling"
mkdir -p "$dir"
(
cd "$dir"
printf '{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"sha256:16d365089e5c10e1673ee82ab5bba38ade9b763296ad918bd24b42a1156c5456","size":264,"annotations":{"org.opencontainers.image.created":"2023-05-19T08:00:44Z"},"platform":{"architecture":"amd64","os":"linux"}}]}' > index.json
printf '[{"Config":"blobs/sha256/0df1207206e5288f4a989a2f13d1f5b3c4e70467702c1d5d21dfc9f002b7bd43","RepoTags":null,"Layers":null}]' > manifest.json
mkdir -p blobs/sha256
printf '{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:0df1207206e5288f4a989a2f13d1f5b3c4e70467702c1d5d21dfc9f002b7bd43","size":390},"layers":[]}' > blobs/sha256/$dangling_mfst
printf '{"architecture":"amd64","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"WorkingDir":"/","Labels":{"org.mobyproject.test.specialimage":"1"},"OnBuild":null},"created":null,"history":[{"created_by":"LABEL org.mobyproject.test.specialimage=1","comment":"buildkit.dockerfile.v0","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":null}}' > blobs/sha256/$dangling_cfg
tar -cf layer.tar --files-from /dev/null
)
(
[ -n "$TESTDEBUG" ] && set -x
tar -cC "$dir" . | docker load
)
rm -rf "$dir"
fi

View file

@ -162,7 +162,3 @@ while ! ${TEST_CLIENT_BINARY} version &> /dev/null; do
sleep 2
done
printf "\n"
if [ "$(docker version --format '{{ .Server.Os }}')" != 'windows' ]; then
bundle .build-empty-images
fi

View file

@ -60,5 +60,4 @@ test_env() {
)
}
sh /scripts/build-empty-images.sh
run_test_integration

View file

@ -13,6 +13,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/integration-cli/cli"
"github.com/docker/docker/internal/testutils/specialimage"
"gotest.tools/v3/assert"
"gotest.tools/v3/icmd"
)
@ -31,7 +32,7 @@ func (s *DockerCLIInspectSuite) OnTimeout(c *testing.T) {
func (s *DockerCLIInspectSuite) TestInspectImage(c *testing.T) {
testRequires(c, DaemonIsLinux)
imageTest := "emptyfs"
imageTest := loadSpecialImage(c, specialimage.EmptyFS)
// It is important that this ID remain stable. If a code change causes
// it to be different, this is equivalent to a cache bust when pulling
// a legacy-format manifest. If the check at the end of this function
@ -136,7 +137,8 @@ func (s *DockerCLIInspectSuite) TestInspectTypeFlagWithInvalidValue(c *testing.T
func (s *DockerCLIInspectSuite) TestInspectImageFilterInt(c *testing.T) {
testRequires(c, DaemonIsLinux)
imageTest := "emptyfs"
imageTest := loadSpecialImage(c, specialimage.EmptyFS)
out := inspectField(c, imageTest, "Size")
size, err := strconv.Atoi(out)

View file

@ -26,6 +26,7 @@ import (
"github.com/docker/docker/integration-cli/cli"
"github.com/docker/docker/integration-cli/cli/build"
"github.com/docker/docker/integration-cli/daemon"
"github.com/docker/docker/internal/testutils/specialimage"
"github.com/docker/docker/libnetwork/resolvconf"
"github.com/docker/docker/pkg/stringid"
"github.com/docker/docker/runconfig"
@ -1933,7 +1934,8 @@ func (s *DockerCLIRunSuite) TestRunCidFileCleanupIfEmpty(c *testing.T) {
tmpCidFile := path.Join(tmpDir, "cid")
// This must be an image that has no CMD or ENTRYPOINT set
image := "emptyfs"
image := loadSpecialImage(c, specialimage.EmptyFS)
out, _, err := dockerCmdWithError("run", "--cidfile", tmpCidFile, image)
if err == nil {
c.Fatalf("Run without command must fail. out=%s", out)

View file

@ -13,6 +13,7 @@ import (
"github.com/docker/docker/api/types"
"github.com/docker/docker/integration-cli/cli"
"github.com/docker/docker/integration-cli/cli/build"
"github.com/docker/docker/internal/testutils/specialimage"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/icmd"
@ -111,8 +112,11 @@ func (s *DockerCLISaveLoadSuite) TestSaveSingleTag(c *testing.T) {
func (s *DockerCLISaveLoadSuite) TestSaveImageId(c *testing.T) {
testRequires(c, DaemonIsLinux)
emptyFSImage := loadSpecialImage(c, specialimage.EmptyFS)
imgRepoName := "foobar-save-image-id-test"
cli.DockerCmd(c, "tag", "emptyfs:latest", fmt.Sprintf("%v:latest", imgRepoName))
cli.DockerCmd(c, "tag", emptyFSImage, fmt.Sprintf("%v:latest", imgRepoName))
out := cli.DockerCmd(c, "images", "-q", "--no-trunc", imgRepoName).Stdout()
cleanedLongImageID := strings.TrimPrefix(strings.TrimSpace(out), "sha256:")
@ -203,13 +207,16 @@ func (s *DockerCLISaveLoadSuite) TestSaveWithNoExistImage(c *testing.T) {
func (s *DockerCLISaveLoadSuite) TestSaveMultipleNames(c *testing.T) {
testRequires(c, DaemonIsLinux)
emptyFSImage := loadSpecialImage(c, specialimage.EmptyFS)
const imgRepoName = "foobar-save-multi-name-test"
oneTag := fmt.Sprintf("%v-one:latest", imgRepoName)
twoTag := fmt.Sprintf("%v-two:latest", imgRepoName)
cli.DockerCmd(c, "tag", "emptyfs:latest", oneTag)
cli.DockerCmd(c, "tag", "emptyfs:latest", twoTag)
cli.DockerCmd(c, "tag", emptyFSImage, oneTag)
cli.DockerCmd(c, "tag", emptyFSImage, twoTag)
out, err := RunCommandPipelineWithOutput(
exec.Command(dockerBinary, "save", strings.TrimSuffix(oneTag, ":latest"), twoTag),

View file

@ -18,6 +18,8 @@ import (
"github.com/docker/docker/client"
"github.com/docker/docker/integration-cli/cli"
"github.com/docker/docker/integration-cli/daemon"
"github.com/docker/docker/internal/testutils/specialimage"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/testutil"
"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"
@ -464,3 +466,43 @@ func sumAsIntegers(vals ...interface{}) interface{} {
}
return s
}
func loadSpecialImage(c *testing.T, imageFunc specialimage.SpecialImageFunc) string {
tmpDir := c.TempDir()
imgDir := filepath.Join(tmpDir, "image")
assert.NilError(c, os.Mkdir(imgDir, 0o755))
assert.NilError(c, imageFunc(imgDir))
rc, err := archive.TarWithOptions(imgDir, &archive.TarOptions{})
assert.NilError(c, err)
defer rc.Close()
imgTar := filepath.Join(tmpDir, "image.tar")
tarFile, err := os.OpenFile(imgTar, os.O_CREATE|os.O_WRONLY, 0o644)
assert.NilError(c, err)
defer tarFile.Close()
_, err = io.Copy(tarFile, rc)
assert.NilError(c, err)
tarFile.Close()
out := cli.DockerCmd(c, "load", "-i", imgTar).Stdout()
for _, line := range strings.Split(out, "\n") {
line := strings.TrimSpace(line)
if _, imageID, hasID := strings.Cut(line, "Loaded image ID: "); hasID {
return imageID
}
if _, imageRef, hasRef := strings.Cut(line, "Loaded image: "); hasRef {
return imageRef
}
}
c.Fatalf("failed to extract image ref from %q", out)
return ""
}

View file

@ -4,7 +4,7 @@ import (
"encoding/json"
"testing"
"github.com/docker/docker/testutil/environment"
"github.com/docker/docker/internal/testutils/specialimage"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/skip"
@ -17,7 +17,7 @@ func TestImageInspectEmptyTagsAndDigests(t *testing.T) {
client := testEnv.APIClient()
danglingID := environment.GetTestDanglingImageId(testEnv)
danglingID := specialimage.Load(ctx, t, client, specialimage.Dangling)
inspect, raw, err := client.ImageInspectWithRaw(ctx, danglingID)
assert.NilError(t, err)

View file

@ -5,7 +5,7 @@ import (
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/integration/internal/container"
"github.com/docker/docker/testutil/environment"
"github.com/docker/docker/internal/testutils/specialimage"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/skip"
@ -18,7 +18,7 @@ func TestPruneDontDeleteUsedDangling(t *testing.T) {
ctx := setupTest(t)
client := testEnv.APIClient()
danglingID := environment.GetTestDanglingImageId(testEnv)
danglingID := specialimage.Load(ctx, t, client, specialimage.Dangling)
container.Create(ctx, t, client,
container.WithImage(danglingID),

View file

@ -0,0 +1,41 @@
package specialimage
import (
"os"
"path/filepath"
"strings"
)
const danglingImageManifestDigest = "sha256:16d365089e5c10e1673ee82ab5bba38ade9b763296ad918bd24b42a1156c5456" // #nosec G101 -- ignoring: Potential hardcoded credentials (gosec)
const danglingImageConfigDigest = "sha256:0df1207206e5288f4a989a2f13d1f5b3c4e70467702c1d5d21dfc9f002b7bd43" // #nosec G101 -- ignoring: Potential hardcoded credentials (gosec)
// Dangling creates an image with no layers and no tag.
// It also has an extra org.mobyproject.test.specialimage=1 label set.
// Layout: OCI.
func Dangling(dir string) error {
if err := os.WriteFile(filepath.Join(dir, "index.json"), []byte(`{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"sha256:16d365089e5c10e1673ee82ab5bba38ade9b763296ad918bd24b42a1156c5456","size":264,"annotations":{"org.opencontainers.image.created":"2023-05-19T08:00:44Z"},"platform":{"architecture":"amd64","os":"linux"}}]}`), 0o644); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(`[{"Config":"blobs/sha256/0df1207206e5288f4a989a2f13d1f5b3c4e70467702c1d5d21dfc9f002b7bd43","RepoTags":null,"Layers":null}]`), 0o644); err != nil {
return err
}
if err := os.Mkdir(filepath.Join(dir, "blobs"), 0o755); err != nil {
return err
}
blobsDir := filepath.Join(dir, "blobs", "sha256")
if err := os.Mkdir(blobsDir, 0o755); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(blobsDir, strings.TrimPrefix(danglingImageManifestDigest, "sha256:")), []byte(`{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:0df1207206e5288f4a989a2f13d1f5b3c4e70467702c1d5d21dfc9f002b7bd43","size":390},"layers":[]}`), 0o644); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(blobsDir, strings.TrimPrefix(danglingImageConfigDigest, "sha256:")), []byte(`{"architecture":"amd64","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"WorkingDir":"/","Labels":{"org.mobyproject.test.specialimage":"1"},"OnBuild":null},"created":null,"history":[{"created_by":"LABEL org.mobyproject.test.specialimage=1","comment":"buildkit.dockerfile.v0","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":null}}`), 0o644); err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,64 @@
package specialimage
import (
"io"
"os"
"path/filepath"
)
// EmptyFS builds an image with an empty rootfs.
// Layout: Legacy Docker Archive
// See https://github.com/docker/docker/pull/5262
// and also https://github.com/docker/docker/issues/4242
func EmptyFS(dir string) error {
if err := os.WriteFile(filepath.Join(dir, "manifest.json"), []byte(`[{"Config":"11f64303f0f7ffdc71f001788132bca5346831939a956e3e975c93267d89a16d.json","RepoTags":["emptyfs:latest"],"Layers":["511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158/layer.tar"]}]`), 0o644); err != nil {
return err
}
if err := os.Mkdir(filepath.Join(dir, "blobs"), 0o755); err != nil {
return err
}
blobsDir := filepath.Join(dir, "511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158")
if err := os.Mkdir(blobsDir, 0o755); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(dir, "VERSION"), []byte(`1.0`), 0o644); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(dir, "repositories"), []byte(`{"emptyfs":{"latest":"511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158"}}`), 0o644); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(dir, "11f64303f0f7ffdc71f001788132bca5346831939a956e3e975c93267d89a16d.json"), []byte(`{"architecture":"x86_64","comment":"Imported from -","container_config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":null,"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"created":"2013-06-13T14:03:50.821769-07:00","docker_version":"0.4.0","history":[{"created":"2013-06-13T14:03:50.821769-07:00","comment":"Imported from -"}],"rootfs":{"type":"layers","diff_ids":["sha256:84ff92691f909a05b224e1c56abb4864f01b4f8e3c854e4bb4c7baf1d3f6d652"]}}`), 0o644); err != nil {
return err
}
if err := os.WriteFile(filepath.Join(blobsDir, "json"), []byte(`{"id":"511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158","comment":"Imported from -","created":"2013-06-13T14:03:50.821769-07:00","container_config":{"Hostname":"","Domainname":"","User":"","Memory":0,"MemorySwap":0,"CpuShares":0,"AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"PortSpecs":null,"ExposedPorts":null,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":null,"Cmd":null,"Image":"","Volumes":null,"WorkingDir":"","Entrypoint":null,"NetworkDisabled":false,"OnBuild":null},"docker_version":"0.4.0","architecture":"x86_64","Size":0}`+"\n"), 0o644); err != nil {
return err
}
layerFile, err := os.OpenFile(filepath.Join(blobsDir, "layer.tar"), os.O_CREATE|os.O_WRONLY, 0o644)
if err != nil {
return err
}
defer layerFile.Close()
// 10240 NUL bytes is a valid empty tar archive.
_, err = io.Copy(layerFile, io.LimitReader(zeroReader{}, 10240))
if err != nil {
return err
}
return nil
}
type zeroReader struct{}
func (_ zeroReader) Read(p []byte) (n int, err error) {
l := len(p)
for idx := 0; idx < l; idx++ {
p[idx] = 0
}
return l, nil
}

View file

@ -0,0 +1,66 @@
package specialimage
import (
"context"
"encoding/json"
"errors"
"io"
"strings"
"testing"
"github.com/docker/docker/client"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/jsonmessage"
"gotest.tools/v3/assert"
)
type SpecialImageFunc func(string) error
func Load(ctx context.Context, t *testing.T, apiClient client.APIClient, imageFunc SpecialImageFunc) string {
tempDir := t.TempDir()
err := imageFunc(tempDir)
assert.NilError(t, err)
rc, err := archive.TarWithOptions(tempDir, &archive.TarOptions{})
assert.NilError(t, err)
defer rc.Close()
resp, err := apiClient.ImageLoad(ctx, rc, true)
assert.NilError(t, err, "Failed to load dangling image")
defer resp.Body.Close()
if !assert.Check(t, err) {
respBody, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("Failed to read response body: %v", err)
return ""
}
t.Fatalf("Failed load: %s", string(respBody))
}
decoder := json.NewDecoder(resp.Body)
for {
var msg jsonmessage.JSONMessage
err := decoder.Decode(&msg)
if errors.Is(err, io.EOF) {
break
} else {
assert.NilError(t, err)
}
msg.Stream = strings.TrimSpace(msg.Stream)
if _, imageID, hasID := strings.Cut(msg.Stream, "Loaded image ID: "); hasID {
return imageID
}
if _, imageRef, hasRef := strings.Cut(msg.Stream, "Loaded image: "); hasRef {
return imageRef
}
}
t.Fatal("failed to read image ID")
return ""
}

View file

@ -101,7 +101,6 @@ func ProtectImages(ctx context.Context, t testing.TB, testEnv *Execution) {
images = append(images, frozenImages...)
}
testEnv.ProtectImage(t, images...)
testEnv.ProtectImage(t, DanglingImageIdGraphDriver, DanglingImageIdSnapshotter)
}
func getExistingImages(ctx context.Context, t testing.TB, testEnv *Execution) []string {

View file

@ -1,18 +0,0 @@
package environment
// DanglingImageIdGraphDriver is the digest for dangling images used
// in tests when the graph driver is used. The graph driver image store
// identifies images by the ID of their config.
const DanglingImageIdGraphDriver = "sha256:0df1207206e5288f4a989a2f13d1f5b3c4e70467702c1d5d21dfc9f002b7bd43" // #nosec G101 -- ignoring: Potential hardcoded credentials (gosec)
// DanglingImageIdSnapshotter is the digest for dangling images used in
// tests when the containerd image store is used. The container image
// store identifies images by the ID of their manifest/manifest list..
const DanglingImageIdSnapshotter = "sha256:16d365089e5c10e1673ee82ab5bba38ade9b763296ad918bd24b42a1156c5456" // #nosec G101 -- ignoring: Potential hardcoded credentials (gosec)
func GetTestDanglingImageId(testEnv *Execution) string {
if testEnv.UsingSnapshotter() {
return DanglingImageIdSnapshotter
}
return DanglingImageIdGraphDriver
}