mirror of
https://github.com/moby/buildkit.git
synced 2026-08-05 15:20:22 +00:00
1827 lines
50 KiB
Go
1827 lines
50 KiB
Go
package client
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
ctd "github.com/containerd/containerd/v2/client"
|
|
"github.com/containerd/containerd/v2/core/images"
|
|
"github.com/containerd/containerd/v2/pkg/namespaces"
|
|
cerrdefs "github.com/containerd/errdefs"
|
|
cacheimporttypes "github.com/moby/buildkit/cache/remotecache/v1/types"
|
|
"github.com/moby/buildkit/client/llb"
|
|
"github.com/moby/buildkit/exporter/containerimage/exptypes"
|
|
"github.com/moby/buildkit/identity"
|
|
"github.com/moby/buildkit/util/testutil"
|
|
"github.com/moby/buildkit/util/testutil/helpers"
|
|
"github.com/moby/buildkit/util/testutil/integration"
|
|
"github.com/moby/buildkit/util/testutil/workers"
|
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/pkg/errors"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tonistiigi/fsutil"
|
|
)
|
|
|
|
func testBasicAzblobCacheImportExport(t *testing.T, sb integration.Sandbox) {
|
|
integration.SkipOnPlatform(t, "windows")
|
|
workers.CheckFeatureCompat(t, sb,
|
|
workers.FeatureCacheExport,
|
|
workers.FeatureCacheImport,
|
|
workers.FeatureCacheBackendAzblob,
|
|
)
|
|
|
|
opts := helpers.AzuriteOpts{
|
|
AccountName: "azblobcacheaccount",
|
|
AccountKey: base64.StdEncoding.EncodeToString([]byte("azblobcacheaccountkey")),
|
|
}
|
|
|
|
azAddr, cleanup, err := helpers.NewAzuriteServer(t, sb, opts)
|
|
require.NoError(t, err)
|
|
defer cleanup()
|
|
|
|
im := CacheOptionsEntry{
|
|
Type: "azblob",
|
|
Attrs: map[string]string{
|
|
"account_url": azAddr,
|
|
"account_name": opts.AccountName,
|
|
"secret_access_key": opts.AccountKey,
|
|
"container": "cachecontainer",
|
|
},
|
|
}
|
|
ex := CacheOptionsEntry{
|
|
Type: "azblob",
|
|
Attrs: map[string]string{
|
|
"account_url": azAddr,
|
|
"account_name": opts.AccountName,
|
|
"secret_access_key": opts.AccountKey,
|
|
"container": "cachecontainer",
|
|
},
|
|
}
|
|
testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex})
|
|
}
|
|
|
|
func testBasicCacheImportExport(t *testing.T, sb integration.Sandbox, cacheOptionsEntryImport, cacheOptionsEntryExport []CacheOptionsEntry) {
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
imgName := integration.UnixOrWindows("busybox:latest", "nanoserver:latest")
|
|
busybox := llb.Image(imgName)
|
|
st := integration.UnixOrWindows(llb.Scratch(), llb.Image(imgName))
|
|
|
|
run := func(cmd string) {
|
|
baseState := integration.UnixOrWindows(busybox, st)
|
|
st = baseState.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st)
|
|
}
|
|
|
|
run(integration.UnixOrWindows(
|
|
`sh -c "echo -n foobar > const"`,
|
|
`cmd /C echo foobar > const`,
|
|
))
|
|
run(integration.UnixOrWindows(
|
|
`sh -c "cat /dev/urandom | head -c 100 | sha256sum > unique"`,
|
|
`cmd /C echo %RANDOM% > unique`,
|
|
))
|
|
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
destDir := t.TempDir()
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterLocal,
|
|
OutputDir: destDir,
|
|
},
|
|
},
|
|
CacheExports: cacheOptionsEntryExport,
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt, err := os.ReadFile(filepath.Join(destDir, "const"))
|
|
require.NoError(t, err)
|
|
newLine := integration.UnixOrWindows("", " \r\n")
|
|
require.Equal(t, "foobar"+newLine, string(dt))
|
|
|
|
dt, err = os.ReadFile(filepath.Join(destDir, "unique"))
|
|
require.NoError(t, err)
|
|
|
|
ensurePruneAll(t, c, sb)
|
|
|
|
destDir = t.TempDir()
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterLocal,
|
|
OutputDir: destDir,
|
|
},
|
|
},
|
|
CacheImports: cacheOptionsEntryImport,
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt2, err := os.ReadFile(filepath.Join(destDir, "const"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "foobar"+newLine, string(dt2))
|
|
|
|
dt2, err = os.ReadFile(filepath.Join(destDir, "unique"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, string(dt), string(dt2))
|
|
}
|
|
|
|
func testBasicInlineCacheImportExport(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb,
|
|
workers.FeatureDirectPush,
|
|
workers.FeatureCacheExport,
|
|
workers.FeatureCacheBackendInline,
|
|
)
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
imgName := integration.UnixOrWindows("busybox:latest", "nanoserver:latest")
|
|
busybox := llb.Image(imgName)
|
|
st := integration.UnixOrWindows(llb.Scratch(), llb.Image(imgName))
|
|
|
|
run := func(cmd string) {
|
|
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st)
|
|
}
|
|
|
|
run(integration.UnixOrWindows(
|
|
`sh -c "echo -n foobar > const"`,
|
|
`cmd /C echo foobar > const`,
|
|
))
|
|
run(integration.UnixOrWindows(
|
|
`sh -c "cat /dev/urandom | head -c 100 | sha256sum > unique"`,
|
|
`cmd /C echo %RANDOM% > unique`,
|
|
))
|
|
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
target := registry + "/buildkit/testexportinline:latest"
|
|
|
|
resp, err := c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
"push": "true",
|
|
},
|
|
},
|
|
},
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "inline",
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dgst, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey]
|
|
require.Equal(t, true, ok)
|
|
|
|
unique, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst, "/unique")
|
|
require.NoError(t, err)
|
|
|
|
ensurePruneAll(t, c, sb)
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureCacheImport, workers.FeatureCacheBackendRegistry)
|
|
|
|
resp, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
// specifying inline cache exporter is needed for reproducing containerimage.digest
|
|
// (not needed for reproducing rootfs/unique)
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
"push": "true",
|
|
},
|
|
},
|
|
},
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "inline",
|
|
},
|
|
},
|
|
CacheImports: []CacheOptionsEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": target,
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dgst2, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey]
|
|
require.Equal(t, true, ok)
|
|
|
|
require.Equal(t, dgst, dgst2)
|
|
|
|
ensurePruneAll(t, c, sb)
|
|
|
|
// Export the cache again with compression
|
|
resp, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
// specifying inline cache exporter is needed for reproducing containerimage.digest
|
|
// (not needed for reproducing rootfs/unique)
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
"push": "true",
|
|
"compression": "uncompressed", // inline cache should work with compression
|
|
"force-compression": "true",
|
|
},
|
|
},
|
|
},
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "inline",
|
|
},
|
|
},
|
|
CacheImports: []CacheOptionsEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": target,
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dgst2uncompress, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey]
|
|
require.Equal(t, true, ok)
|
|
|
|
// dgst2uncompress != dgst, because the compression type is different
|
|
unique2uncompress, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst2uncompress, "/unique")
|
|
require.NoError(t, err)
|
|
require.Equal(t, unique, unique2uncompress)
|
|
|
|
ensurePruneAll(t, c, sb)
|
|
|
|
resp, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
"push": "true",
|
|
},
|
|
},
|
|
},
|
|
CacheImports: []CacheOptionsEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": target,
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dgst3, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey]
|
|
require.Equal(t, true, ok)
|
|
|
|
// dgst3 != dgst, because inline cache is not exported for dgst3
|
|
unique3, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst3, "/unique")
|
|
require.NoError(t, err)
|
|
require.Equal(t, unique, unique3)
|
|
}
|
|
|
|
func testBasicLocalCacheImportExport(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb,
|
|
workers.FeatureCacheExport,
|
|
workers.FeatureCacheImport,
|
|
workers.FeatureCacheBackendLocal,
|
|
)
|
|
dir := t.TempDir()
|
|
im := CacheOptionsEntry{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"src": dir,
|
|
},
|
|
}
|
|
ex := CacheOptionsEntry{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"dest": dir,
|
|
},
|
|
}
|
|
testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex})
|
|
}
|
|
|
|
func testBasicRegistryCacheImportExport(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb,
|
|
workers.FeatureCacheExport,
|
|
workers.FeatureCacheImport,
|
|
workers.FeatureCacheBackendRegistry,
|
|
)
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
target := registry + "/buildkit/testexport:latest"
|
|
o := CacheOptionsEntry{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": target,
|
|
},
|
|
}
|
|
testBasicCacheImportExport(t, sb, []CacheOptionsEntry{o}, []CacheOptionsEntry{o})
|
|
}
|
|
|
|
func testBasicS3CacheImportExport(t *testing.T, sb integration.Sandbox) {
|
|
integration.SkipOnPlatform(t, "windows")
|
|
workers.CheckFeatureCompat(t, sb,
|
|
workers.FeatureCacheExport,
|
|
workers.FeatureCacheImport,
|
|
workers.FeatureCacheBackendS3,
|
|
)
|
|
|
|
opts := helpers.MinioOpts{
|
|
Region: "us-east-1",
|
|
AccessKeyID: "minioadmin",
|
|
SecretAccessKey: "minioadmin",
|
|
}
|
|
|
|
var putRequests atomic.Int64
|
|
opts.RequestVerifier = func(r *http.Request) error {
|
|
if r.Method != http.MethodPut {
|
|
return nil
|
|
}
|
|
putRequests.Add(1)
|
|
if got := r.Header.Get("X-Amz-Sdk-Checksum-Algorithm"); got != "" {
|
|
return errors.Errorf("unexpected checksum algorithm header %q", got)
|
|
}
|
|
if got := r.Header.Get("X-Amz-Trailer"); got != "" {
|
|
return errors.Errorf("unexpected checksum trailer header %q", got)
|
|
}
|
|
if got := r.Header.Get("Content-Encoding"); strings.Contains(strings.ToLower(got), "aws-chunked") {
|
|
return errors.Errorf("unexpected aws-chunked content encoding %q", got)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
s3Addr, s3Bucket, cleanup, err := helpers.NewMinioServer(t, sb, opts)
|
|
require.NoError(t, err)
|
|
defer cleanup()
|
|
|
|
im := CacheOptionsEntry{
|
|
Type: "s3",
|
|
Attrs: map[string]string{
|
|
"region": opts.Region,
|
|
"access_key_id": opts.AccessKeyID,
|
|
"secret_access_key": opts.SecretAccessKey,
|
|
"bucket": s3Bucket,
|
|
"endpoint_url": s3Addr,
|
|
"use_path_style": "true",
|
|
},
|
|
}
|
|
ex := CacheOptionsEntry{
|
|
Type: "s3",
|
|
Attrs: map[string]string{
|
|
"region": opts.Region,
|
|
"access_key_id": opts.AccessKeyID,
|
|
"secret_access_key": opts.SecretAccessKey,
|
|
"bucket": s3Bucket,
|
|
"endpoint_url": s3Addr,
|
|
"use_path_style": "true",
|
|
},
|
|
}
|
|
testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex})
|
|
require.NotZero(t, putRequests.Load())
|
|
}
|
|
|
|
func testCacheExportCacheDeletedContent(t *testing.T, sb integration.Sandbox) {
|
|
integration.SkipOnPlatform(t, "windows")
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureCacheExport, workers.FeatureCacheBackendLocal)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
tmpdir := integration.Tmpdir(t)
|
|
|
|
err = os.WriteFile(filepath.Join(tmpdir.Name, "foo"), []byte("foodata"), 0600)
|
|
require.NoError(t, err)
|
|
|
|
base := llb.Image("alpine:latest").Run(llb.Shlex(`sh -c "echo abc-def > /foo && echo abc > /detection-file"`)).Root()
|
|
st := llb.Scratch().File(llb.Copy(base, "/foo", "/out"))
|
|
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"dest": filepath.Join(tmpdir.Name, "cache"),
|
|
"mode": "max",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt, err := os.ReadFile(filepath.Join(tmpdir.Name, "cache", "index.json"))
|
|
require.NoError(t, err)
|
|
|
|
var index ocispecs.Index
|
|
err = json.Unmarshal(dt, &index)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, index.Manifests, 1)
|
|
|
|
dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache/blobs/sha256", index.Manifests[0].Digest.Hex()))
|
|
require.NoError(t, err)
|
|
|
|
var mfst ocispecs.Manifest
|
|
err = json.Unmarshal(dt, &mfst)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, mfst.Layers, 3)
|
|
require.Equal(t, "application/vnd.buildkit.cacheconfig.v0", mfst.Config.MediaType)
|
|
|
|
dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache/blobs/sha256", mfst.Config.Digest.Hex()))
|
|
require.NoError(t, err)
|
|
|
|
var cc cacheimporttypes.CacheConfig
|
|
err = json.Unmarshal(dt, &cc)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 3, len(cc.Layers))
|
|
require.Equal(t, 5, len(cc.Records))
|
|
|
|
var runLayer *int
|
|
for i, l := range cc.Layers {
|
|
if l.ParentIndex != -1 {
|
|
if runLayer != nil {
|
|
t.Fatal("multiple RUN layers")
|
|
}
|
|
runLayer = &i
|
|
}
|
|
}
|
|
require.NotNil(t, runLayer)
|
|
|
|
dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache/blobs/sha256", cc.Layers[*runLayer].Blob.Hex()))
|
|
require.NoError(t, err)
|
|
|
|
m, err := testutil.ReadTarToMap(dt, true)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "abc\n", string(m["detection-file"].Data))
|
|
|
|
// delete the blob for the run layer
|
|
err = os.Remove(filepath.Join(tmpdir.Name, "cache/blobs/sha256", cc.Layers[*runLayer].Blob.Hex()))
|
|
require.NoError(t, err)
|
|
|
|
// prune all buildkit state
|
|
ensurePruneAll(t, c, sb)
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
CacheImports: []CacheOptionsEntry{
|
|
{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"src": filepath.Join(tmpdir.Name, "cache"),
|
|
},
|
|
},
|
|
},
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"dest": filepath.Join(tmpdir.Name, "cache2"),
|
|
"mode": "max",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache2", "index.json"))
|
|
require.NoError(t, err)
|
|
|
|
var index2 ocispecs.Index
|
|
err = json.Unmarshal(dt, &index2)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, index2.Manifests, 1)
|
|
|
|
dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache2/blobs/sha256", index2.Manifests[0].Digest.Hex()))
|
|
require.NoError(t, err)
|
|
|
|
var mfst2 ocispecs.Manifest
|
|
err = json.Unmarshal(dt, &mfst2)
|
|
require.NoError(t, err)
|
|
|
|
require.Len(t, mfst2.Layers, 1)
|
|
require.Equal(t, "application/vnd.buildkit.cacheconfig.v0", mfst2.Config.MediaType)
|
|
|
|
dt, err = os.ReadFile(filepath.Join(tmpdir.Name, "cache2/blobs/sha256", mfst2.Config.Digest.Hex()))
|
|
require.NoError(t, err)
|
|
|
|
var cc2 cacheimporttypes.CacheConfig
|
|
err = json.Unmarshal(dt, &cc2)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 1, len(cc2.Layers))
|
|
require.Equal(t, 5, len(cc2.Records))
|
|
|
|
for i, r := range cc.Records {
|
|
require.Equal(t, cc2.Records[i].Digest, r.Digest)
|
|
require.Equal(t, cc2.Records[i].Inputs, r.Inputs)
|
|
}
|
|
}
|
|
|
|
// moby/buildkit#1336
|
|
func testCacheExportCacheKeyLoop(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureCacheExport, workers.FeatureCacheBackendLocal)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
tmpdir := integration.Tmpdir(t)
|
|
|
|
err = os.WriteFile(filepath.Join(tmpdir.Name, "foo"), []byte("foodata"), 0600)
|
|
require.NoError(t, err)
|
|
|
|
imgName := integration.UnixOrWindows("alpine:latest", "nanoserver:latest")
|
|
scratch := integration.UnixOrWindows(
|
|
llb.Scratch(),
|
|
llb.Image(imgName),
|
|
)
|
|
cmdStr := integration.UnixOrWindows("true", "cmd /C exit 0")
|
|
for _, mode := range []bool{false, true} {
|
|
func(mode bool) {
|
|
t.Run(fmt.Sprintf("mode=%v", mode), func(t *testing.T) {
|
|
buildbase := llb.Image(imgName).File(llb.Copy(llb.Local("mylocal"), "foo", "foo"))
|
|
if mode { // same cache keys with a separating node go to different code-path
|
|
buildbase = buildbase.Run(llb.Shlex(cmdStr)).Root()
|
|
}
|
|
intermed := llb.Image(imgName).File(llb.Copy(buildbase, "foo", "foo"))
|
|
final := scratch.File(llb.Copy(intermed, "foo", "foooooo"))
|
|
|
|
def, err := final.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"dest": filepath.Join(tmpdir.Name, "cache"),
|
|
},
|
|
},
|
|
},
|
|
LocalMounts: map[string]fsutil.FS{
|
|
"mylocal": tmpdir,
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
})
|
|
}(mode)
|
|
}
|
|
}
|
|
|
|
func testCacheExportIgnoreError(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureCacheExport)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
imgName := integration.UnixOrWindows("busybox:latest", "nanoserver:latest")
|
|
busybox := llb.Image(imgName)
|
|
cmd := integration.UnixOrWindows(
|
|
`sh -e -c "echo -n ignore-error > data"`,
|
|
"cmd /C echo ignore-error> data",
|
|
)
|
|
|
|
st := integration.UnixOrWindows(llb.Scratch(), llb.Image(imgName))
|
|
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st)
|
|
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
tests := map[string]struct {
|
|
Exports []ExportEntry
|
|
CacheExports []CacheOptionsEntry
|
|
expectedErrors []string
|
|
}{
|
|
"local-ignore-error": {
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterLocal,
|
|
OutputDir: t.TempDir(),
|
|
},
|
|
},
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"dest": "éèç",
|
|
},
|
|
},
|
|
},
|
|
expectedErrors: []string{"failed to solve", "contains value with non-printable ASCII characters"},
|
|
},
|
|
"registry-ignore-error": {
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": "test-registry-ignore-error",
|
|
"push": "false",
|
|
},
|
|
},
|
|
},
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": "fake-url:5000/myrepo:buildcache",
|
|
},
|
|
},
|
|
},
|
|
expectedErrors: []string{"failed to solve", "dial tcp: lookup fake-url", "no such host"},
|
|
},
|
|
"s3-ignore-error": {
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterLocal,
|
|
OutputDir: t.TempDir(),
|
|
},
|
|
},
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "s3",
|
|
Attrs: map[string]string{
|
|
"endpoint_url": "http://fake-url:9000",
|
|
"bucket": "my-bucket",
|
|
"region": "us-east-1",
|
|
"access_key_id": "minioadmin",
|
|
"secret_access_key": "minioadmin",
|
|
"use_path_style": "true",
|
|
},
|
|
},
|
|
},
|
|
expectedErrors: []string{"failed to solve", "dial tcp: lookup fake-url", "no such host"},
|
|
},
|
|
}
|
|
ignoreErrorValues := []bool{true, false}
|
|
for _, ignoreError := range ignoreErrorValues {
|
|
ignoreErrStr := strconv.FormatBool(ignoreError)
|
|
for n, test := range tests {
|
|
require.Equal(t, 1, len(test.Exports))
|
|
require.Equal(t, 1, len(test.CacheExports))
|
|
require.NotEmpty(t, test.CacheExports[0].Attrs)
|
|
test.CacheExports[0].Attrs["ignore-error"] = ignoreErrStr
|
|
testName := fmt.Sprintf("%s-%s", n, ignoreErrStr)
|
|
t.Run(testName, func(t *testing.T) {
|
|
switch n {
|
|
case "local-ignore-error":
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureCacheBackendLocal)
|
|
case "registry-ignore-error":
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureCacheBackendRegistry)
|
|
case "s3-ignore-error":
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureCacheBackendS3)
|
|
}
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: test.Exports,
|
|
CacheExports: test.CacheExports,
|
|
}, nil)
|
|
if ignoreError {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
for _, errStr := range test.expectedErrors {
|
|
require.Contains(t, err.Error(), errStr)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func testImageManifestRegistryCacheImportExport(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb,
|
|
workers.FeatureCacheExport,
|
|
workers.FeatureCacheImport,
|
|
workers.FeatureCacheBackendRegistry,
|
|
)
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
target := registry + "/buildkit/testexport:latest"
|
|
im := CacheOptionsEntry{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": target,
|
|
},
|
|
}
|
|
ex := CacheOptionsEntry{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": target,
|
|
"oci-mediatypes": "true",
|
|
"mode": "max",
|
|
},
|
|
}
|
|
testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex})
|
|
}
|
|
|
|
func testLocalCacheExportReset(t *testing.T, sb integration.Sandbox) {
|
|
integration.SkipOnPlatform(t, "windows")
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureCacheExport, workers.FeatureCacheBackendLocal)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
tmpdir := integration.Tmpdir(t)
|
|
cacheDir := filepath.Join(tmpdir.Name, "cache")
|
|
|
|
// Build 1: export cache for a simple build
|
|
st1 := llb.Image("alpine:latest").Run(llb.Shlex(`sh -c "echo build1 > /out"`)).Root()
|
|
def1, err := llb.Scratch().File(llb.Copy(st1, "/out", "/out")).Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.Solve(sb.Context(), def1, SolveOpt{
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"dest": cacheDir,
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Verify blobs were written
|
|
blobDir := filepath.Join(cacheDir, "blobs", "sha256")
|
|
entries1, err := os.ReadDir(blobDir)
|
|
require.NoError(t, err)
|
|
require.Greater(t, len(entries1), 0)
|
|
|
|
// Build 2: different build, export with reset=true
|
|
st2 := llb.Image("alpine:latest").Run(llb.Shlex(`sh -c "echo build2-different > /out2"`)).Root()
|
|
def2, err := llb.Scratch().File(llb.Copy(st2, "/out2", "/out2")).Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.Solve(sb.Context(), def2, SolveOpt{
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"dest": cacheDir,
|
|
"reset": "true",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Read the manifest to determine which blobs should be referenced
|
|
dt, err := os.ReadFile(filepath.Join(cacheDir, "index.json"))
|
|
require.NoError(t, err)
|
|
|
|
var index ocispecs.Index
|
|
err = json.Unmarshal(dt, &index)
|
|
require.NoError(t, err)
|
|
require.Len(t, index.Manifests, 1)
|
|
|
|
dt, err = os.ReadFile(filepath.Join(blobDir, index.Manifests[0].Digest.Hex()))
|
|
require.NoError(t, err)
|
|
|
|
var mfst ocispecs.Manifest
|
|
err = json.Unmarshal(dt, &mfst)
|
|
require.NoError(t, err)
|
|
|
|
// Build the set of expected blobs: manifest + config + all layers
|
|
expectedBlobs := map[string]struct{}{
|
|
index.Manifests[0].Digest.Hex(): {},
|
|
mfst.Config.Digest.Hex(): {},
|
|
}
|
|
for _, l := range mfst.Layers {
|
|
expectedBlobs[l.Digest.Hex()] = struct{}{}
|
|
}
|
|
|
|
// Verify only referenced blobs remain
|
|
entries2, err := os.ReadDir(blobDir)
|
|
require.NoError(t, err)
|
|
|
|
actualBlobs := map[string]struct{}{}
|
|
for _, e := range entries2 {
|
|
actualBlobs[e.Name()] = struct{}{}
|
|
}
|
|
require.Equal(t, expectedBlobs, actualBlobs, "after reset, only blobs referenced by the current manifest should remain")
|
|
}
|
|
|
|
func testMultipleCacheExports(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureMultiCacheExport)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
imgName := integration.UnixOrWindows("busybox:latest", "nanoserver:latest")
|
|
busybox := llb.Image(imgName)
|
|
st := integration.UnixOrWindows(llb.Scratch(), llb.Image(imgName))
|
|
|
|
run := func(cmd string) {
|
|
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st)
|
|
}
|
|
|
|
constCmd := integration.UnixOrWindows(
|
|
`sh -c "echo -n foobar > const"`,
|
|
`cmd /C echo foobar > const`,
|
|
)
|
|
uniqueCmd := integration.UnixOrWindows(
|
|
`sh -c "cat /dev/urandom | head -c 100 | sha256sum > unique"`,
|
|
`cmd /C echo %RANDOM% > unique`,
|
|
)
|
|
|
|
run(constCmd)
|
|
run(uniqueCmd)
|
|
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
target := path.Join(registry, "image:test")
|
|
target2 := path.Join(registry, "image-copy:test")
|
|
cacheRef := path.Join(registry, "cache:test")
|
|
cacheOutDir, cacheOutDir2 := t.TempDir(), t.TempDir()
|
|
|
|
res, err := c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
"push": "true",
|
|
},
|
|
},
|
|
},
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"dest": cacheOutDir,
|
|
},
|
|
},
|
|
{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"dest": cacheOutDir2,
|
|
},
|
|
},
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": cacheRef,
|
|
},
|
|
},
|
|
{
|
|
Type: "inline",
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
ensureFile(t, filepath.Join(cacheOutDir, ocispecs.ImageIndexFile))
|
|
ensureFile(t, filepath.Join(cacheOutDir2, ocispecs.ImageIndexFile))
|
|
|
|
dgst := res.ExporterResponse[exptypes.ExporterImageDigestKey]
|
|
|
|
uniqueFile, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst, "/unique")
|
|
require.NoError(t, err)
|
|
|
|
res, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target2,
|
|
"push": "true",
|
|
},
|
|
},
|
|
},
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "inline",
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dgst2 := res.ExporterResponse[exptypes.ExporterImageDigestKey]
|
|
require.Equal(t, dgst, dgst2)
|
|
|
|
destDir := t.TempDir()
|
|
ensurePruneAll(t, c, sb)
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterLocal,
|
|
OutputDir: destDir,
|
|
},
|
|
},
|
|
CacheImports: []CacheOptionsEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": cacheRef,
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
ensureFileContents(t, filepath.Join(destDir, "const"), integration.UnixOrWindows("foobar", "foobar \r\n"))
|
|
ensureFileContents(t, filepath.Join(destDir, "unique"), string(uniqueFile))
|
|
}
|
|
|
|
func testMultipleRecordsWithSameLayersCacheImportExport(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb,
|
|
workers.FeatureCacheExport,
|
|
workers.FeatureCacheImport,
|
|
workers.FeatureCacheBackendRegistry,
|
|
)
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
target := registry + "/buildkit/testexport:latest"
|
|
cacheOpts := []CacheOptionsEntry{{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": target,
|
|
},
|
|
}}
|
|
|
|
requiresLinux(t)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
base := llb.Image("busybox:latest")
|
|
// layerA and layerB create identical layers with different LLB
|
|
layerA := base.Run(llb.Args([]string{
|
|
"sh", "-c",
|
|
`echo $(( 1 + 2 )) > /result && touch -d "1970-01-01 00:00:00" /result`,
|
|
})).Root()
|
|
layerB := base.Run(llb.Args([]string{
|
|
"sh", "-c",
|
|
`echo $(( 2 + 1 )) > /result && touch -d "1970-01-01 00:00:00" /result`,
|
|
})).Root()
|
|
|
|
combined := llb.Merge([]llb.State{layerA, layerB})
|
|
combinedDef, err := combined.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.Solve(sb.Context(), combinedDef, SolveOpt{
|
|
CacheExports: cacheOpts,
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
ensurePruneAll(t, c, sb)
|
|
|
|
singleDef, err := layerA.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.Solve(sb.Context(), singleDef, SolveOpt{
|
|
CacheImports: cacheOpts,
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Ensure that even though layerA and layerB were both loaded as possible results
|
|
// and only was used, all the cache refs are released
|
|
// More context: https://github.com/moby/buildkit/pull/3815
|
|
ensurePruneAll(t, c, sb)
|
|
}
|
|
|
|
func testMultipleRegistryCacheImportExport(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb,
|
|
workers.FeatureCacheExport,
|
|
workers.FeatureCacheImport,
|
|
workers.FeatureCacheBackendRegistry,
|
|
)
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
target := registry + "/buildkit/testexport:latest"
|
|
o := CacheOptionsEntry{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": target,
|
|
},
|
|
}
|
|
o2 := CacheOptionsEntry{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": target + "notexist",
|
|
},
|
|
}
|
|
testBasicCacheImportExport(t, sb, []CacheOptionsEntry{o, o2}, []CacheOptionsEntry{o})
|
|
}
|
|
|
|
func testRegistryEmptyCacheExport(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb,
|
|
workers.FeatureCacheExport,
|
|
workers.FeatureCacheBackendRegistry,
|
|
)
|
|
|
|
for _, ociMediaTypes := range []bool{true, false} {
|
|
for _, imageManifest := range []bool{true, false} {
|
|
if imageManifest && !ociMediaTypes {
|
|
// invalid configuration for remote cache
|
|
continue
|
|
}
|
|
|
|
t.Run(t.Name()+fmt.Sprintf("/ociMediaTypes=%t/imageManifest=%t", ociMediaTypes, imageManifest), func(t *testing.T) {
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
st := llb.Scratch()
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
cacheTarget := registry + "/buildkit/testregistryemptycache:latest"
|
|
|
|
cacheOptionsEntry := CacheOptionsEntry{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": cacheTarget,
|
|
"image-manifest": strconv.FormatBool(imageManifest),
|
|
"oci-mediatypes": strconv.FormatBool(ociMediaTypes),
|
|
},
|
|
}
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
CacheExports: []CacheOptionsEntry{cacheOptionsEntry},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
ctx := namespaces.WithNamespace(sb.Context(), "buildkit")
|
|
cdAddress := sb.ContainerdAddress()
|
|
var client *ctd.Client
|
|
if cdAddress != "" {
|
|
client, err = newContainerd(cdAddress)
|
|
require.NoError(t, err)
|
|
defer client.Close()
|
|
|
|
_, err := client.Fetch(ctx, cacheTarget)
|
|
require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// testSnapshotWithMultipleBlobs verifies that BuildKit handles a single snapshot with multiple
|
|
// compressed blob representations. It pushes the same layer with compression levels 0 and 9,
|
|
// producing different blob digests, then pulls and exports both images to ensure no conflicts.
|
|
// moby/buildkit#3809
|
|
func testSnapshotWithMultipleBlobs(t *testing.T, sb integration.Sandbox) {
|
|
// TODO: Re-enable on Windows once the HCS layer activation race is fixed upstream.
|
|
// Passes locally but fails on GitHub Actions with: mkdir ...\ProgramData\Microsoft:
|
|
// Cannot create a file when that file already exists. The bug is in the Windows
|
|
// snapshotter/differ layer activation path, not in this test.
|
|
integration.SkipOnPlatform(t, "windows", "HCS layer activation race: concurrent mkdir fails on CI (passes locally)")
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
// build two images with same layer but different compressed blobs
|
|
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
now := time.Now()
|
|
|
|
imgName := integration.UnixOrWindows("alpine", "nanoserver")
|
|
// On Windows:
|
|
// - llb.Scratch() doesn't work (Windows snapshotter requires a base OS layer)
|
|
// - llb.Copy across images doesn't work (dual mount: "number of mounts should always be 1")
|
|
// So we use llb.Image(nanoserver).File(llb.Mkfile(...)) which only needs one mount.
|
|
// The nanoserver base layers appear before our layer, so the final assertion
|
|
// compares a layer index that skips the base layers.
|
|
st := integration.UnixOrWindows(
|
|
llb.Scratch().File(llb.Copy(llb.Image(imgName), "/", "/"+imgName+"/", llb.WithCreatedTime(now))),
|
|
llb.Image(imgName).File(llb.Mkfile("/testdata", 0600, []byte("snapshot-test-content"), llb.WithCreatedTime(now))),
|
|
)
|
|
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
name1 := registry + "/multiblobtest1/image:latest"
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: "image",
|
|
Attrs: map[string]string{
|
|
"name": name1,
|
|
"push": "true",
|
|
"compression-level": "0",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
ensurePruneAll(t, c, sb)
|
|
|
|
st = st.File(llb.Mkfile("test", 0600, []byte("test"))) // extra layer so we don't get a cache match based on image config rootfs only
|
|
|
|
def, err = st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
name2 := registry + "/multiblobtest2/image:latest"
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: "image",
|
|
Attrs: map[string]string{
|
|
"name": name2,
|
|
"push": "true",
|
|
"compression-level": "9",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
ensurePruneAll(t, c, sb)
|
|
|
|
// first build with first image
|
|
destDir := t.TempDir()
|
|
|
|
out1 := filepath.Join(destDir, "out1.tar")
|
|
outW1, err := os.Create(out1)
|
|
require.NoError(t, err)
|
|
|
|
st = llb.Image(name1).File(llb.Mkfile("test", 0600, []byte("test1")))
|
|
|
|
def, err = st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterOCI, // make sure to export so blobs need to be loaded
|
|
Output: fixedWriteCloser(outW1),
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
// make sure second image does not cause any errors
|
|
out2 := filepath.Join(destDir, "out2.tar")
|
|
outW2, err := os.Create(out2)
|
|
require.NoError(t, err)
|
|
|
|
st = llb.Image(name2).File(llb.Mkfile("test", 0600, []byte("test2")))
|
|
|
|
def, err = st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
FrontendAttrs: integration.UnixOrWindows(
|
|
map[string]string{"attest:sbom": ""},
|
|
map[string]string{},
|
|
),
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterOCI, // make sure to export so blobs need to be loaded
|
|
Output: fixedWriteCloser(outW2),
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
// extra validation that we did get different layer blobs
|
|
dt, err := os.ReadFile(out1)
|
|
require.NoError(t, err)
|
|
|
|
m, err := testutil.ReadTarToMap(dt, false)
|
|
require.NoError(t, err)
|
|
|
|
var index ocispecs.Index
|
|
err = json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &index)
|
|
require.NoError(t, err)
|
|
|
|
var mfst ocispecs.Manifest
|
|
err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+index.Manifests[0].Digest.Hex()].Data, &mfst)
|
|
require.NoError(t, err)
|
|
|
|
dt, err = os.ReadFile(out2)
|
|
require.NoError(t, err)
|
|
|
|
m, err = testutil.ReadTarToMap(dt, false)
|
|
require.NoError(t, err)
|
|
|
|
err = json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &index)
|
|
require.NoError(t, err)
|
|
|
|
err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+index.Manifests[0].Digest.Hex()].Data, &index)
|
|
require.NoError(t, err)
|
|
|
|
var mfst2 ocispecs.Manifest
|
|
err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+index.Manifests[0].Digest.Hex()].Data, &mfst2)
|
|
require.NoError(t, err)
|
|
|
|
// On Linux, Layers[0] is the layer we created (copy from alpine).
|
|
// On Windows, the nanoserver base layer(s) come first, so our layer is at a later index.
|
|
// We find the first layer that differs between the two manifests — that's the layer
|
|
// pushed with different compression levels.
|
|
layerIdx := integration.UnixOrWindows(0, len(mfst.Layers)-2)
|
|
require.NotEqual(t, mfst.Layers[layerIdx].Digest, mfst2.Layers[layerIdx].Digest)
|
|
}
|
|
|
|
func testUncompressedLocalCacheImportExport(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb,
|
|
workers.FeatureCacheExport,
|
|
workers.FeatureCacheImport,
|
|
workers.FeatureCacheBackendLocal,
|
|
)
|
|
dir := t.TempDir()
|
|
im := CacheOptionsEntry{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"src": dir,
|
|
},
|
|
}
|
|
ex := CacheOptionsEntry{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"dest": dir,
|
|
"compression": "uncompressed",
|
|
"force-compression": "true",
|
|
},
|
|
}
|
|
testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex})
|
|
}
|
|
|
|
func testUncompressedRegistryCacheImportExport(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb,
|
|
workers.FeatureCacheExport,
|
|
workers.FeatureCacheImport,
|
|
workers.FeatureCacheBackendRegistry,
|
|
)
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
target := registry + "/buildkit/testexport:latest"
|
|
im := CacheOptionsEntry{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": target,
|
|
},
|
|
}
|
|
ex := CacheOptionsEntry{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": target,
|
|
"compression": "uncompressed",
|
|
"force-compression": "true",
|
|
},
|
|
}
|
|
testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex})
|
|
}
|
|
|
|
// TODO: Re-enable the skipped test on Windows (see issue #6296)
|
|
func testZstdLocalCacheExport(t *testing.T, sb integration.Sandbox) {
|
|
integration.SkipOnPlatform(t, "windows", "This test passed locally on windows, but failed on github action")
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureCacheExport, workers.FeatureCacheBackendLocal)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
imgName := integration.UnixOrWindows("busybox:latest", "nanoserver:latest")
|
|
cmdStr := integration.UnixOrWindows(
|
|
`sh -e -c "echo -n zstd > data"`,
|
|
`cmd /C "echo zstd > C:\data"`,
|
|
)
|
|
|
|
st := integration.UnixOrWindows(llb.Scratch(), llb.Image(imgName))
|
|
st = llb.Image(imgName).Run(llb.Shlex(cmdStr), llb.Dir("/wd")).AddMount("/wd", st)
|
|
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
destDir := t.TempDir()
|
|
destOutDir := t.TempDir()
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterLocal,
|
|
OutputDir: destOutDir,
|
|
},
|
|
},
|
|
// compression option should work even with inline cache exports
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"dest": destDir,
|
|
"image-manifest": "false",
|
|
"compression": "zstd",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
var index ocispecs.Index
|
|
dt, err := os.ReadFile(filepath.Join(destDir, ocispecs.ImageIndexFile))
|
|
require.NoError(t, err)
|
|
err = json.Unmarshal(dt, &index)
|
|
require.NoError(t, err)
|
|
|
|
var layerIndex ocispecs.Index
|
|
dt, err = os.ReadFile(filepath.Join(destDir, ocispecs.ImageBlobsDir+"/sha256/"+index.Manifests[0].Digest.Hex()))
|
|
require.NoError(t, err)
|
|
err = json.Unmarshal(dt, &layerIndex)
|
|
require.NoError(t, err)
|
|
|
|
lastLayer := layerIndex.Manifests[len(layerIndex.Manifests)-2]
|
|
require.Equal(t, ocispecs.MediaTypeImageLayerZstd, lastLayer.MediaType)
|
|
|
|
zstdLayerDigest := lastLayer.Digest.Hex()
|
|
dt, err = os.ReadFile(filepath.Join(destDir, ocispecs.ImageBlobsDir+"/sha256/"+zstdLayerDigest))
|
|
require.NoError(t, err)
|
|
require.Equal(t, []byte{0x28, 0xb5, 0x2f, 0xfd}, dt[:4])
|
|
}
|
|
|
|
func testZstdLocalCacheImportExport(t *testing.T, sb integration.Sandbox) {
|
|
integration.SkipOnPlatform(t, "windows")
|
|
|
|
workers.CheckFeatureCompat(t, sb,
|
|
workers.FeatureCacheExport,
|
|
workers.FeatureCacheImport,
|
|
workers.FeatureCacheBackendLocal,
|
|
)
|
|
dir := t.TempDir()
|
|
im := CacheOptionsEntry{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"src": dir,
|
|
},
|
|
}
|
|
ex := CacheOptionsEntry{
|
|
Type: "local",
|
|
Attrs: map[string]string{
|
|
"dest": dir,
|
|
"compression": "zstd",
|
|
"force-compression": "true",
|
|
"oci-mediatypes": "true", // containerd applier supports only zstd with oci-mediatype.
|
|
},
|
|
}
|
|
testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex})
|
|
}
|
|
|
|
// TODO: Re-enable the skipped test on Windows (see issue #6296)
|
|
func testZstdRegistryCacheImportExport(t *testing.T, sb integration.Sandbox) {
|
|
integration.SkipOnPlatform(t, "windows", "This test passed locally on windows, but failed on github action")
|
|
|
|
workers.CheckFeatureCompat(t, sb,
|
|
workers.FeatureCacheExport,
|
|
workers.FeatureCacheImport,
|
|
workers.FeatureCacheBackendRegistry,
|
|
)
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
target := registry + "/buildkit/testexport:latest"
|
|
im := CacheOptionsEntry{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": target,
|
|
},
|
|
}
|
|
ex := CacheOptionsEntry{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": target,
|
|
"compression": "zstd",
|
|
"force-compression": "true",
|
|
"image-manifest": "false",
|
|
"oci-mediatypes": "true", // containerd applier supports only zstd with oci-mediatype.
|
|
},
|
|
}
|
|
testBasicCacheImportExport(t, sb, []CacheOptionsEntry{im}, []CacheOptionsEntry{ex})
|
|
}
|
|
|
|
func testStargzLazyInlineCacheImportExport(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb,
|
|
workers.FeatureCacheExport,
|
|
workers.FeatureCacheImport,
|
|
workers.FeatureCacheBackendInline,
|
|
workers.FeatureCacheBackendRegistry,
|
|
)
|
|
requiresLinux(t)
|
|
cdAddress := sb.ContainerdAddress()
|
|
if cdAddress == "" || sb.Snapshotter() != "stargz" {
|
|
t.Skip("test requires containerd worker with stargz snapshotter")
|
|
}
|
|
|
|
client, err := newContainerd(cdAddress)
|
|
require.NoError(t, err)
|
|
defer client.Close()
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
var (
|
|
imageService = client.ImageService()
|
|
contentStore = client.ContentStore()
|
|
ctx = namespaces.WithNamespace(sb.Context(), "buildkit")
|
|
)
|
|
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
// Prepare stargz inline cache
|
|
orgImage := "docker.io/library/alpine:latest"
|
|
sgzImage := registry + "/stargz/alpine:" + identity.NewID()
|
|
baseDef := llb.Image(orgImage).Run(llb.Args([]string{"/bin/touch", "/foo"}))
|
|
def, err := baseDef.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": sgzImage,
|
|
"push": "true",
|
|
"compression": "estargz",
|
|
"oci-mediatypes": "true",
|
|
"force-compression": "true",
|
|
},
|
|
},
|
|
},
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "inline",
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
// clear all local state out
|
|
err = imageService.Delete(ctx, sgzImage, images.SynchronousDelete())
|
|
require.NoError(t, err)
|
|
checkAllReleasable(t, c, sb, true)
|
|
|
|
// stargz layers should be lazy even for executing something on them
|
|
def, err = baseDef.
|
|
Run(llb.Args([]string{"/bin/touch", "/bar"})).
|
|
Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
target := registry + "/buildkit/testlazyimage:" + identity.NewID()
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
"push": "true",
|
|
"oci-mediatypes": "true",
|
|
"compression": "estargz",
|
|
"store": "true",
|
|
"unsafe-internal-store-allow-incomplete": "true",
|
|
},
|
|
},
|
|
},
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "inline",
|
|
},
|
|
},
|
|
CacheImports: []CacheOptionsEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": sgzImage,
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
img, err := imageService.Get(ctx, target)
|
|
require.NoError(t, err)
|
|
|
|
manifest, err := images.Manifest(ctx, contentStore, img.Target, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Check if image layers are lazy.
|
|
// The topmost(last) layer created by `Run` isn't lazy so we skip the check for the layer.
|
|
var sgzLayers []ocispecs.Descriptor
|
|
for i, layer := range manifest.Layers[:len(manifest.Layers)-1] {
|
|
_, err = contentStore.Info(ctx, layer.Digest)
|
|
require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v on layer %+v (%d)", err, layer, i)
|
|
sgzLayers = append(sgzLayers, layer)
|
|
}
|
|
require.NotEqual(t, 0, len(sgzLayers), "no layer can be used for checking lazypull")
|
|
|
|
// The topmost(last) layer created by `Run` shouldn't be lazy
|
|
_, err = contentStore.Info(ctx, manifest.Layers[len(manifest.Layers)-1].Digest)
|
|
require.NoError(t, err)
|
|
|
|
// clear all local state out
|
|
err = imageService.Delete(ctx, img.Name, images.SynchronousDelete())
|
|
require.NoError(t, err)
|
|
checkAllReleasable(t, c, sb, true)
|
|
|
|
// stargz layers should be exportable
|
|
destDir := t.TempDir()
|
|
out := filepath.Join(destDir, "out.tar")
|
|
outW, err := os.Create(out)
|
|
require.NoError(t, err)
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterOCI,
|
|
Output: fixedWriteCloser(outW),
|
|
},
|
|
},
|
|
CacheImports: []CacheOptionsEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": sgzImage,
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Check if image layers are un-lazied
|
|
for _, layer := range sgzLayers {
|
|
_, err = contentStore.Info(ctx, layer.Digest)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
ensurePruneAll(t, c, sb)
|
|
}
|
|
|
|
func testStargzLazyRegistryCacheImportExport(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb,
|
|
workers.FeatureCacheExport,
|
|
workers.FeatureCacheBackendRegistry,
|
|
workers.FeatureOCIExporter,
|
|
)
|
|
requiresLinux(t)
|
|
cdAddress := sb.ContainerdAddress()
|
|
if cdAddress == "" || sb.Snapshotter() != "stargz" {
|
|
t.Skip("test requires containerd worker with stargz snapshotter")
|
|
}
|
|
|
|
client, err := newContainerd(cdAddress)
|
|
require.NoError(t, err)
|
|
defer client.Close()
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
var (
|
|
imageService = client.ImageService()
|
|
contentStore = client.ContentStore()
|
|
ctx = namespaces.WithNamespace(sb.Context(), "buildkit")
|
|
)
|
|
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
destDir := t.TempDir()
|
|
|
|
// Prepare stargz registry cache
|
|
orgImage := "docker.io/library/alpine:latest"
|
|
sgzCache := registry + "/stargz/alpinecache:" + identity.NewID()
|
|
baseDef := llb.Image(orgImage).Run(llb.Args([]string{"/bin/touch", "/foo"}))
|
|
def, err := baseDef.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
out := filepath.Join(destDir, "out.tar")
|
|
outW, err := os.Create(out)
|
|
require.NoError(t, err)
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterOCI,
|
|
Output: fixedWriteCloser(outW),
|
|
},
|
|
},
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": sgzCache,
|
|
"compression": "estargz",
|
|
"oci-mediatypes": "true",
|
|
"force-compression": "true",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
// clear all local state out
|
|
ensurePruneAll(t, c, sb)
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureCacheImport, workers.FeatureDirectPush)
|
|
|
|
// stargz layers should be lazy even for executing something on them
|
|
def, err = baseDef.
|
|
Run(llb.Args([]string{"sh", "-c", "cat /dev/urandom | head -c 100 | sha256sum > unique"})).
|
|
Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
target := registry + "/buildkit/testlazyimage:" + identity.NewID()
|
|
resp, err := c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
"push": "true",
|
|
"store": "true",
|
|
"oci-mediatypes": "true",
|
|
"unsafe-internal-store-allow-incomplete": "true",
|
|
},
|
|
},
|
|
},
|
|
CacheImports: []CacheOptionsEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": sgzCache,
|
|
},
|
|
},
|
|
},
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": sgzCache,
|
|
"compression": "estargz",
|
|
"oci-mediatypes": "true",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dgst, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey]
|
|
require.Equal(t, true, ok)
|
|
|
|
unique, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst, "/unique")
|
|
require.NoError(t, err)
|
|
|
|
img, err := imageService.Get(ctx, target)
|
|
require.NoError(t, err)
|
|
|
|
manifest, err := images.Manifest(ctx, contentStore, img.Target, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Check if image layers are lazy.
|
|
// The topmost(last) layer created by `Run` isn't lazy so we skip the check for the layer.
|
|
var sgzLayers []ocispecs.Descriptor
|
|
for i, layer := range manifest.Layers[:len(manifest.Layers)-1] {
|
|
_, err = contentStore.Info(ctx, layer.Digest)
|
|
require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v on layer %+v (%d)", err, layer, i)
|
|
sgzLayers = append(sgzLayers, layer)
|
|
}
|
|
require.NotEqual(t, 0, len(sgzLayers), "no layer can be used for checking lazypull")
|
|
|
|
// The topmost(last) layer created by `Run` shouldn't be lazy
|
|
_, err = contentStore.Info(ctx, manifest.Layers[len(manifest.Layers)-1].Digest)
|
|
require.NoError(t, err)
|
|
|
|
// Run build again and check if cache is reused
|
|
resp, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
"push": "true",
|
|
"store": "true",
|
|
"oci-mediatypes": "true",
|
|
"unsafe-internal-store-allow-incomplete": "true",
|
|
},
|
|
},
|
|
},
|
|
CacheImports: []CacheOptionsEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": sgzCache,
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dgst2, ok := resp.ExporterResponse[exptypes.ExporterImageDigestKey]
|
|
require.Equal(t, true, ok)
|
|
|
|
unique2, err := readFileInImage(sb.Context(), t, c, target+"@"+dgst2, "/unique")
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, dgst, dgst2)
|
|
require.Equal(t, unique, unique2)
|
|
|
|
// clear all local state out
|
|
err = imageService.Delete(ctx, img.Name, images.SynchronousDelete())
|
|
require.NoError(t, err)
|
|
checkAllReleasable(t, c, sb, true)
|
|
|
|
// stargz layers should be exportable
|
|
out = filepath.Join(destDir, "out2.tar")
|
|
outW, err = os.Create(out)
|
|
require.NoError(t, err)
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterOCI,
|
|
Output: fixedWriteCloser(outW),
|
|
},
|
|
},
|
|
CacheImports: []CacheOptionsEntry{
|
|
{
|
|
Type: "registry",
|
|
Attrs: map[string]string{
|
|
"ref": sgzCache,
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
// Check if image layers are un-lazied
|
|
for _, layer := range sgzLayers {
|
|
_, err = contentStore.Info(ctx, layer.Digest)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
ensurePruneAll(t, c, sb)
|
|
}
|