Files
buildkit/client/client_oci_source_test.go
Tonis Tiigi 38f8536ee3 client: split client integration tests by area
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>
2026-07-06 22:19:20 -07:00

607 lines
16 KiB
Go

package client
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"testing"
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/plugins/content/local"
"github.com/containerd/platforms"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
gateway "github.com/moby/buildkit/frontend/gateway/client"
provenancetypes "github.com/moby/buildkit/solver/llbsolver/provenance/types"
"github.com/moby/buildkit/util/contentutil"
"github.com/moby/buildkit/util/iohelper"
"github.com/moby/buildkit/util/purl"
"github.com/moby/buildkit/util/testutil"
"github.com/moby/buildkit/util/testutil/integration"
"github.com/moby/buildkit/util/testutil/workers"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
packageurl "github.com/package-url/packageurl-go"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
)
func testImageBlobSource(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.Image("alpine")
def, err := st.Marshal(sb.Context())
require.NoError(t, err)
name := registry + "/foo/blobtest:img"
_, err = c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{
{
Type: "image",
Attrs: map[string]string{
"name": name,
"push": "true",
},
},
},
}, nil)
require.NoError(t, err)
desc, provider, err := contentutil.ProviderFromRef(name)
require.NoError(t, err)
imgs, err := testutil.ReadImages(sb.Context(), provider, desc)
require.NoError(t, err)
require.Equal(t, 1, len(imgs.Images))
mfst := imgs.Images[0].Manifest
require.GreaterOrEqual(t, len(mfst.Layers), 1)
l := mfst.Layers[0]
blob := llb.ImageBlob(registry+"/foo/blobtest@"+l.Digest.String(), llb.Filename("layer.tar.gz"), llb.Chown(123, 456))
st = llb.Image("alpine").Run(llb.Shlex(`sh -c 'sha256sum /layers/layer.tar.gz | cut -d" " -f0 > /out/checksum && stat -c "%u-%g-%s" /layers/layer.tar.gz > /out/stat'`), llb.AddMount("/layers", blob, llb.Readonly)).AddMount("/out", llb.Scratch())
def, err = st.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)
dt, err := os.ReadFile(filepath.Join(destDir, "stat"))
require.NoError(t, err)
require.Equal(t, "123-456-"+strconv.FormatInt(l.Size, 10), strings.TrimSpace(string(dt)))
dt, err = os.ReadFile(filepath.Join(destDir, "checksum"))
require.NoError(t, err)
require.Equal(t, l.Digest.Hex(), strings.TrimSpace(string(dt)))
provDt, err := os.ReadFile(filepath.Join(destDir, "provenance.json"))
require.NoError(t, err)
var stmt struct {
intoto.StatementHeader
Predicate provenancetypes.ProvenancePredicateSLSA1 `json:"predicate"`
}
require.NoError(t, json.Unmarshal(provDt, &stmt))
expectedName, err := purl.RefToPURL(packageurl.TypeDocker, registry+"/foo/blobtest@"+l.Digest.String(), nil)
require.NoError(t, err)
purlObj, err := packageurl.FromString(expectedName)
require.NoError(t, err)
purlObj.Qualifiers = append(purlObj.Qualifiers, packageurl.Qualifier{Key: "ref_type", Value: "blob"})
expectedName = purlObj.ToString()
found := false
for _, m := range stmt.Predicate.BuildDefinition.ResolvedDependencies {
if m.URI == expectedName {
found = true
require.Equal(t, l.Digest.Hex(), m.Digest["sha256"])
break
}
}
require.True(t, found, "expected to find %q in %+v", expectedName, stmt.Predicate.BuildDefinition.ResolvedDependencies)
}
func testNoTarOCIIndexMediaType(t *testing.T, sb integration.Sandbox) {
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter)
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 -c "echo -n hello > hello"`,
`cmd /C "echo hello> hello"`,
)
st := llb.Image(imgName).Run(llb.Shlex(cmdStr))
def, err := st.Marshal(sb.Context())
require.NoError(t, err)
destDir, err := os.MkdirTemp("", "buildkit")
require.NoError(t, err)
defer os.RemoveAll(destDir)
outDir := filepath.Join(destDir, "out.d")
require.NoError(t, err)
_, err = c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterOCI,
Attrs: map[string]string{
"tar": "false",
},
OutputDir: outDir,
},
},
}, nil)
require.NoError(t, err)
dt, err := os.ReadFile(filepath.Join(outDir, "index.json"))
require.NoError(t, err)
var index ocispecs.Index
err = json.Unmarshal(dt, &index)
require.NoError(t, err)
require.Equal(t, "application/vnd.oci.image.index.v1+json", index.MediaType)
checkAllReleasable(t, c, sb, true)
}
func testOCIIndexMediatype(t *testing.T, sb integration.Sandbox) {
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter)
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 -c "echo -n hello > hello"`,
`cmd /C "echo hello> hello"`,
)
st := llb.Image(imgName).Run(llb.Shlex(cmdStr))
def, err := st.Marshal(sb.Context())
require.NoError(t, err)
destDir, err := os.MkdirTemp("", "buildkit")
require.NoError(t, err)
defer os.RemoveAll(destDir)
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)
dt, err := os.ReadFile(out)
require.NoError(t, err)
m, err := testutil.ReadTarToMap(dt, false)
require.NoError(t, err)
indexDt, ok := m[ocispecs.ImageIndexFile]
require.True(t, ok)
var index ocispecs.Index
err = json.Unmarshal(indexDt.Data, &index)
require.NoError(t, err)
require.Equal(t, "application/vnd.oci.image.index.v1+json", index.MediaType)
checkAllReleasable(t, c, sb, true)
}
func testOCILayoutBlobSource(t *testing.T, sb integration.Sandbox) {
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureOCILayout)
requiresLinux(t)
c, err := New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()
st := llb.Image("alpine")
def, err := st.Marshal(sb.Context())
require.NoError(t, err)
ociDir := t.TempDir()
_, err = c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterOCI,
Attrs: map[string]string{
"tar": "false",
},
OutputDir: ociDir,
},
},
}, nil)
require.NoError(t, err)
indexDt, err := os.ReadFile(filepath.Join(ociDir, ocispecs.ImageIndexFile))
require.NoError(t, err)
var index ocispecs.Index
err = json.Unmarshal(indexDt, &index)
require.NoError(t, err)
require.Equal(t, 1, len(index.Manifests))
var mfst ocispecs.Manifest
mfstDt, err := os.ReadFile(filepath.Join(ociDir, "blobs/sha256", index.Manifests[0].Digest.Hex()))
require.NoError(t, err)
err = json.Unmarshal(mfstDt, &mfst)
require.NoError(t, err)
require.GreaterOrEqual(t, len(mfst.Layers), 1)
layer := mfst.Layers[0]
store, err := local.NewStore(ociDir)
require.NoError(t, err)
csID := "my-blob-content-store"
blob := llb.OCILayoutBlob("not/real@"+layer.Digest.String(), llb.ImageBlobOCIStore("", csID), llb.Filename("layer.tar.gz"), llb.Chown(123, 456))
st = llb.Image("alpine").Run(llb.Shlex(`sh -c 'sha256sum /layers/layer.tar.gz | cut -d" " -f0 > /out/checksum && stat -c "%u-%g-%s" /layers/layer.tar.gz > /out/stat'`), llb.AddMount("/layers", blob, llb.Readonly)).AddMount("/out", llb.Scratch())
def, err = st.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,
},
},
OCIStores: map[string]content.Store{
csID: store,
},
}, nil)
require.NoError(t, err)
dt, err := os.ReadFile(filepath.Join(destDir, "stat"))
require.NoError(t, err)
require.Equal(t, "123-456-"+strconv.FormatInt(layer.Size, 10), strings.TrimSpace(string(dt)))
dt, err = os.ReadFile(filepath.Join(destDir, "checksum"))
require.NoError(t, err)
require.Equal(t, layer.Digest.Hex(), strings.TrimSpace(string(dt)))
provDt, err := os.ReadFile(filepath.Join(destDir, "provenance.json"))
require.NoError(t, err)
var stmt struct {
intoto.StatementHeader
Predicate provenancetypes.ProvenancePredicateSLSA1 `json:"predicate"`
}
require.NoError(t, json.Unmarshal(provDt, &stmt))
expectedName, err := purl.RefToPURL(packageurl.TypeOCI, "not/real@"+layer.Digest.String(), nil)
require.NoError(t, err)
purlObj, err := packageurl.FromString(expectedName)
require.NoError(t, err)
purlObj.Qualifiers = append(purlObj.Qualifiers, packageurl.Qualifier{Key: "ref_type", Value: "blob"})
expectedName = purlObj.ToString()
found := false
for _, m := range stmt.Predicate.BuildDefinition.ResolvedDependencies {
if m.URI == expectedName {
found = true
require.Equal(t, layer.Digest.Hex(), m.Digest["sha256"])
break
}
}
require.True(t, found, "expected to find %q in %+v", expectedName, stmt.Predicate.BuildDefinition.ResolvedDependencies)
}
func testOCILayoutPlatformSource(t *testing.T, sb integration.Sandbox) {
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureOCILayout)
requiresLinux(t)
c, err := New(context.TODO(), sb.Address())
require.NoError(t, err)
defer c.Close()
// create a tempdir where we will store the OCI layout
dir := t.TempDir()
platformsToTest := []string{"linux/amd64", "linux/arm64"}
frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
res := gateway.NewResult()
expPlatforms := &exptypes.Platforms{
Platforms: make([]exptypes.Platform, len(platformsToTest)),
}
for i, platform := range platformsToTest {
st := llb.Scratch().File(
llb.Mkfile("platform", 0600, []byte(platform)),
)
def, err := st.Marshal(ctx)
if err != nil {
return nil, 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
}
_, err = ref.ToState()
if err != nil {
return nil, err
}
res.AddRef(platform, ref)
expPlatforms.Platforms[i] = exptypes.Platform{
ID: platform,
Platform: platforms.MustParse(platform),
}
}
dt, err := json.Marshal(expPlatforms)
if err != nil {
return nil, err
}
res.AddMeta(exptypes.ExporterPlatformsKey, dt)
return res, nil
}
attrs := map[string]string{}
outW := bytes.NewBuffer(nil)
_, err = c.Build(sb.Context(), SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterOCI,
Attrs: attrs,
Output: fixedWriteCloser(&iohelper.NopWriteCloser{Writer: outW}),
},
},
}, "", frontend, nil)
require.NoError(t, err)
// extract the tar stream to the directory as OCI layout
m, err := testutil.ReadTarToMap(outW.Bytes(), false)
require.NoError(t, err)
for filename, tarItem := range m {
fullFilename := path.Join(dir, filename)
err = os.MkdirAll(path.Dir(fullFilename), 0755)
require.NoError(t, err)
if tarItem.Header.FileInfo().IsDir() {
err = os.MkdirAll(fullFilename, 0755)
require.NoError(t, err)
} else {
err = os.WriteFile(fullFilename, tarItem.Data, 0644)
require.NoError(t, err)
}
}
var index ocispecs.Index
err = json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &index)
require.NoError(t, err)
require.Equal(t, 1, len(index.Manifests))
digest := index.Manifests[0].Digest
store, err := local.NewStore(dir)
require.NoError(t, err)
csID := "my-content-store"
destDir := t.TempDir()
frontendOCILayout := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
res := gateway.NewResult()
expPlatforms := &exptypes.Platforms{
Platforms: make([]exptypes.Platform, len(platformsToTest)),
}
for i, platform := range platformsToTest {
st := llb.OCILayout(fmt.Sprintf("not/real@%s", digest), llb.OCIStore("", csID))
def, err := st.Marshal(ctx, llb.Platform(platforms.MustParse(platform)))
if err != nil {
return nil, 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
}
_, err = ref.ToState()
if err != nil {
return nil, err
}
res.AddRef(platform, ref)
expPlatforms.Platforms[i] = exptypes.Platform{
ID: platform,
Platform: platforms.MustParse(platform),
}
}
dt, err := json.Marshal(expPlatforms)
if err != nil {
return nil, err
}
res.AddMeta(exptypes.ExporterPlatformsKey, dt)
return res, nil
}
_, err = c.Build(sb.Context(), SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterLocal,
OutputDir: destDir,
},
},
OCIStores: map[string]content.Store{
csID: store,
},
}, "", frontendOCILayout, nil)
require.NoError(t, err)
for _, platform := range platformsToTest {
dt, err := os.ReadFile(filepath.Join(destDir, strings.ReplaceAll(platform, "/", "_"), "platform"))
require.NoError(t, err)
require.Equal(t, []byte(platform), dt)
}
}
func testOCILayoutSource(t *testing.T, sb integration.Sandbox) {
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter, workers.FeatureOCILayout)
c, err := New(context.TODO(), sb.Address())
require.NoError(t, err)
defer c.Close()
// create a tempdir where we will store the OCI layout
dir := t.TempDir()
// make an image that is exported there
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)
}
cmdPrefix := integration.UnixOrWindows(
`sh -c "echo -n`,
`cmd /C "echo`,
)
run(fmt.Sprintf(`%s first > foo"`, cmdPrefix))
run(fmt.Sprintf(`%s second > bar"`, cmdPrefix))
def, err := st.Marshal(sb.Context())
require.NoError(t, err)
outW := bytes.NewBuffer(nil)
attrs := map[string]string{}
_, err = c.Solve(sb.Context(), def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterOCI,
Attrs: attrs,
Output: fixedWriteCloser(&iohelper.NopWriteCloser{Writer: outW}),
},
},
}, nil)
require.NoError(t, err)
// extract the tar stream to the directory as OCI layout
m, err := testutil.ReadTarToMap(outW.Bytes(), false)
require.NoError(t, err)
for filename, content := range m {
fullFilename := path.Join(dir, filename)
err = os.MkdirAll(path.Dir(fullFilename), 0755)
require.NoError(t, err)
if content.Header.FileInfo().IsDir() {
err = os.MkdirAll(fullFilename, 0755)
require.NoError(t, err)
} else {
err = os.WriteFile(fullFilename, content.Data, 0644)
require.NoError(t, err)
}
}
var index ocispecs.Index
err = json.Unmarshal(m[ocispecs.ImageIndexFile].Data, &index)
require.NoError(t, err)
require.Equal(t, 1, len(index.Manifests))
digest := index.Manifests[0].Digest
store, err := local.NewStore(dir)
require.NoError(t, err)
// reference the OCI Layout in a build
// note that the key does not need to be the directory name, just something
// unique. since we are doing just one build with one remote here, we can
// give it any ID
csID := "my-content-store"
st = llb.OCILayout(fmt.Sprintf("not/real@%s", digest), llb.OCIStore("", csID))
def, err = st.Marshal(context.TODO())
require.NoError(t, err)
destDir := t.TempDir()
_, err = c.Solve(context.TODO(), def, SolveOpt{
Exports: []ExportEntry{
{
Type: ExporterLocal,
OutputDir: destDir,
},
},
OCIStores: map[string]content.Store{
csID: store,
},
}, nil)
require.NoError(t, err)
dt, err := os.ReadFile(filepath.Join(destDir, "foo"))
require.NoError(t, err)
newLine := integration.UnixOrWindows("", " \r\n")
require.Equal(t, []byte("first"+newLine), dt)
dt, err = os.ReadFile(filepath.Join(destDir, "bar"))
require.NoError(t, err)
require.Equal(t, []byte("second"+newLine), dt)
}