mirror of
https://github.com/moby/buildkit.git
synced 2026-08-05 07:10:23 +00:00
Move client integration test declarations out of the large harness files into focused files for cache, exporters, sources, mounts, gateway behavior, and related client features. Keep registration lists grouped by source file, move non-gateway tests out of the gateway harness, and remove the now-empty build_test.go. This is intended as a mechanical split with no test logic changes. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2006 lines
56 KiB
Go
2006 lines
56 KiB
Go
package client
|
|
|
|
import (
|
|
"archive/tar"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
ctd "github.com/containerd/containerd/v2/client"
|
|
"github.com/containerd/containerd/v2/core/content"
|
|
"github.com/containerd/containerd/v2/core/content/proxy"
|
|
"github.com/containerd/containerd/v2/core/images"
|
|
"github.com/containerd/containerd/v2/core/remotes/docker"
|
|
"github.com/containerd/containerd/v2/pkg/namespaces"
|
|
"github.com/containerd/containerd/v2/plugins/content/local"
|
|
cerrdefs "github.com/containerd/errdefs"
|
|
"github.com/containerd/platforms"
|
|
controlapi "github.com/moby/buildkit/api/services/control"
|
|
"github.com/moby/buildkit/client/llb"
|
|
"github.com/moby/buildkit/client/llb/sourceresolver"
|
|
"github.com/moby/buildkit/exporter/containerimage/exptypes"
|
|
gateway "github.com/moby/buildkit/frontend/gateway/client"
|
|
"github.com/moby/buildkit/identity"
|
|
"github.com/moby/buildkit/solver/pb"
|
|
"github.com/moby/buildkit/util/contentutil"
|
|
"github.com/moby/buildkit/util/testutil"
|
|
"github.com/moby/buildkit/util/testutil/integration"
|
|
"github.com/moby/buildkit/util/testutil/workers"
|
|
digest "github.com/opencontainers/go-digest"
|
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/pkg/errors"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
func testBuildExportScratch(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureImageExporter)
|
|
requiresLinux(t)
|
|
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)
|
|
|
|
makeFrontend := func(ps []string) func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
|
return func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
|
st := llb.Scratch()
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
r, err := c.Solve(ctx, gateway.SolveRequest{
|
|
Definition: def.ToPB(),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ref, err := r.SingleRef()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
res := gateway.NewResult()
|
|
if ps == nil {
|
|
res.SetRef(ref)
|
|
} else {
|
|
for _, p := range ps {
|
|
res.AddRef(p, ref)
|
|
}
|
|
|
|
expPlatforms := &exptypes.Platforms{
|
|
Platforms: make([]exptypes.Platform, len(ps)),
|
|
}
|
|
for i, pk := range ps {
|
|
p := platforms.MustParse(pk)
|
|
|
|
img := ocispecs.Image{
|
|
Platform: p,
|
|
Config: ocispecs.ImageConfig{
|
|
Labels: map[string]string{
|
|
"foo": "i am platform " + platforms.Format(p),
|
|
},
|
|
},
|
|
}
|
|
config, err := json.Marshal(img)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to marshal image config")
|
|
}
|
|
res.AddMeta(fmt.Sprintf("%s/%s", exptypes.ExporterImageConfigKey, pk), config)
|
|
|
|
expPlatforms.Platforms[i] = exptypes.Platform{
|
|
ID: pk,
|
|
Platform: p,
|
|
}
|
|
}
|
|
dt, err := json.Marshal(expPlatforms)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res.AddMeta(exptypes.ExporterPlatformsKey, dt)
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
}
|
|
|
|
target := registry + "/buildkit/build/exporter:withnocompressed"
|
|
|
|
_, err = c.Build(sb.Context(), SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
"push": "true",
|
|
"unpack": "true",
|
|
"compression": "uncompressed",
|
|
"attest:provenance": "mode=max",
|
|
},
|
|
},
|
|
},
|
|
}, "", makeFrontend(nil), nil)
|
|
require.NoError(t, err)
|
|
|
|
targetMulti := registry + "/buildkit/build/exporter-multi:withnocompressed"
|
|
_, err = c.Build(sb.Context(), SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": targetMulti,
|
|
"push": "true",
|
|
"unpack": "true",
|
|
"compression": "uncompressed",
|
|
"attest:provenance": "mode=max",
|
|
},
|
|
},
|
|
},
|
|
}, "", makeFrontend([]string{"linux/amd64", "linux/arm64"}), nil)
|
|
require.NoError(t, err)
|
|
|
|
desc, provider, err := contentutil.ProviderFromRef(target)
|
|
require.NoError(t, err)
|
|
imgs, err := testutil.ReadImages(sb.Context(), provider, desc)
|
|
require.NoError(t, err)
|
|
require.Len(t, imgs.Images, 1)
|
|
img := imgs.Images[0]
|
|
require.Empty(t, img.Layers)
|
|
require.True(t, platforms.Only(platforms.DefaultSpec()).Match(img.Img.Platform))
|
|
|
|
desc, provider, err = contentutil.ProviderFromRef(targetMulti)
|
|
require.NoError(t, err)
|
|
imgs, err = testutil.ReadImages(sb.Context(), provider, desc)
|
|
require.NoError(t, err)
|
|
require.Len(t, imgs.Images, 2)
|
|
img = imgs.Find("linux/amd64")
|
|
require.Empty(t, img.Layers)
|
|
require.Equal(t, "linux/amd64", platforms.Format(img.Img.Platform))
|
|
require.Equal(t, "i am platform linux/amd64", img.Img.Config.Labels["foo"])
|
|
img = imgs.Find("linux/arm64")
|
|
require.Empty(t, img.Layers)
|
|
require.Equal(t, "linux/arm64", platforms.Format(img.Img.Platform))
|
|
require.Equal(t, "i am platform linux/arm64", img.Img.Config.Labels["foo"])
|
|
}
|
|
|
|
// testBuildExportWithForeignLayer verifies foreign (non-distributable) layer handling during
|
|
// image export. propagate=1 preserves foreign media type and URLs; propagate=0 converts to
|
|
// regular layers. Skipped on Windows: the test image is Linux-only and BuildKit's Windows
|
|
// exporter does not preserve foreign layer media types.
|
|
func testBuildExportWithForeignLayer(t *testing.T, sb integration.Sandbox) {
|
|
integration.SkipOnPlatform(t, "windows", "test image is Linux-only and BuildKit's Windows exporter strips foreign layer media types")
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureImageExporter)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
st := llb.Image("cpuguy83/buildkit-foreign:latest")
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
t.Run("propagate=1", func(t *testing.T) {
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
target := registry + "/buildkit/build/exporter/foreign:latest"
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
"push": "true",
|
|
"oci-mediatypes": "false",
|
|
"prefer-nondist-layers": "true",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
ctx := namespaces.WithNamespace(sb.Context(), "buildkit")
|
|
|
|
resolver := docker.NewResolver(docker.ResolverOptions{PlainHTTP: true})
|
|
name, desc, err := resolver.Resolve(ctx, target)
|
|
require.NoError(t, err)
|
|
|
|
fetcher, err := resolver.Fetcher(ctx, name)
|
|
require.NoError(t, err)
|
|
mfst, err := images.Manifest(ctx, contentutil.FromFetcher(fetcher), desc, platforms.Any())
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, len(mfst.Layers))
|
|
require.Equal(t, images.MediaTypeDockerSchema2LayerForeign, mfst.Layers[0].MediaType)
|
|
require.Len(t, mfst.Layers[0].URLs, 1)
|
|
require.Equal(t, images.MediaTypeDockerSchema2Layer, mfst.Layers[1].MediaType)
|
|
|
|
rc, err := fetcher.Fetch(ctx, ocispecs.Descriptor{Digest: mfst.Layers[0].Digest, Size: mfst.Layers[0].Size})
|
|
require.NoError(t, err)
|
|
defer rc.Close()
|
|
|
|
// `Fetch` doesn't error (in the docker resolver), it just returns a reader immediately and does not make a request.
|
|
// The request is only made when we attempt to read from the reader.
|
|
buf := make([]byte, 1)
|
|
_, err = rc.Read(buf)
|
|
require.Truef(t, cerrdefs.IsNotFound(err), "expected error for blob that should not be in registry: %s, %v", mfst.Layers[0].Digest, err)
|
|
})
|
|
t.Run("propagate=0", func(t *testing.T) {
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
target := registry + "/buildkit/build/exporter/noforeign:latest"
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
"push": "true",
|
|
"oci-mediatypes": "false",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
ctx := namespaces.WithNamespace(sb.Context(), "buildkit")
|
|
|
|
resolver := docker.NewResolver(docker.ResolverOptions{PlainHTTP: true})
|
|
name, desc, err := resolver.Resolve(ctx, target)
|
|
require.NoError(t, err)
|
|
|
|
fetcher, err := resolver.Fetcher(ctx, name)
|
|
require.NoError(t, err)
|
|
|
|
mfst, err := images.Manifest(ctx, contentutil.FromFetcher(fetcher), desc, platforms.Any())
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, len(mfst.Layers))
|
|
require.Equal(t, images.MediaTypeDockerSchema2Layer, mfst.Layers[0].MediaType)
|
|
require.Len(t, mfst.Layers[0].URLs, 0)
|
|
require.Equal(t, images.MediaTypeDockerSchema2Layer, mfst.Layers[1].MediaType)
|
|
|
|
rc, err := fetcher.Fetch(ctx, ocispecs.Descriptor{Digest: mfst.Layers[0].Digest, Size: mfst.Layers[0].Size})
|
|
require.NoError(t, err)
|
|
defer rc.Close()
|
|
|
|
// `Fetch` doesn't error (in the docker resolver), it just returns a reader immediately and does not make a request.
|
|
// The request is only made when we attempt to read from the reader.
|
|
buf := make([]byte, 1)
|
|
_, err = rc.Read(buf)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func testBuildExportWithUncompressed(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureImageExporter)
|
|
requiresLinux(t)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
busybox := llb.Image("busybox:latest")
|
|
cmd := `sh -e -c "echo -n uncompressed > data"`
|
|
|
|
st := llb.Scratch()
|
|
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st)
|
|
|
|
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)
|
|
|
|
target := registry + "/buildkit/build/exporter:withnocompressed"
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
"push": "true",
|
|
"compression": "uncompressed",
|
|
},
|
|
},
|
|
},
|
|
}, 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()
|
|
|
|
img, err := client.GetImage(ctx, target)
|
|
require.NoError(t, err)
|
|
mfst, err := images.Manifest(ctx, client.ContentStore(), img.Target(), nil)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(mfst.Layers))
|
|
require.Equal(t, ocispecs.MediaTypeImageLayer, mfst.Layers[0].MediaType)
|
|
}
|
|
|
|
// new layer with gzip compression
|
|
targetImg := llb.Image(target)
|
|
cmd = `sh -e -c "echo -n gzip > data"`
|
|
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", targetImg)
|
|
|
|
def, err = st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
compressedTarget := registry + "/buildkit/build/exporter:withcompressed"
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": compressedTarget,
|
|
"push": "true",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
allCompressedTarget := registry + "/buildkit/build/exporter:withallcompressed"
|
|
_, err = c.Solve(context.TODO(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": allCompressedTarget,
|
|
"push": "true",
|
|
"compression": "gzip",
|
|
"force-compression": "true",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
if cdAddress == "" {
|
|
t.Skip("rest of test requires containerd worker")
|
|
}
|
|
|
|
err = client.ImageService().Delete(ctx, target, images.SynchronousDelete())
|
|
require.NoError(t, err)
|
|
err = client.ImageService().Delete(ctx, compressedTarget, images.SynchronousDelete())
|
|
require.NoError(t, err)
|
|
err = client.ImageService().Delete(ctx, allCompressedTarget, images.SynchronousDelete())
|
|
require.NoError(t, err)
|
|
|
|
checkAllReleasable(t, c, sb, true)
|
|
|
|
// check if the new layer is compressed with compression option
|
|
img, err := client.Pull(ctx, compressedTarget)
|
|
require.NoError(t, err)
|
|
|
|
dt, err := content.ReadBlob(ctx, img.ContentStore(), img.Target())
|
|
require.NoError(t, err)
|
|
|
|
mfst := struct {
|
|
MediaType string `json:"mediaType,omitempty"`
|
|
ocispecs.Manifest
|
|
}{}
|
|
|
|
err = json.Unmarshal(dt, &mfst)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, len(mfst.Layers))
|
|
require.Equal(t, ocispecs.MediaTypeImageLayer, mfst.Layers[0].MediaType)
|
|
require.Equal(t, ocispecs.MediaTypeImageLayerGzip, mfst.Layers[1].MediaType)
|
|
|
|
dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[0].Digest})
|
|
require.NoError(t, err)
|
|
|
|
m, err := testutil.ReadTarToMap(dt, false)
|
|
require.NoError(t, err)
|
|
|
|
item, ok := m["data"]
|
|
require.True(t, ok)
|
|
require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag))
|
|
require.Equal(t, []byte("uncompressed"), item.Data)
|
|
|
|
dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[1].Digest})
|
|
require.NoError(t, err)
|
|
|
|
m, err = testutil.ReadTarToMap(dt, true)
|
|
require.NoError(t, err)
|
|
|
|
item, ok = m["data"]
|
|
require.True(t, ok)
|
|
require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag))
|
|
require.Equal(t, []byte("gzip"), item.Data)
|
|
|
|
err = client.ImageService().Delete(ctx, compressedTarget, images.SynchronousDelete())
|
|
require.NoError(t, err)
|
|
|
|
checkAllReleasable(t, c, sb, true)
|
|
|
|
// check if all layers are compressed with force-compressoin option
|
|
img, err = client.Pull(ctx, allCompressedTarget)
|
|
require.NoError(t, err)
|
|
|
|
dt, err = content.ReadBlob(ctx, img.ContentStore(), img.Target())
|
|
require.NoError(t, err)
|
|
|
|
mfst = struct {
|
|
MediaType string `json:"mediaType,omitempty"`
|
|
ocispecs.Manifest
|
|
}{}
|
|
|
|
err = json.Unmarshal(dt, &mfst)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, len(mfst.Layers))
|
|
require.Equal(t, ocispecs.MediaTypeImageLayerGzip, mfst.Layers[0].MediaType)
|
|
require.Equal(t, ocispecs.MediaTypeImageLayerGzip, mfst.Layers[1].MediaType)
|
|
|
|
dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[0].Digest})
|
|
require.NoError(t, err)
|
|
|
|
m, err = testutil.ReadTarToMap(dt, true)
|
|
require.NoError(t, err)
|
|
|
|
item, ok = m["data"]
|
|
require.True(t, ok)
|
|
require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag))
|
|
require.Equal(t, []byte("uncompressed"), item.Data)
|
|
|
|
dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[1].Digest})
|
|
require.NoError(t, err)
|
|
|
|
m, err = testutil.ReadTarToMap(dt, true)
|
|
require.NoError(t, err)
|
|
|
|
item, ok = m["data"]
|
|
require.True(t, ok)
|
|
require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag))
|
|
require.Equal(t, []byte("gzip"), item.Data)
|
|
}
|
|
|
|
// testBuildExportZstd verifies OCI export with Zstd-compressed layers.
|
|
// It builds a simple layer, exports it with Zstd compression, and validates both
|
|
// OCI and Docker schema 2 media types.
|
|
func testBuildExportZstd(t *testing.T, sb integration.Sandbox) {
|
|
// Skipped on Windows:
|
|
// 1. AddMount() fails because Windows snapshots allow only a single mount per layer.
|
|
// 2. OCI export fails because windowsLcowDiff lacks the Compare method needed to
|
|
// generate compressed layer diffs.
|
|
integration.SkipOnPlatform(t, "windows", "Windows container support incomplete: AddMount() fails with 'number of mounts should always be 1 for Windows layers', OCI export fails with 'windowsLcowDiff does not implement Compare method'")
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
busybox := llb.Image("busybox:latest")
|
|
cmd := `sh -e -c "echo -n zstd > data"`
|
|
|
|
st := llb.Scratch()
|
|
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st)
|
|
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
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),
|
|
Attrs: map[string]string{
|
|
"compression": "zstd",
|
|
},
|
|
},
|
|
},
|
|
// compression option should work even with inline cache exports
|
|
CacheExports: []CacheOptionsEntry{
|
|
{
|
|
Type: "inline",
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt, err := os.ReadFile(out)
|
|
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)
|
|
|
|
lastLayer := mfst.Layers[len(mfst.Layers)-1]
|
|
require.Equal(t, ocispecs.MediaTypeImageLayerZstd, lastLayer.MediaType)
|
|
|
|
zstdLayerDigest := lastLayer.Digest.Hex()
|
|
require.Equal(t, []byte{0x28, 0xb5, 0x2f, 0xfd}, m[ocispecs.ImageBlobsDir+"/sha256/"+zstdLayerDigest].Data[:4])
|
|
|
|
// repeat without oci mediatype
|
|
outW, err = os.Create(out)
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterOCI,
|
|
Output: fixedWriteCloser(outW),
|
|
Attrs: map[string]string{
|
|
"compression": "zstd",
|
|
"oci-mediatypes": "false",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt, err = os.ReadFile(out)
|
|
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, &mfst)
|
|
require.NoError(t, err)
|
|
|
|
lastLayer = mfst.Layers[len(mfst.Layers)-1]
|
|
require.Equal(t, images.MediaTypeDockerSchema2LayerZstd, lastLayer.MediaType)
|
|
|
|
require.Equal(t, lastLayer.Digest.Hex(), zstdLayerDigest)
|
|
}
|
|
|
|
func testBuildPushAndValidate(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
|
|
requiresLinux(t)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
busybox := llb.Image("busybox:latest")
|
|
st := llb.Scratch()
|
|
|
|
run := func(cmd string) {
|
|
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st)
|
|
}
|
|
|
|
run(`sh -e -c "mkdir -p foo/sub; echo -n first > foo/sub/bar; chmod 0741 foo;"`)
|
|
run(`true`) // this doesn't create a layer
|
|
run(`sh -c "echo -n second > foo/sub/baz"`)
|
|
|
|
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)
|
|
|
|
target := registry + "/buildkit/testpush:latest"
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
"push": "true",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
// test existence of the image with next build
|
|
firstBuild := llb.Image(target)
|
|
|
|
def, err = firstBuild.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
destDir := t.TempDir()
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterLocal,
|
|
OutputDir: destDir,
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt, err := os.ReadFile(filepath.Join(destDir, "foo/sub/bar"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, dt, []byte("first"))
|
|
|
|
dt, err = os.ReadFile(filepath.Join(destDir, "foo/sub/baz"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, dt, []byte("second"))
|
|
|
|
fi, err := os.Stat(filepath.Join(destDir, "foo"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0741, int(fi.Mode()&0777))
|
|
|
|
checkAllReleasable(t, c, sb, false)
|
|
|
|
// examine contents of exported tars (requires containerd)
|
|
cdAddress := sb.ContainerdAddress()
|
|
if cdAddress == "" {
|
|
t.Skip("rest of test requires containerd worker")
|
|
}
|
|
|
|
// TODO: make public pull helper function so this can be checked for standalone as well
|
|
|
|
client, err := newContainerd(cdAddress)
|
|
require.NoError(t, err)
|
|
defer client.Close()
|
|
|
|
ctx := namespaces.WithNamespace(sb.Context(), "buildkit")
|
|
|
|
// check image in containerd
|
|
_, err = client.ImageService().Get(ctx, target)
|
|
require.NoError(t, err)
|
|
|
|
// deleting image should release all content
|
|
err = client.ImageService().Delete(ctx, target, images.SynchronousDelete())
|
|
require.NoError(t, err)
|
|
|
|
checkAllReleasable(t, c, sb, true)
|
|
|
|
img, err := client.Pull(ctx, target)
|
|
require.NoError(t, err)
|
|
|
|
desc, err := img.Config(ctx)
|
|
require.NoError(t, err)
|
|
|
|
dt, err = content.ReadBlob(ctx, img.ContentStore(), desc)
|
|
require.NoError(t, err)
|
|
|
|
var ociimg ocispecs.Image
|
|
err = json.Unmarshal(dt, &ociimg)
|
|
require.NoError(t, err)
|
|
|
|
require.NotEqual(t, "", ociimg.OS)
|
|
require.NotEqual(t, "", ociimg.Architecture)
|
|
require.NotEqual(t, "", ociimg.Config.WorkingDir)
|
|
require.Equal(t, "layers", ociimg.RootFS.Type)
|
|
require.Equal(t, 3, len(ociimg.RootFS.DiffIDs))
|
|
require.NotNil(t, ociimg.Created)
|
|
require.Less(t, time.Since(*ociimg.Created), 2*time.Minute)
|
|
require.Condition(t, func() bool {
|
|
for _, env := range ociimg.Config.Env {
|
|
if strings.HasPrefix(env, "PATH=") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
|
|
require.Equal(t, 3, len(ociimg.History))
|
|
require.Contains(t, ociimg.History[0].CreatedBy, "foo/sub/bar")
|
|
require.Contains(t, ociimg.History[1].CreatedBy, "true")
|
|
require.Contains(t, ociimg.History[2].CreatedBy, "foo/sub/baz")
|
|
require.False(t, ociimg.History[0].EmptyLayer)
|
|
require.False(t, ociimg.History[1].EmptyLayer)
|
|
require.False(t, ociimg.History[2].EmptyLayer)
|
|
|
|
dt, err = content.ReadBlob(ctx, img.ContentStore(), img.Target())
|
|
require.NoError(t, err)
|
|
|
|
mfst := struct {
|
|
MediaType string `json:"mediaType,omitempty"`
|
|
ocispecs.Manifest
|
|
}{}
|
|
|
|
err = json.Unmarshal(dt, &mfst)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, ocispecs.MediaTypeImageManifest, mfst.MediaType)
|
|
require.Equal(t, 3, len(mfst.Layers))
|
|
require.Equal(t, ocispecs.MediaTypeImageLayerGzip, mfst.Layers[0].MediaType)
|
|
require.Equal(t, ocispecs.MediaTypeImageLayerGzip, mfst.Layers[1].MediaType)
|
|
|
|
dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[0].Digest})
|
|
require.NoError(t, err)
|
|
|
|
m, err := testutil.ReadTarToMap(dt, true)
|
|
require.NoError(t, err)
|
|
|
|
item, ok := m["foo/"]
|
|
require.True(t, ok)
|
|
require.Equal(t, tar.TypeDir, int32(item.Header.Typeflag))
|
|
require.Equal(t, 0741, int(item.Header.Mode&0777))
|
|
|
|
item, ok = m["foo/sub/"]
|
|
require.True(t, ok)
|
|
require.Equal(t, tar.TypeDir, int32(item.Header.Typeflag))
|
|
|
|
item, ok = m["foo/sub/bar"]
|
|
require.True(t, ok)
|
|
require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag))
|
|
require.Equal(t, []byte("first"), item.Data)
|
|
|
|
_, ok = m["foo/sub/baz"]
|
|
require.False(t, ok)
|
|
|
|
dt, err = content.ReadBlob(ctx, img.ContentStore(), ocispecs.Descriptor{Digest: mfst.Layers[2].Digest})
|
|
require.NoError(t, err)
|
|
|
|
m, err = testutil.ReadTarToMap(dt, true)
|
|
require.NoError(t, err)
|
|
|
|
item, ok = m["foo/sub/baz"]
|
|
require.True(t, ok)
|
|
require.Equal(t, tar.TypeReg, int32(item.Header.Typeflag))
|
|
require.Equal(t, []byte("second"), item.Data)
|
|
|
|
item, ok = m["foo/"]
|
|
require.True(t, ok)
|
|
require.Equal(t, tar.TypeDir, int32(item.Header.Typeflag))
|
|
require.Equal(t, 0741, int(item.Header.Mode&0777))
|
|
|
|
item, ok = m["foo/sub/"]
|
|
require.True(t, ok)
|
|
require.Equal(t, tar.TypeDir, int32(item.Header.Typeflag))
|
|
|
|
_, ok = m["foo/sub/bar"]
|
|
require.False(t, ok)
|
|
}
|
|
|
|
// #877
|
|
func testExportBusyboxLocal(t *testing.T, sb integration.Sandbox) {
|
|
integration.SkipOnPlatform(t, "windows")
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
def, err := llb.Image("busybox").Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
destDir := t.TempDir()
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
FrontendAttrs: map[string]string{
|
|
"attest:provenance": "",
|
|
},
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterLocal,
|
|
OutputDir: destDir,
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
fi, err := os.Stat(filepath.Join(destDir, "bin/busybox"))
|
|
require.NoError(t, err)
|
|
|
|
fi2, err := os.Stat(filepath.Join(destDir, "bin/vi"))
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, os.SameFile(fi, fi2))
|
|
}
|
|
|
|
func testExportedImageLabels(t *testing.T, sb integration.Sandbox) {
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
cdAddress := sb.ContainerdAddress()
|
|
if cdAddress == "" {
|
|
t.Skip("only supported with containerd")
|
|
}
|
|
|
|
ctx := sb.Context()
|
|
|
|
imgName := integration.UnixOrWindows("busybox", "nanoserver")
|
|
prefix := integration.UnixOrWindows(
|
|
"",
|
|
"cmd /C ", // TODO(profnandaa): currently needs the shell prefix, to be fixed
|
|
)
|
|
def, err := llb.Image(imgName).
|
|
Run(llb.Shlexf(fmt.Sprintf("%secho foo > /foo", prefix))).
|
|
Marshal(ctx)
|
|
require.NoError(t, err)
|
|
|
|
target := "docker.io/buildkit/build/exporter:labels"
|
|
|
|
_, err = c.Solve(ctx, def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
client, err := newContainerd(cdAddress)
|
|
require.NoError(t, err)
|
|
defer client.Close()
|
|
|
|
ctx = namespaces.WithNamespace(ctx, "buildkit")
|
|
|
|
img, err := client.GetImage(ctx, target)
|
|
require.NoError(t, err)
|
|
|
|
store := client.ContentStore()
|
|
|
|
info, err := store.Info(ctx, img.Target().Digest)
|
|
require.NoError(t, err)
|
|
|
|
dt, err := content.ReadBlob(ctx, store, img.Target())
|
|
require.NoError(t, err)
|
|
|
|
var mfst ocispecs.Manifest
|
|
err = json.Unmarshal(dt, &mfst)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, len(mfst.Layers))
|
|
|
|
hasLabel := func(dgst digest.Digest) bool {
|
|
for k, v := range info.Labels {
|
|
if strings.HasPrefix(k, "containerd.io/gc.ref.content.") && v == dgst.String() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// check that labels are set on all layers and config
|
|
for _, l := range mfst.Layers {
|
|
require.True(t, hasLabel(l.Digest))
|
|
}
|
|
require.True(t, hasLabel(mfst.Config.Digest))
|
|
|
|
err = c.Prune(sb.Context(), nil, PruneAll)
|
|
require.NoError(t, err)
|
|
|
|
// layer should not be deleted
|
|
_, err = store.Info(ctx, mfst.Layers[1].Digest)
|
|
require.NoError(t, err)
|
|
|
|
err = client.ImageService().Delete(ctx, target, images.SynchronousDelete())
|
|
require.NoError(t, err)
|
|
|
|
// layers should be deleted
|
|
_, err = store.Info(ctx, mfst.Layers[1].Digest)
|
|
require.Error(t, err)
|
|
require.True(t, errors.Is(err, cerrdefs.ErrNotFound))
|
|
|
|
// config should be deleted
|
|
_, err = store.Info(ctx, mfst.Config.Digest)
|
|
require.Error(t, err)
|
|
require.True(t, errors.Is(err, cerrdefs.ErrNotFound))
|
|
|
|
// buildkit contentstore still has the layer because it is multi-ns
|
|
bkstore := proxy.NewContentStore(c.ContentClient())
|
|
|
|
// layer should be deleted as not kept by history
|
|
_, err = bkstore.Info(ctx, mfst.Layers[1].Digest)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "not found")
|
|
|
|
// config should still be there
|
|
_, err = bkstore.Info(ctx, img.Metadata().Target.Digest)
|
|
require.NoError(t, err)
|
|
|
|
_, err = bkstore.Info(ctx, mfst.Config.Digest)
|
|
require.NoError(t, err)
|
|
|
|
cl, err := c.ControlClient().ListenBuildHistory(sb.Context(), &controlapi.BuildHistoryRequest{
|
|
EarlyExit: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
for {
|
|
resp, err := cl.Recv()
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
require.NoError(t, err)
|
|
_, err = c.ControlClient().UpdateBuildHistory(sb.Context(), &controlapi.UpdateBuildHistoryRequest{
|
|
Ref: resp.Record.Ref,
|
|
Delete: true,
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// now everything should be deleted
|
|
_, err = bkstore.Info(ctx, img.Metadata().Target.Digest)
|
|
require.Error(t, err)
|
|
|
|
_, err = bkstore.Info(ctx, mfst.Config.Digest)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func testImageResolveConfigDefaultLocalFallback(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureImageExporter)
|
|
|
|
cdAddress := sb.ContainerdAddress()
|
|
if cdAddress == "" {
|
|
t.Skip("test requires containerd worker")
|
|
}
|
|
|
|
ctx := sb.Context()
|
|
c, err := New(ctx, sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
cdClient, err := newContainerd(cdAddress)
|
|
require.NoError(t, err)
|
|
defer cdClient.Close()
|
|
|
|
registry, err := sb.NewRegistry()
|
|
if errors.Is(err, integration.ErrRequirements) {
|
|
t.Skip(err.Error())
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
target := registry + "/buildkit/default-local-fallback:" + identity.NewID()
|
|
st := integration.UnixOrWindows(
|
|
llb.Scratch(),
|
|
llb.Image("nanoserver:latest"),
|
|
)
|
|
def, err := st.File(llb.Mkfile("/fallback", 0600, []byte("local"))).Marshal(ctx)
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.Solve(ctx, def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
"store": "true",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
imageService := cdClient.ImageService()
|
|
imageStoreCtx := namespaces.WithNamespace(ctx, "buildkit")
|
|
img, err := imageService.Get(imageStoreCtx, target)
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.Build(ctx, SolveOpt{}, "buildkit_test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
|
|
ref, dgst, config, err := c.ResolveImageConfig(ctx, target, sourceresolver.Opt{
|
|
ImageOpt: &sourceresolver.ResolveImageOpt{
|
|
ResolveMode: pb.AttrImageResolveModeDefault,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
require.Equal(t, target, ref)
|
|
require.Equal(t, img.Target.Digest, dgst)
|
|
require.NotEmpty(t, config)
|
|
|
|
var ociimg ocispecs.Image
|
|
require.NoError(t, json.Unmarshal(config, &ociimg))
|
|
require.NotEmpty(t, ociimg.OS)
|
|
require.NotEmpty(t, ociimg.Architecture)
|
|
|
|
return gateway.NewResult(), nil
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
checkAllReleasable(t, c, sb, true)
|
|
}
|
|
|
|
func testInvalidExporter(t *testing.T, sb integration.Sandbox) {
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
imgName := integration.UnixOrWindows("busybox:latest", "nanoserver:latest")
|
|
def, err := llb.Image(imgName).Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
destDir := t.TempDir()
|
|
|
|
target := "example.com/buildkit/testoci:latest"
|
|
attrs := map[string]string{
|
|
"name": target,
|
|
}
|
|
for _, exp := range []string{ExporterOCI, ExporterDocker} {
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: exp,
|
|
Attrs: attrs,
|
|
},
|
|
},
|
|
}, nil)
|
|
// output file writer is required
|
|
require.Error(t, err)
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: exp,
|
|
Attrs: attrs,
|
|
OutputDir: destDir,
|
|
},
|
|
},
|
|
}, nil)
|
|
// output directory is not supported
|
|
require.Error(t, err)
|
|
}
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterLocal,
|
|
Attrs: attrs,
|
|
},
|
|
},
|
|
}, nil)
|
|
// output directory is required
|
|
require.Error(t, err)
|
|
|
|
f, err := os.Create(filepath.Join(destDir, "a"))
|
|
require.NoError(t, err)
|
|
defer f.Close()
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterLocal,
|
|
Attrs: attrs,
|
|
Output: fixedWriteCloser(f),
|
|
},
|
|
},
|
|
}, nil)
|
|
// output file writer is not supported
|
|
require.Error(t, err)
|
|
|
|
checkAllReleasable(t, c, sb, true)
|
|
}
|
|
|
|
func testLazyImagePush(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
|
|
cdAddress := sb.ContainerdAddress()
|
|
if cdAddress == "" {
|
|
t.Skip("test requires containerd worker")
|
|
}
|
|
|
|
client, err := newContainerd(cdAddress)
|
|
require.NoError(t, err)
|
|
defer client.Close()
|
|
|
|
ctx := namespaces.WithNamespace(sb.Context(), "buildkit")
|
|
|
|
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()
|
|
|
|
// push the busybox image to the mutable registry
|
|
sourceImage := integration.UnixOrWindows("busybox:latest", "nanoserver:latest")
|
|
def, err := llb.Image(sourceImage).Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
targetNoTag := registry + "/buildkit/testlazyimage:"
|
|
target := targetNoTag + "latest"
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
"push": "true",
|
|
"store": "true",
|
|
"unsafe-internal-store-allow-incomplete": "true",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
imageService := client.ImageService()
|
|
contentStore := client.ContentStore()
|
|
|
|
img, err := imageService.Get(ctx, target)
|
|
require.NoError(t, err)
|
|
|
|
manifest, err := images.Manifest(ctx, contentStore, img.Target, nil)
|
|
require.NoError(t, err)
|
|
|
|
for _, layer := range manifest.Layers {
|
|
_, err = contentStore.Info(ctx, layer.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)
|
|
|
|
// retag the image we just pushed with no actual changes, which
|
|
// should not result in the image getting un-lazied
|
|
def, err = llb.Image(target).Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
target2 := targetNoTag + "newtag"
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target2,
|
|
"push": "true",
|
|
"store": "true",
|
|
"unsafe-internal-store-allow-incomplete": "true",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
img, err = imageService.Get(ctx, target2)
|
|
require.NoError(t, err)
|
|
|
|
manifest, err = images.Manifest(ctx, contentStore, img.Target, nil)
|
|
require.NoError(t, err)
|
|
|
|
for _, layer := range manifest.Layers {
|
|
_, err = contentStore.Info(ctx, layer.Digest)
|
|
require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v", err)
|
|
}
|
|
|
|
// clear all local state out again
|
|
err = imageService.Delete(ctx, img.Name, images.SynchronousDelete())
|
|
require.NoError(t, err)
|
|
checkAllReleasable(t, c, sb, true)
|
|
|
|
// try a cross-repo push to same registry, which should still result in the
|
|
// image remaining lazy
|
|
target3 := registry + "/buildkit/testlazycrossrepo:latest"
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target3,
|
|
"push": "true",
|
|
"store": "true",
|
|
"unsafe-internal-store-allow-incomplete": "true",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
img, err = imageService.Get(ctx, target3)
|
|
require.NoError(t, err)
|
|
|
|
manifest, err = images.Manifest(ctx, contentStore, img.Target, nil)
|
|
require.NoError(t, err)
|
|
|
|
for _, layer := range manifest.Layers {
|
|
_, err = contentStore.Info(ctx, layer.Digest)
|
|
require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v", err)
|
|
}
|
|
|
|
// check that a subsequent build can use the previously lazy image in an exec
|
|
cmdStr := integration.UnixOrWindows("true", "cmd /C exit 0")
|
|
def, err = llb.Image(target2).Run(llb.Args([]string{cmdStr})).Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func testOCIExporter(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter)
|
|
requiresLinux(t)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
busybox := llb.Image("busybox:latest")
|
|
st := llb.Scratch()
|
|
|
|
run := func(cmd string) {
|
|
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st)
|
|
}
|
|
|
|
run(`sh -c "echo -n first > foo"`)
|
|
run(`sh -c "echo -n second > bar"`)
|
|
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
target := "example.com/buildkit/testoci:latest"
|
|
for _, tc := range []struct {
|
|
name string
|
|
exporter string
|
|
attrs map[string]string
|
|
indexMediaType string
|
|
manifestMediaType string
|
|
configMediaType string
|
|
layerMediaType string
|
|
}{
|
|
{
|
|
name: ExporterOCI,
|
|
exporter: ExporterOCI,
|
|
attrs: map[string]string{},
|
|
indexMediaType: ocispecs.MediaTypeImageIndex,
|
|
manifestMediaType: ocispecs.MediaTypeImageManifest,
|
|
configMediaType: ocispecs.MediaTypeImageConfig,
|
|
layerMediaType: ocispecs.MediaTypeImageLayerGzip,
|
|
},
|
|
{
|
|
name: ExporterDocker,
|
|
exporter: ExporterDocker,
|
|
attrs: map[string]string{
|
|
"name": target,
|
|
},
|
|
indexMediaType: ocispecs.MediaTypeImageIndex,
|
|
manifestMediaType: ocispecs.MediaTypeImageManifest,
|
|
configMediaType: ocispecs.MediaTypeImageConfig,
|
|
layerMediaType: ocispecs.MediaTypeImageLayerGzip,
|
|
},
|
|
{
|
|
name: "docker-oci-mediatypes-false",
|
|
exporter: ExporterDocker,
|
|
attrs: map[string]string{
|
|
"name": target,
|
|
"oci-mediatypes": "false",
|
|
},
|
|
indexMediaType: ocispecs.MediaTypeImageIndex,
|
|
manifestMediaType: images.MediaTypeDockerSchema2Manifest,
|
|
configMediaType: images.MediaTypeDockerSchema2Config,
|
|
layerMediaType: images.MediaTypeDockerSchema2LayerGzip,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
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: tc.exporter,
|
|
Attrs: tc.attrs,
|
|
Output: fixedWriteCloser(outW),
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt, err := os.ReadFile(out)
|
|
require.NoError(t, err)
|
|
|
|
m, err := testutil.ReadTarToMap(dt, false)
|
|
require.NoError(t, err)
|
|
|
|
_, ok := m["oci-layout"]
|
|
require.True(t, ok)
|
|
|
|
var index ocispecs.Index
|
|
err = json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &index)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, index.SchemaVersion)
|
|
require.Equal(t, tc.indexMediaType, index.MediaType)
|
|
require.Equal(t, 1, len(index.Manifests))
|
|
require.Equal(t, tc.manifestMediaType, index.Manifests[0].MediaType)
|
|
|
|
var mfst ocispecs.Manifest
|
|
err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+index.Manifests[0].Digest.Hex()].Data, &mfst)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.manifestMediaType, mfst.MediaType)
|
|
require.Equal(t, tc.configMediaType, mfst.Config.MediaType)
|
|
require.Equal(t, 2, len(mfst.Layers))
|
|
for _, layer := range mfst.Layers {
|
|
require.Equal(t, tc.layerMediaType, layer.MediaType)
|
|
}
|
|
|
|
var ociimg ocispecs.Image
|
|
err = json.Unmarshal(m[ocispecs.ImageBlobsDir+"/sha256/"+mfst.Config.Digest.Hex()].Data, &ociimg)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "layers", ociimg.RootFS.Type)
|
|
require.Equal(t, 2, len(ociimg.RootFS.DiffIDs))
|
|
|
|
_, ok = m[ocispecs.ImageBlobsDir+"/sha256/"+mfst.Layers[0].Digest.Hex()]
|
|
require.True(t, ok)
|
|
_, ok = m[ocispecs.ImageBlobsDir+"/sha256/"+mfst.Layers[1].Digest.Hex()]
|
|
require.True(t, ok)
|
|
|
|
if tc.exporter != ExporterDocker {
|
|
return
|
|
}
|
|
|
|
var dockerMfst []struct {
|
|
Config string
|
|
RepoTags []string
|
|
Layers []string
|
|
}
|
|
err = json.Unmarshal(m["manifest.json"].Data, &dockerMfst)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(dockerMfst))
|
|
|
|
_, ok = m[dockerMfst[0].Config]
|
|
require.True(t, ok)
|
|
require.Equal(t, 2, len(dockerMfst[0].Layers))
|
|
require.Equal(t, 1, len(dockerMfst[0].RepoTags))
|
|
require.Equal(t, target, dockerMfst[0].RepoTags[0])
|
|
|
|
for _, l := range dockerMfst[0].Layers {
|
|
_, ok := m[l]
|
|
require.True(t, ok)
|
|
}
|
|
})
|
|
}
|
|
|
|
checkAllReleasable(t, c, sb, true)
|
|
}
|
|
|
|
func testOCIExporterContentStore(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter)
|
|
requiresLinux(t)
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
busybox := llb.Image("busybox:latest")
|
|
st := llb.Scratch()
|
|
|
|
run := func(cmd string) {
|
|
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st)
|
|
}
|
|
|
|
run(`sh -c "echo -n first > foo"`)
|
|
run(`sh -c "echo -n second > bar"`)
|
|
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
for _, exp := range []string{ExporterOCI, ExporterDocker} {
|
|
destDir := t.TempDir()
|
|
target := "example.com/buildkit/testoci:latest"
|
|
|
|
outTar := filepath.Join(destDir, "out.tar")
|
|
outW, err := os.Create(outTar)
|
|
require.NoError(t, err)
|
|
attrs := map[string]string{}
|
|
if exp == ExporterDocker {
|
|
attrs["name"] = target
|
|
}
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: exp,
|
|
Attrs: attrs,
|
|
Output: fixedWriteCloser(outW),
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt, err := os.ReadFile(outTar)
|
|
require.NoError(t, err)
|
|
m, err := testutil.ReadTarToMap(dt, false)
|
|
require.NoError(t, err)
|
|
|
|
checkStore := func(dir string) {
|
|
err = filepath.Walk(dir, func(filename string, fi os.FileInfo, err error) error {
|
|
filename = strings.TrimPrefix(filename, dir)
|
|
filename = strings.Trim(filename, "/")
|
|
if filename == "" || filename == "ingest" {
|
|
return nil
|
|
}
|
|
|
|
if fi.IsDir() {
|
|
require.Contains(t, m, filename+"/")
|
|
} else {
|
|
require.Contains(t, m, filename)
|
|
if filename == ocispecs.ImageIndexFile {
|
|
// this file has a timestamp in it, so we can't compare
|
|
return nil
|
|
}
|
|
f, err := os.Open(path.Join(dir, filename))
|
|
require.NoError(t, err)
|
|
data, err := io.ReadAll(f)
|
|
require.NoError(t, err)
|
|
require.Equal(t, m[filename].Data, data)
|
|
}
|
|
return nil
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
outDir := filepath.Join(destDir, "out.d")
|
|
attrs = map[string]string{
|
|
"tar": "false",
|
|
}
|
|
if exp == ExporterDocker {
|
|
attrs["name"] = target
|
|
}
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: exp,
|
|
Attrs: attrs,
|
|
OutputDir: outDir,
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
checkStore(outDir)
|
|
|
|
outStoreDir := filepath.Join(destDir, "store.d")
|
|
store, err := local.NewStore(outStoreDir)
|
|
require.NoError(t, err)
|
|
attrs = map[string]string{
|
|
"tar": "false",
|
|
}
|
|
if exp == ExporterDocker {
|
|
attrs["name"] = target
|
|
}
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: exp,
|
|
Attrs: attrs,
|
|
OutputStore: store,
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
checkStore(outDir)
|
|
}
|
|
|
|
checkAllReleasable(t, c, sb, true)
|
|
}
|
|
|
|
func testPullWithDigestCheck(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
|
|
requiresLinux(t)
|
|
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)
|
|
|
|
st := llb.Scratch().File(llb.Mkfile("foo", 0600, []byte("data1")))
|
|
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
name1 := registry + "/foo/bar:v1.0.0"
|
|
|
|
resp, err := c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: "image",
|
|
Attrs: map[string]string{
|
|
"name": name1,
|
|
"push": "true",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dgst1Str := resp.ExporterResponse[exptypes.ExporterImageDigestKey]
|
|
dgst1, err := digest.Parse(dgst1Str)
|
|
require.NoError(t, err)
|
|
|
|
st = llb.Scratch().File(llb.Mkfile("foo", 0600, []byte("data2")))
|
|
|
|
def, err = st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
name2 := registry + "/foo/bar:v2.0.0"
|
|
|
|
resp, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: "image",
|
|
Attrs: map[string]string{
|
|
"name": name2,
|
|
"push": "true",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dgst2str := resp.ExporterResponse[exptypes.ExporterImageDigestKey]
|
|
dgst2, err := digest.Parse(dgst2str)
|
|
require.NoError(t, err)
|
|
|
|
require.NotEqual(t, dgst1, dgst2)
|
|
|
|
// if digest is set in ref then pull happens only by the digest
|
|
st = llb.Image(name2 + "@" + dgst1.String())
|
|
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,
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt, err := os.ReadFile(filepath.Join(destDir, "foo"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "data1", string(dt))
|
|
|
|
// if digest is set by checksum then pull happens by tag
|
|
st = llb.Image(name2, llb.WithImageChecksum(dgst2))
|
|
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,
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt, err = os.ReadFile(filepath.Join(destDir, "foo"))
|
|
require.NoError(t, err)
|
|
require.Equal(t, "data2", string(dt))
|
|
|
|
// if checksum doesn't match then pull fails
|
|
st = llb.Image(name2, llb.WithImageChecksum(dgst1))
|
|
def, err = st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), fmt.Sprintf("image digest %s for %s does not match expected checksum %s", dgst2, name2, dgst1))
|
|
}
|
|
|
|
// testPullZstdImage verifies pulling and re-exporting a Zstd-compressed image.
|
|
// It pushes a Zstd-compressed image to a registry, pulls it back, and validates
|
|
// the layer media types and Zstd magic bytes for both OCI and Docker schema 2.
|
|
func testPullZstdImage(t *testing.T, sb integration.Sandbox) {
|
|
// Skipped on Windows:
|
|
// 1. AddMount() fails because Windows snapshots allow only a single mount per layer.
|
|
// 2. Zstd-compressed push/export fails because windowsLcowDiff lacks the Compare
|
|
// method needed to generate compressed layer diffs.
|
|
integration.SkipOnPlatform(t, "windows", "Windows container support incomplete: AddMount() fails with 'number of mounts should always be 1 for Windows layers', Zstd export fails with 'windowsLcowDiff does not implement Compare method'")
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
|
|
for _, ociMediaTypes := range []bool{true, false} {
|
|
t.Run(t.Name()+fmt.Sprintf("/ociMediaTypes=%t", ociMediaTypes), func(t *testing.T) {
|
|
c, err := New(sb.Context(), sb.Address())
|
|
require.NoError(t, err)
|
|
defer c.Close()
|
|
|
|
busybox := llb.Image("busybox:latest")
|
|
cmd := `sh -e -c "echo -n zstd > data"`
|
|
|
|
st := llb.Scratch()
|
|
st = busybox.Run(llb.Shlex(cmd), llb.Dir("/wd")).AddMount("/wd", st)
|
|
|
|
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)
|
|
|
|
target := registry + "/buildkit/build/exporter:zstd"
|
|
|
|
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: ExporterImage,
|
|
Attrs: map[string]string{
|
|
"name": target,
|
|
"push": "true",
|
|
"compression": "zstd",
|
|
"oci-mediatypes": strconv.FormatBool(ociMediaTypes),
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
ensurePruneAll(t, c, sb)
|
|
|
|
st = llb.Image(target).File(llb.Copy(llb.Image(target), "/data", "/zdata"))
|
|
def, err = st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
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),
|
|
Attrs: map[string]string{
|
|
"oci-mediatypes": strconv.FormatBool(ociMediaTypes),
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
dt, err := os.ReadFile(out)
|
|
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)
|
|
|
|
firstLayer := mfst.Layers[0]
|
|
if ociMediaTypes {
|
|
require.Equal(t, ocispecs.MediaTypeImageLayerZstd, firstLayer.MediaType)
|
|
} else {
|
|
require.Equal(t, images.MediaTypeDockerSchema2LayerZstd, firstLayer.MediaType)
|
|
}
|
|
|
|
zstdLayerDigest := firstLayer.Digest.Hex()
|
|
require.Equal(t, []byte{0x28, 0xb5, 0x2f, 0xfd}, m[ocispecs.ImageBlobsDir+"/sha256/"+zstdLayerDigest].Data[:4])
|
|
})
|
|
}
|
|
}
|
|
|
|
func testPushByDigest(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
|
|
requiresLinux(t)
|
|
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)
|
|
|
|
st := llb.Scratch().File(llb.Mkfile("foo", 0600, []byte("data")))
|
|
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
name := registry + "/foo/bar"
|
|
|
|
resp, err := c.Solve(sb.Context(), def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: "image",
|
|
Attrs: map[string]string{
|
|
"name": name,
|
|
"push": "true",
|
|
"push-by-digest": "true",
|
|
},
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = contentutil.ProviderFromRef(name + ":latest")
|
|
require.Error(t, err)
|
|
|
|
desc, _, err := contentutil.ProviderFromRef(name + "@" + resp.ExporterResponse[exptypes.ExporterImageDigestKey])
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, resp.ExporterResponse[exptypes.ExporterImageDigestKey], desc.Digest.String())
|
|
require.Equal(t, ocispecs.MediaTypeImageManifest, desc.MediaType)
|
|
require.Greater(t, desc.Size, int64(0))
|
|
}
|
|
|
|
func testPushProgressSameVertex(t *testing.T, sb integration.Sandbox) {
|
|
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
|
|
requiresLinux(t)
|
|
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)
|
|
|
|
st := llb.Scratch().File(llb.Mkfile("foo", 0600, []byte("data")))
|
|
def, err := st.Marshal(sb.Context())
|
|
require.NoError(t, err)
|
|
|
|
imageName := registry + "/foo/bar:latest"
|
|
|
|
var vertexes []*Vertex
|
|
var statuses []*VertexStatus
|
|
ch := make(chan *SolveStatus)
|
|
eg, ctx := errgroup.WithContext(sb.Context())
|
|
eg.Go(func() error {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return context.Cause(ctx)
|
|
case ss, ok := <-ch:
|
|
if !ok {
|
|
return nil
|
|
}
|
|
vertexes = append(vertexes, ss.Vertexes...)
|
|
statuses = append(statuses, ss.Statuses...)
|
|
}
|
|
}
|
|
})
|
|
eg.Go(func() error {
|
|
_, err := c.Solve(ctx, def, SolveOpt{
|
|
Exports: []ExportEntry{
|
|
{
|
|
Type: "image",
|
|
Attrs: map[string]string{
|
|
"name": imageName,
|
|
"push": "true",
|
|
},
|
|
},
|
|
},
|
|
}, ch)
|
|
return err
|
|
})
|
|
require.NoError(t, eg.Wait())
|
|
|
|
exportVertexes := map[digest.Digest]struct{}{}
|
|
pushVertexes := map[digest.Digest]struct{}{}
|
|
pushManifestPrefix := "pushing manifest for " + imageName + "@"
|
|
|
|
for _, v := range vertexes {
|
|
if v.Name == "exporting to image" {
|
|
exportVertexes[v.Digest] = struct{}{}
|
|
}
|
|
}
|
|
|
|
for _, st := range statuses {
|
|
switch {
|
|
case st.ID == "pushing layers":
|
|
pushVertexes[st.Vertex] = struct{}{}
|
|
case strings.HasPrefix(st.ID, pushManifestPrefix):
|
|
pushVertexes[st.Vertex] = struct{}{}
|
|
}
|
|
}
|
|
|
|
require.Len(t, exportVertexes, 1, "expected exactly one export vertex in status stream")
|
|
require.NotEmpty(t, pushVertexes, "expected at least one push status in status stream")
|
|
|
|
var exportVertex digest.Digest
|
|
for v := range exportVertexes {
|
|
exportVertex = v
|
|
}
|
|
for v := range pushVertexes {
|
|
require.Equal(t, exportVertex, v, "push statuses should use the same vertex as exporting to image")
|
|
}
|
|
|
|
hasPushManifest := false
|
|
for _, st := range statuses {
|
|
if strings.HasPrefix(st.ID, pushManifestPrefix) {
|
|
hasPushManifest = true
|
|
require.Equal(t, exportVertex, st.Vertex, "pushing manifest should use the same vertex as exporting to image")
|
|
}
|
|
}
|
|
require.True(t, hasPushManifest, "expected pushing manifest status in status stream")
|
|
}
|
|
|
|
func testStargzLazyPull(t *testing.T, sb integration.Sandbox) {
|
|
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 image
|
|
orgImage := "docker.io/library/alpine:latest"
|
|
sgzImage := registry + "/stargz/alpine:" + identity.NewID()
|
|
def, err := llb.Image(orgImage).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",
|
|
},
|
|
},
|
|
},
|
|
}, 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 = llb.Image(sgzImage).
|
|
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",
|
|
"oci-mediatypes": "true",
|
|
"store": "true",
|
|
"unsafe-internal-store-allow-incomplete": "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 _, layer := range manifest.Layers[:len(manifest.Layers)-1] {
|
|
_, err = contentStore.Info(ctx, layer.Digest)
|
|
require.ErrorIs(t, err, cerrdefs.ErrNotFound, "unexpected error %v", err)
|
|
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",
|
|
},
|
|
},
|
|
},
|
|
}, 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
|
|
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),
|
|
},
|
|
},
|
|
}, 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)
|
|
}
|