Merge pull request #2654 from crazy-max/buildinfo-contexts

buildinfo: named input contexts support
master
Tõnis Tiigi 2022-02-23 12:13:14 -08:00 committed by GitHub
commit b0e56cd563
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 590 additions and 54 deletions

View File

@ -5350,12 +5350,13 @@ func testBuildInfoInline(t *testing.T, sb integration.Sandbox) {
require.NoError(t, err)
var config binfotypes.ImageConfig
err = json.Unmarshal(dt, &config)
require.NoError(t, json.Unmarshal(dt, &config))
dec, err := base64.StdEncoding.DecodeString(config.BuildInfo)
require.NoError(t, err)
var bi binfotypes.BuildInfo
err = json.Unmarshal(config.BuildInfo, &bi)
require.NoError(t, err)
require.NoError(t, json.Unmarshal(dec, &bi))
if tt.buildAttrs {
attrval := "bar"

View File

@ -6,18 +6,6 @@ Build dependencies are generated when your image has been built. These
dependencies include versions of used images, git repositories and HTTP URLs
used by LLB `Source` operation as well as build request attributes.
By default, the build dependencies are inlined in the image configuration. You
can disable this behavior with the [`buildinfo` attribute](../README.md#imageregistry).
### Image config
A new field similar to the one for inline cache has been added to the image
configuration to embed build dependencies:
```text
"moby.buildkit.buildinfo.v1": <base64>
```
The structure is base64 encoded and has the following format when decoded:
```json
@ -57,10 +45,25 @@ The structure is base64 encoded and has the following format when decoded:
* `frontend` defines the frontend used to build.
* `attrs` defines build request attributes.
* `sources` defines build dependencies.
* `sources` defines build sources.
* `type` defines the source type (`docker-image`, `git` or `http`).
* `ref` is the reference of the source.
* `pin` is the source digest.
* `deps` defines build dependencies of input contexts.
### Image config
A new field similar to the one for inline cache has been added to the image
configuration to embed build dependencies:
```json
{
"moby.buildkit.buildinfo.v0": "<base64>"
}
```
By default, the build dependencies are inlined in the image configuration. You
can disable this behavior with the [`buildinfo` attribute](../README.md#imageregistry).
### Exporter response (metadata)

View File

@ -382,16 +382,16 @@ func Dockerfile2LLB(ctx context.Context, dt []byte, opt ConvertOpt) (*llb.State,
buildContext := &mutableOutput{}
ctxPaths := map[string]struct{}{}
buildinfo := &binfotypes.BuildInfo{}
buildInfo := &binfotypes.BuildInfo{}
for _, d := range allDispatchStates.states {
if !isReachable(target, d) {
continue
}
// collect build dependencies
// collect build sources and dependencies
if d.buildSource != nil {
buildinfo.Sources = append(buildinfo.Sources, *d.buildSource)
buildInfo.Sources = append(buildInfo.Sources, *d.buildSource)
}
if d.base != nil {
@ -469,9 +469,9 @@ func Dockerfile2LLB(ctx context.Context, dt []byte, opt ConvertOpt) (*llb.State,
}
// sort build sources
if len(buildinfo.Sources) > 0 {
sort.Slice(buildinfo.Sources, func(i, j int) bool {
return buildinfo.Sources[i].Ref < buildinfo.Sources[j].Ref
if len(buildInfo.Sources) > 0 {
sort.Slice(buildInfo.Sources, func(i, j int) bool {
return buildInfo.Sources[i].Ref < buildInfo.Sources[j].Ref
})
}
@ -512,7 +512,7 @@ func Dockerfile2LLB(ctx context.Context, dt []byte, opt ConvertOpt) (*llb.State,
target.image.Variant = platformOpt.targetPlatform.Variant
}
return &st, &target.image, buildinfo, nil
return &st, &target.image, buildInfo, nil
}
func metaArgsToMap(metaArgs []instructions.KeyValuePairOptional) map[string]string {

View File

@ -1,6 +1,7 @@
package dockerfile
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
@ -16,16 +17,21 @@ import (
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/moby/buildkit/frontend/dockerfile/builder"
gateway "github.com/moby/buildkit/frontend/gateway/client"
"github.com/moby/buildkit/solver/pb"
binfotypes "github.com/moby/buildkit/util/buildinfo/types"
"github.com/moby/buildkit/util/testutil/integration"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var buildinfoTests = integration.TestFuncs(
testBuildSources,
testBuildAttrs,
testBuildInfoSources,
testBuildInfoAttrs,
testBuildInfoMultiPlatform,
testBuildInfoDeps,
testBuildInfoDepsMultiPlatform,
)
func init() {
@ -33,7 +39,7 @@ func init() {
}
// moby/buildkit#2311
func testBuildSources(t *testing.T, sb integration.Sandbox) {
func testBuildInfoSources(t *testing.T, sb integration.Sandbox) {
f := getFrontend(t, sb)
gitDir, err := ioutil.TempDir("", "buildkit")
@ -114,7 +120,7 @@ COPY --from=alpine /bin/busybox /alpine-busybox
}
// moby/buildkit#2476
func testBuildAttrs(t *testing.T, sb integration.Sandbox) {
func testBuildInfoAttrs(t *testing.T, sb integration.Sandbox) {
f := getFrontend(t, sb)
f.RequiresBuildctl(t)
@ -245,3 +251,276 @@ ADD https://raw.githubusercontent.com/moby/moby/master/README.md /
assert.Equal(t, "sha256:419455202b0ef97e480d7f8199b26a721a417818bc0e2d106975f74323f25e6c", sources[1].Pin)
}
}
func testBuildInfoDeps(t *testing.T, sb integration.Sandbox) {
ctx := sb.Context()
f := getFrontend(t, sb)
f.RequiresBuildctl(t)
c, err := client.New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()
dockerfile := []byte(`
FROM alpine
ENV FOO=bar
RUN echo first > /out
`)
dir, err := tmpdir(
fstest.CreateFile("Dockerfile", dockerfile, 0600),
)
require.NoError(t, err)
defer os.RemoveAll(dir)
dockerfile2 := []byte(`
FROM base AS build
RUN echo "foo is $FOO" > /foo
FROM busybox
COPY --from=build /foo /out /
`)
dir2, err := tmpdir(
fstest.CreateFile("Dockerfile", dockerfile2, 0600),
)
require.NoError(t, err)
defer os.RemoveAll(dir)
b := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
res, err := f.SolveGateway(ctx, c, gateway.SolveRequest{})
if err != nil {
return nil, err
}
ref, err := res.SingleRef()
if err != nil {
return nil, err
}
st, err := ref.ToState()
if err != nil {
return nil, err
}
def, err := st.Marshal(ctx)
if err != nil {
return nil, err
}
dtic, ok := res.Metadata[exptypes.ExporterImageConfigKey]
if !ok {
return nil, errors.Errorf("no containerimage.config in metadata")
}
dtbi, ok := res.Metadata[exptypes.ExporterBuildInfo]
if !ok {
return nil, errors.Errorf("no containerimage.buildinfo in metadata")
}
dt, err := json.Marshal(map[string][]byte{
exptypes.ExporterImageConfigKey: dtic,
exptypes.ExporterBuildInfo: dtbi,
})
if err != nil {
return nil, err
}
res, err = f.SolveGateway(ctx, c, gateway.SolveRequest{
FrontendOpt: map[string]string{
"dockerfilekey": builder.DefaultLocalNameDockerfile + "2",
"context:base": "input:base",
"input-metadata:base": string(dt),
},
FrontendInputs: map[string]*pb.Definition{
"base": def.ToPB(),
},
})
if err != nil {
return nil, err
}
return res, nil
}
destDir, err := ioutil.TempDir("", "buildkit")
require.NoError(t, err)
defer os.RemoveAll(destDir)
res, err := c.Build(ctx, client.SolveOpt{
LocalDirs: map[string]string{
builder.DefaultLocalNameDockerfile: dir,
builder.DefaultLocalNameContext: dir,
builder.DefaultLocalNameDockerfile + "2": dir2,
},
Exports: []client.ExportEntry{
{
Type: client.ExporterLocal,
OutputDir: destDir,
},
},
}, "", b, nil)
require.NoError(t, err)
require.Contains(t, res.ExporterResponse, exptypes.ExporterBuildInfo)
dtbi, err := base64.StdEncoding.DecodeString(res.ExporterResponse[exptypes.ExporterBuildInfo])
require.NoError(t, err)
var bi binfotypes.BuildInfo
err = json.Unmarshal(dtbi, &bi)
require.NoError(t, err)
require.Equal(t, 2, len(bi.Sources))
assert.Equal(t, binfotypes.SourceTypeDockerImage, bi.Sources[0].Type)
assert.True(t, strings.HasPrefix(bi.Sources[0].Ref, "docker.io/library/alpine"))
assert.NotEmpty(t, bi.Sources[0].Pin)
assert.Equal(t, binfotypes.SourceTypeDockerImage, bi.Sources[1].Type)
assert.Equal(t, "docker.io/library/busybox:latest", bi.Sources[1].Ref)
assert.NotEmpty(t, bi.Sources[1].Pin)
require.Contains(t, bi.Deps, "base")
depsrc := bi.Deps["base"].Sources
require.Equal(t, 1, len(depsrc))
assert.Equal(t, binfotypes.SourceTypeDockerImage, depsrc[0].Type)
assert.Equal(t, "alpine", depsrc[0].Ref)
assert.NotEmpty(t, depsrc[0].Pin)
}
func testBuildInfoDepsMultiPlatform(t *testing.T, sb integration.Sandbox) {
ctx := sb.Context()
f := getFrontend(t, sb)
f.RequiresBuildctl(t)
platforms := []string{"linux/amd64", "linux/arm64"}
c, err := client.New(ctx, sb.Address())
require.NoError(t, err)
defer c.Close()
dockerfile := []byte(`
FROM --platform=$BUILDPLATFORM alpine
ARG TARGETARCH
ENV FOO=bar-$TARGETARCH
RUN echo "foo $TARGETARCH" > /out
`)
dir, err := tmpdir(
fstest.CreateFile("Dockerfile", dockerfile, 0600),
)
require.NoError(t, err)
defer os.RemoveAll(dir)
dockerfile2 := []byte(`
FROM base AS build
RUN echo "foo is $FOO" > /foo
FROM busybox
COPY --from=build /foo /out /
`)
dir2, err := tmpdir(
fstest.CreateFile("Dockerfile", dockerfile2, 0600),
)
require.NoError(t, err)
defer os.RemoveAll(dir)
b := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
res, err := f.SolveGateway(ctx, c, gateway.SolveRequest{
FrontendOpt: map[string]string{
"platform": strings.Join(platforms, ","),
},
})
if err != nil {
return nil, err
}
if len(res.Refs) != 2 {
return nil, errors.Errorf("expected 2 refs, got %d", len(res.Refs))
}
frontendOpt := map[string]string{
"dockerfilekey": builder.DefaultLocalNameDockerfile + "2",
"platform": strings.Join(platforms, ","),
}
inputs := map[string]*pb.Definition{}
for _, platform := range platforms {
frontendOpt["context:base::"+platform] = "input:base::" + platform
st, err := res.Refs[platform].ToState()
if err != nil {
return nil, err
}
def, err := st.Marshal(ctx)
if err != nil {
return nil, err
}
inputs["base::"+platform] = def.ToPB()
dtic, ok := res.Metadata[exptypes.ExporterImageConfigKey+"/"+platform]
if !ok {
return nil, errors.Errorf("no containerimage.config/" + platform + " in metadata")
}
dtbi, ok := res.Metadata[exptypes.ExporterBuildInfo+"/"+platform]
if !ok {
return nil, errors.Errorf("no containerimage.buildinfo/" + platform + " in metadata")
}
dt, err := json.Marshal(map[string][]byte{
exptypes.ExporterImageConfigKey: dtic,
exptypes.ExporterBuildInfo: dtbi,
})
if err != nil {
return nil, err
}
frontendOpt["input-metadata:base::"+platform] = string(dt)
}
res, err = f.SolveGateway(ctx, c, gateway.SolveRequest{
FrontendOpt: frontendOpt,
FrontendInputs: inputs,
})
if err != nil {
return nil, err
}
return res, nil
}
destDir, err := ioutil.TempDir("", "buildkit")
require.NoError(t, err)
defer os.RemoveAll(destDir)
res, err := c.Build(ctx, client.SolveOpt{
LocalDirs: map[string]string{
builder.DefaultLocalNameDockerfile: dir,
builder.DefaultLocalNameContext: dir,
builder.DefaultLocalNameDockerfile + "2": dir2,
},
Exports: []client.ExportEntry{
{
Type: client.ExporterLocal,
OutputDir: destDir,
},
},
}, "", b, nil)
require.NoError(t, err)
for _, platform := range platforms {
require.Contains(t, res.ExporterResponse, fmt.Sprintf("%s/%s", exptypes.ExporterBuildInfo, platform))
dtbi, err := base64.StdEncoding.DecodeString(res.ExporterResponse[fmt.Sprintf("%s/%s", exptypes.ExporterBuildInfo, platform)])
require.NoError(t, err)
var bi binfotypes.BuildInfo
err = json.Unmarshal(dtbi, &bi)
require.NoError(t, err)
require.Equal(t, 2, len(bi.Sources))
assert.Equal(t, binfotypes.SourceTypeDockerImage, bi.Sources[0].Type)
assert.True(t, strings.HasPrefix(bi.Sources[0].Ref, "docker.io/library/alpine"))
assert.NotEmpty(t, bi.Sources[0].Pin)
assert.Equal(t, binfotypes.SourceTypeDockerImage, bi.Sources[1].Type)
assert.Equal(t, "docker.io/library/busybox:latest", bi.Sources[1].Ref)
assert.NotEmpty(t, bi.Sources[1].Pin)
require.Contains(t, bi.Deps, "base")
depsrc := bi.Deps["base"].Sources
require.Equal(t, 1, len(depsrc))
assert.Equal(t, binfotypes.SourceTypeDockerImage, depsrc[0].Type)
assert.Equal(t, "alpine", depsrc[0].Ref)
assert.NotEmpty(t, depsrc[0].Pin)
}
}

View File

@ -161,7 +161,7 @@ func (s *Solver) Solve(ctx context.Context, id string, sessionID string, req fro
res.Metadata = make(map[string][]byte)
}
if r := res.Ref; r != nil {
dtbi, err := buildinfo.Encode(ctx, res.Metadata[exptypes.ExporterBuildInfo], r.BuildSources())
dtbi, err := buildinfo.Encode(ctx, res.Metadata, exptypes.ExporterBuildInfo, r.BuildSources())
if err != nil {
return nil, err
}
@ -174,7 +174,7 @@ func (s *Solver) Solve(ctx context.Context, id string, sessionID string, req fro
if r == nil {
continue
}
dtbi, err := buildinfo.Encode(ctx, res.Metadata[fmt.Sprintf("%s/%s", exptypes.ExporterBuildInfo, k)], r.BuildSources())
dtbi, err := buildinfo.Encode(ctx, res.Metadata, fmt.Sprintf("%s/%s", exptypes.ExporterBuildInfo, k), r.BuildSources())
if err != nil {
return nil, err
}

View File

@ -8,6 +8,7 @@ import (
"strings"
"github.com/docker/distribution/reference"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/moby/buildkit/source"
binfotypes "github.com/moby/buildkit/util/buildinfo/types"
"github.com/moby/buildkit/util/urlutil"
@ -25,22 +26,28 @@ func Decode(enc string) (bi binfotypes.BuildInfo, _ error) {
}
// Encode encodes build info.
func Encode(ctx context.Context, buildInfo []byte, buildSources map[string]string) ([]byte, error) {
func Encode(ctx context.Context, metadata map[string][]byte, key string, buildSources map[string]string) ([]byte, error) {
var bi binfotypes.BuildInfo
if buildInfo != nil {
if err := json.Unmarshal(buildInfo, &bi); err != nil {
if metadata == nil {
metadata = make(map[string][]byte)
}
if v, ok := metadata[key]; ok && v != nil {
if err := json.Unmarshal(v, &bi); err != nil {
return nil, err
}
}
msources, err := mergeSources(ctx, buildSources, bi.Sources)
if err != nil {
if deps, err := decodeDeps(key, bi.Attrs); err == nil {
bi.Deps = reduceMapBuildInfo(deps, bi.Deps)
} else {
return nil, err
}
return json.Marshal(binfotypes.BuildInfo{
Frontend: bi.Frontend,
Attrs: filterAttrs(bi.Attrs),
Sources: msources,
})
if sources, err := mergeSources(ctx, buildSources, bi.Sources); err == nil {
bi.Sources = sources
} else {
return nil, err
}
bi.Attrs = filterAttrs(key, bi.Attrs)
return json.Marshal(bi)
}
// mergeSources combines and fixes build sources from frontend sources.
@ -136,6 +143,60 @@ func mergeSources(ctx context.Context, buildSources map[string]string, frontendS
return srcs, nil
}
// decodeDeps decodes dependencies (buildinfo) added via the input context.
func decodeDeps(key string, attrs map[string]*string) (map[string]binfotypes.BuildInfo, error) {
var platform string
// extract platform from metadata key
skey := strings.SplitN(key, "/", 2)
if len(skey) == 2 {
platform = skey[1]
}
res := make(map[string]binfotypes.BuildInfo)
for k, v := range attrs {
// dependencies are only handled via the input context
if v == nil || !strings.HasPrefix(k, "input-metadata:") {
continue
}
// if platform is defined, only decode dependencies for that platform
if platform != "" && !strings.HasSuffix(k, "::"+platform) {
continue
}
// decode input metadata
var inputresp map[string]string
if err := json.Unmarshal([]byte(*v), &inputresp); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal input-metadata")
}
// check buildinfo key is present
if _, ok := inputresp[exptypes.ExporterBuildInfo]; !ok {
continue
}
// decode buildinfo
bi, err := Decode(inputresp[exptypes.ExporterBuildInfo])
if err != nil {
return nil, errors.Wrap(err, "failed to decode buildinfo from input-metadata")
}
// set dep key
var depkey string
kl := strings.SplitN(k, ":", 2)
depkey = kl[1]
if platform != "" {
depkey = strings.TrimSuffix(depkey, "::"+platform)
}
res[depkey] = bi
}
if len(res) == 0 {
return nil, nil
}
return res, nil
}
// FormatOpts holds build info format options.
type FormatOpts struct {
RemoveAttrs bool
@ -178,22 +239,43 @@ var knownAttrs = []string{
// filterAttrs filters frontent opt by picking only those that
// could effectively change the build result.
func filterAttrs(attrs map[string]*string) map[string]*string {
func filterAttrs(key string, attrs map[string]*string) map[string]*string {
var platform string
// extract platform from metadata key
skey := strings.SplitN(key, "/", 2)
if len(skey) == 2 {
platform = skey[1]
}
filtered := make(map[string]*string)
for k, v := range attrs {
if v == nil {
continue
}
// Control args are filtered out
// control args are filtered out
if isControlArg(k) {
continue
}
// Always include args and labels
// always include
if strings.HasPrefix(k, "build-arg:") || strings.HasPrefix(k, "label:") {
filtered[k] = v
continue
}
// Filter only for known attributes
// input context key and value has to be cleaned up
// before being included
if strings.HasPrefix(k, "context:") {
if platform != "" {
// if platform is defined, only include the relevant platform
if !strings.HasSuffix(k, "::"+platform) {
continue
}
ctxival := strings.TrimSuffix(*v, "::"+platform)
filtered[strings.TrimSuffix(k, "::"+platform)] = &ctxival
continue
}
filtered[k] = v
continue
}
// filter only for known attributes
for _, knownAttr := range knownAttrs {
if knownAttr == k {
filtered[k] = v
@ -227,11 +309,11 @@ func isControlArg(attrKey string) bool {
// GetMetadata returns buildinfo metadata for the specified key. If the key
// is already there, result will be merged.
func GetMetadata(metadata map[string][]byte, key string, reqFrontend string, reqAttrs map[string]string) ([]byte, error) {
var (
dtbi []byte
err error
)
if v, ok := metadata[key]; ok {
if metadata == nil {
metadata = make(map[string][]byte)
}
var dtbi []byte
if v, ok := metadata[key]; ok && v != nil {
var mbi binfotypes.BuildInfo
if errm := json.Unmarshal(v, &mbi); errm != nil {
return nil, errors.Wrapf(errm, "failed to unmarshal build info for %q", key)
@ -239,15 +321,26 @@ func GetMetadata(metadata map[string][]byte, key string, reqFrontend string, req
if reqFrontend != "" {
mbi.Frontend = reqFrontend
}
mbi.Attrs = convertMap(reduceMap(reqAttrs, mbi.Attrs))
if deps, err := decodeDeps(key, convertMap(reduceMapString(reqAttrs, mbi.Attrs))); err == nil {
mbi.Deps = reduceMapBuildInfo(deps, mbi.Deps)
} else {
return nil, err
}
mbi.Attrs = filterAttrs(key, convertMap(reduceMapString(reqAttrs, mbi.Attrs)))
var err error
dtbi, err = json.Marshal(mbi)
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal build info for %q", key)
}
} else {
deps, err := decodeDeps(key, convertMap(reqAttrs))
if err != nil {
return nil, err
}
dtbi, err = json.Marshal(binfotypes.BuildInfo{
Frontend: reqFrontend,
Attrs: convertMap(reqAttrs),
Attrs: filterAttrs(key, convertMap(reqAttrs)),
Deps: deps,
})
if err != nil {
return nil, errors.Wrapf(err, "failed to marshal build info for %q", key)
@ -256,7 +349,26 @@ func GetMetadata(metadata map[string][]byte, key string, reqFrontend string, req
return dtbi, nil
}
func reduceMap(m1 map[string]string, m2 map[string]*string) map[string]string {
// FromImageConfig returns build info from image config.
func FromImageConfig(dt []byte) (*binfotypes.BuildInfo, error) {
if len(dt) == 0 {
return nil, nil
}
var config binfotypes.ImageConfig
if err := json.Unmarshal(dt, &config); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal image config")
}
if len(config.BuildInfo) == 0 {
return nil, nil
}
bi, err := Decode(config.BuildInfo)
if err != nil {
return nil, errors.Wrap(err, "failed to decode build info from image config")
}
return &bi, nil
}
func reduceMapString(m1 map[string]string, m2 map[string]*string) map[string]string {
if m1 == nil && m2 == nil {
return nil
}
@ -271,6 +383,19 @@ func reduceMap(m1 map[string]string, m2 map[string]*string) map[string]string {
return m1
}
func reduceMapBuildInfo(m1 map[string]binfotypes.BuildInfo, m2 map[string]binfotypes.BuildInfo) map[string]binfotypes.BuildInfo {
if m1 == nil && m2 == nil {
return nil
}
if m1 == nil {
m1 = map[string]binfotypes.BuildInfo{}
}
for k, v := range m2 {
m1[k] = v
}
return m1
}
func convertMap(m map[string]string) map[string]*string {
res := make(map[string]*string)
for k, v := range m {

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"testing"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
binfotypes "github.com/moby/buildkit/util/buildinfo/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -91,6 +92,131 @@ func TestMergeSources(t *testing.T) {
}, srcs)
}
func TestDecodeDeps(t *testing.T) {
cases := []struct {
name string
key string
attrs map[string]*string
want map[string]binfotypes.BuildInfo
}{
{
name: "simple",
key: exptypes.ExporterBuildInfo,
attrs: map[string]*string{
"build-arg:bar": stringPtr("foo"),
"build-arg:foo": stringPtr("bar"),
"context:baseapp": stringPtr("input:0-base"),
"filename": stringPtr("Dockerfile"),
"input-metadata:0-base": stringPtr("{\"containerimage.buildinfo\":\"eyJmcm9udGVuZCI6ImRvY2tlcmZpbGUudjAiLCJhdHRycyI6eyJidWlsZC1hcmc6YmFyIjoiZm9vIiwiYnVpbGQtYXJnOmZvbyI6ImJhciIsImZpbGVuYW1lIjoiYmFzZWFwcC5Eb2NrZXJmaWxlIn0sInNvdXJjZXMiOlt7InR5cGUiOiJkb2NrZXItaW1hZ2UiLCJyZWYiOiJidXN5Ym94IiwiYWxpYXMiOiJkb2NrZXIuaW8vbGlicmFyeS9idXN5Ym94QHNoYTI1NjphZmNjN2YxYWMxYjQ5ZGIzMTdhNzE5NmM5MDJlNjFjNmMzYzQ2MDdkNjM1OTllZTFhODJkNzAyZDI0OWEwY2NiIiwicGluIjoic2hhMjU2OmFmY2M3ZjFhYzFiNDlkYjMxN2E3MTk2YzkwMmU2MWM2YzNjNDYwN2Q2MzU5OWVlMWE4MmQ3MDJkMjQ5YTBjY2IifV19\",\"containerimage.config\":\"eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsIm9zIjoibGludXgiLCJyb290ZnMiOnsidHlwZSI6ImxheWVycyIsImRpZmZfaWRzIjpbInNoYTI1NjpkMzE1MDVmZDUwNTBmNmI5NmNhMzI2OGQxZGI1OGZjOTFhZTU2MWRkZjE0ZWFhYmM0MWQ2M2VhMmVmOGMxYzZkIl19LCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMi0wMi0wNFQyMToyMDoxMi4zMTg5MTc4MjJaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIEFERCBmaWxlOjFjODUwN2UzZTliMjJiOTc3OGYyZWRiYjk1MDA2MWUwNmJkZTZhMWY1M2I2OWUxYzYxMDI1MDAyOWMzNzNiNzIgaW4gLyAifSx7ImNyZWF0ZWQiOiIyMDIyLTAyLTA0VDIxOjIwOjEyLjQ5Nzc5NDgwOVoiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgIENNRCBbXCJzaFwiXSIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWRfYnkiOiJXT1JLRElSIC9zcmMiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9XSwiY29uZmlnIjp7IkVudiI6WyJQQVRIPS91c3IvbG9jYWwvc2JpbjovdXNyL2xvY2FsL2JpbjovdXNyL3NiaW46L3Vzci9iaW46L3NiaW46L2JpbiJdLCJDbWQiOlsic2giXSwiV29ya2luZ0RpciI6Ii9zcmMiLCJPbkJ1aWxkIjpudWxsfX0=\"}"),
},
want: map[string]binfotypes.BuildInfo{
"0-base": {
Frontend: "dockerfile.v0",
Attrs: map[string]*string{
"build-arg:bar": stringPtr("foo"),
"build-arg:foo": stringPtr("bar"),
"filename": stringPtr("baseapp.Dockerfile"),
},
Sources: []binfotypes.Source{
{
Type: binfotypes.SourceTypeDockerImage,
Ref: "busybox",
Alias: "docker.io/library/busybox@sha256:afcc7f1ac1b49db317a7196c902e61c6c3c4607d63599ee1a82d702d249a0ccb",
Pin: "sha256:afcc7f1ac1b49db317a7196c902e61c6c3c4607d63599ee1a82d702d249a0ccb",
},
},
},
},
},
{
name: "multiplatform",
key: exptypes.ExporterBuildInfo + "/linux/amd64",
attrs: map[string]*string{
"context:base::linux/amd64": stringPtr("input:base::linux/amd64"),
"context:base::linux/arm64": stringPtr("input:base::linux/arm64"),
"dockerfilekey": stringPtr("dockerfile2"),
"input-metadata:base::linux/amd64": stringPtr("{\"containerimage.buildinfo\":\"eyJmcm9udGVuZCI6ImRvY2tlcmZpbGUudjAiLCJzb3VyY2VzIjpbeyJ0eXBlIjoiZG9ja2VyLWltYWdlIiwicmVmIjoiYWxwaW5lIiwiYWxpYXMiOiJkb2NrZXIuaW8vbGlicmFyeS9hbHBpbmVAc2hhMjU2OmU3ZDg4ZGU3M2RiM2QzZmQ5YjJkNjNhYTdmNDQ3YTEwZmQwMjIwYjdjYmYzOTgwM2M4MDNmMmFmOWJhMjU2YjMiLCJwaW4iOiJzaGEyNTY6ZTdkODhkZTczZGIzZDNmZDliMmQ2M2FhN2Y0NDdhMTBmZDAyMjBiN2NiZjM5ODAzYzgwM2YyYWY5YmEyNTZiMyJ9XX0=\",\"containerimage.config\":\"eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsIm9zIjoibGludXgiLCJyb290ZnMiOnsidHlwZSI6ImxheWVycyIsImRpZmZfaWRzIjpbInNoYTI1Njo4ZDNhYzM0ODk5OTY0MjNmNTNkNjA4N2M4MTE4MDAwNjI2M2I3OWYyMDZkM2ZkZWM5ZTY2ZjBlMjdjZWI4NzU5Il19LCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMS0xMS0yNFQyMDoxOTo0MC4xOTk3MDA5NDZaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIEFERCBmaWxlOjkyMzNmNmYyMjM3ZDc5NjU5YTk1MjFmN2UzOTBkZjIxN2NlYzQ5ZjFhOGFhM2ExMjE0N2JiY2ExOTU2YWNkYjkgaW4gLyAifSx7ImNyZWF0ZWQiOiIyMDIxLTExLTI0VDIwOjE5OjQwLjQ4MzM2NzU0NloiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgIENNRCBbXCIvYmluL3NoXCJdIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZF9ieSI6IkFSRyBUQVJHRVRBUkNIIiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkX2J5IjoiRU5WIEZPTz1iYXItYW1kNjQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWRfYnkiOiJSVU4gfDEgVEFSR0VUQVJDSD1hbWQ2NCAvYmluL3NoIC1jIGVjaG8gXCJmb28gJFRBUkdFVEFSQ0hcIiBcdTAwM2UgL291dCAjIGJ1aWxka2l0IiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAifV0sImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iLCJGT089YmFyLWFtZDY0Il0sIkNtZCI6WyIvYmluL3NoIl0sIk9uQnVpbGQiOm51bGx9fQ==\"}"),
"input-metadata:base::linux/arm64": stringPtr("{\"containerimage.buildinfo\":\"eyJmcm9udGVuZCI6ImRvY2tlcmZpbGUudjAiLCJzb3VyY2VzIjpbeyJ0eXBlIjoiZG9ja2VyLWltYWdlIiwicmVmIjoiYWxwaW5lIiwiYWxpYXMiOiJkb2NrZXIuaW8vbGlicmFyeS9hbHBpbmVAc2hhMjU2OmU3ZDg4ZGU3M2RiM2QzZmQ5YjJkNjNhYTdmNDQ3YTEwZmQwMjIwYjdjYmYzOTgwM2M4MDNmMmFmOWJhMjU2YjMiLCJwaW4iOiJzaGEyNTY6ZTdkODhkZTczZGIzZDNmZDliMmQ2M2FhN2Y0NDdhMTBmZDAyMjBiN2NiZjM5ODAzYzgwM2YyYWY5YmEyNTZiMyJ9XX0=\",\"containerimage.config\":\"eyJhcmNoaXRlY3R1cmUiOiJhcm02NCIsIm9zIjoibGludXgiLCJyb290ZnMiOnsidHlwZSI6ImxheWVycyIsImRpZmZfaWRzIjpbInNoYTI1Njo4ZDNhYzM0ODk5OTY0MjNmNTNkNjA4N2M4MTE4MDAwNjI2M2I3OWYyMDZkM2ZkZWM5ZTY2ZjBlMjdjZWI4NzU5Il19LCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMS0xMS0yNFQyMDoxOTo0MC4xOTk3MDA5NDZaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIEFERCBmaWxlOjkyMzNmNmYyMjM3ZDc5NjU5YTk1MjFmN2UzOTBkZjIxN2NlYzQ5ZjFhOGFhM2ExMjE0N2JiY2ExOTU2YWNkYjkgaW4gLyAifSx7ImNyZWF0ZWQiOiIyMDIxLTExLTI0VDIwOjE5OjQwLjQ4MzM2NzU0NloiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgIENNRCBbXCIvYmluL3NoXCJdIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZF9ieSI6IkFSRyBUQVJHRVRBUkNIIiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkX2J5IjoiRU5WIEZPTz1iYXItYXJtNjQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWRfYnkiOiJSVU4gfDEgVEFSR0VUQVJDSD1hcm02NCAvYmluL3NoIC1jIGVjaG8gXCJmb28gJFRBUkdFVEFSQ0hcIiBcdTAwM2UgL291dCAjIGJ1aWxka2l0IiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAifV0sImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iLCJGT089YmFyLWFybTY0Il0sIkNtZCI6WyIvYmluL3NoIl0sIk9uQnVpbGQiOm51bGx9fQ==\"}"),
"platform": stringPtr("linux/amd64,linux/arm64"),
},
want: map[string]binfotypes.BuildInfo{
"base": {
Frontend: "dockerfile.v0",
Attrs: nil,
Sources: []binfotypes.Source{
{
Type: binfotypes.SourceTypeDockerImage,
Ref: "alpine",
Alias: "docker.io/library/alpine@sha256:e7d88de73db3d3fd9b2d63aa7f447a10fd0220b7cbf39803c803f2af9ba256b3",
Pin: "sha256:e7d88de73db3d3fd9b2d63aa7f447a10fd0220b7cbf39803c803f2af9ba256b3",
},
},
},
},
},
}
for _, tt := range cases {
tt := tt
t.Run(tt.name, func(t *testing.T) {
deps, err := decodeDeps(tt.key, tt.attrs)
require.NoError(t, err)
assert.Equal(t, tt.want, deps)
})
}
}
func TestFilterAttrs(t *testing.T) {
cases := []struct {
name string
key string
attrs map[string]*string
want map[string]*string
}{
{
name: "simple",
key: exptypes.ExporterBuildInfo,
attrs: map[string]*string{
"build-arg:foo": stringPtr("bar"),
"cmdline": stringPtr("crazymax/dockerfile:buildattrs"),
"context": stringPtr("https://github.com/crazy-max/buildkit-buildsources-test.git#master"),
"filename": stringPtr("Dockerfile"),
"source": stringPtr("crazymax/dockerfile:master"),
},
want: map[string]*string{
"build-arg:foo": stringPtr("bar"),
"context": stringPtr("https://github.com/crazy-max/buildkit-buildsources-test.git#master"),
"filename": stringPtr("Dockerfile"),
"source": stringPtr("crazymax/dockerfile:master"),
},
},
{
name: "multiplatform",
key: exptypes.ExporterBuildInfo + "/linux/amd64",
attrs: map[string]*string{
"build-arg:bar": stringPtr("foo"),
"build-arg:foo": stringPtr("bar"),
"context:base::linux/amd64": stringPtr("input:base::linux/amd64"),
"context:base::linux/arm64": stringPtr("input:base::linux/arm64"),
"dockerfilekey": stringPtr("dockerfile2"),
"input-metadata:base::linux/amd64": stringPtr("{\"containerimage.buildinfo\":\"eyJmcm9udGVuZCI6ImRvY2tlcmZpbGUudjAiLCJzb3VyY2VzIjpbeyJ0eXBlIjoiZG9ja2VyLWltYWdlIiwicmVmIjoiYWxwaW5lIiwiYWxpYXMiOiJkb2NrZXIuaW8vbGlicmFyeS9hbHBpbmVAc2hhMjU2OmU3ZDg4ZGU3M2RiM2QzZmQ5YjJkNjNhYTdmNDQ3YTEwZmQwMjIwYjdjYmYzOTgwM2M4MDNmMmFmOWJhMjU2YjMiLCJwaW4iOiJzaGEyNTY6ZTdkODhkZTczZGIzZDNmZDliMmQ2M2FhN2Y0NDdhMTBmZDAyMjBiN2NiZjM5ODAzYzgwM2YyYWY5YmEyNTZiMyJ9XX0=\",\"containerimage.config\":\"eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsIm9zIjoibGludXgiLCJyb290ZnMiOnsidHlwZSI6ImxheWVycyIsImRpZmZfaWRzIjpbInNoYTI1Njo4ZDNhYzM0ODk5OTY0MjNmNTNkNjA4N2M4MTE4MDAwNjI2M2I3OWYyMDZkM2ZkZWM5ZTY2ZjBlMjdjZWI4NzU5Il19LCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMS0xMS0yNFQyMDoxOTo0MC4xOTk3MDA5NDZaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIEFERCBmaWxlOjkyMzNmNmYyMjM3ZDc5NjU5YTk1MjFmN2UzOTBkZjIxN2NlYzQ5ZjFhOGFhM2ExMjE0N2JiY2ExOTU2YWNkYjkgaW4gLyAifSx7ImNyZWF0ZWQiOiIyMDIxLTExLTI0VDIwOjE5OjQwLjQ4MzM2NzU0NloiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgIENNRCBbXCIvYmluL3NoXCJdIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZF9ieSI6IkFSRyBUQVJHRVRBUkNIIiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkX2J5IjoiRU5WIEZPTz1iYXItYW1kNjQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWRfYnkiOiJSVU4gfDEgVEFSR0VUQVJDSD1hbWQ2NCAvYmluL3NoIC1jIGVjaG8gXCJmb28gJFRBUkdFVEFSQ0hcIiBcdTAwM2UgL291dCAjIGJ1aWxka2l0IiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAifV0sImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iLCJGT089YmFyLWFtZDY0Il0sIkNtZCI6WyIvYmluL3NoIl0sIk9uQnVpbGQiOm51bGx9fQ==\"}"),
"input-metadata:base::linux/arm64": stringPtr("{\"containerimage.buildinfo\":\"eyJmcm9udGVuZCI6ImRvY2tlcmZpbGUudjAiLCJzb3VyY2VzIjpbeyJ0eXBlIjoiZG9ja2VyLWltYWdlIiwicmVmIjoiYWxwaW5lIiwiYWxpYXMiOiJkb2NrZXIuaW8vbGlicmFyeS9hbHBpbmVAc2hhMjU2OmU3ZDg4ZGU3M2RiM2QzZmQ5YjJkNjNhYTdmNDQ3YTEwZmQwMjIwYjdjYmYzOTgwM2M4MDNmMmFmOWJhMjU2YjMiLCJwaW4iOiJzaGEyNTY6ZTdkODhkZTczZGIzZDNmZDliMmQ2M2FhN2Y0NDdhMTBmZDAyMjBiN2NiZjM5ODAzYzgwM2YyYWY5YmEyNTZiMyJ9XX0=\",\"containerimage.config\":\"eyJhcmNoaXRlY3R1cmUiOiJhcm02NCIsIm9zIjoibGludXgiLCJyb290ZnMiOnsidHlwZSI6ImxheWVycyIsImRpZmZfaWRzIjpbInNoYTI1Njo4ZDNhYzM0ODk5OTY0MjNmNTNkNjA4N2M4MTE4MDAwNjI2M2I3OWYyMDZkM2ZkZWM5ZTY2ZjBlMjdjZWI4NzU5Il19LCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMS0xMS0yNFQyMDoxOTo0MC4xOTk3MDA5NDZaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIEFERCBmaWxlOjkyMzNmNmYyMjM3ZDc5NjU5YTk1MjFmN2UzOTBkZjIxN2NlYzQ5ZjFhOGFhM2ExMjE0N2JiY2ExOTU2YWNkYjkgaW4gLyAifSx7ImNyZWF0ZWQiOiIyMDIxLTExLTI0VDIwOjE5OjQwLjQ4MzM2NzU0NloiLCJjcmVhdGVkX2J5IjoiL2Jpbi9zaCAtYyAjKG5vcCkgIENNRCBbXCIvYmluL3NoXCJdIiwiZW1wdHlfbGF5ZXIiOnRydWV9LHsiY3JlYXRlZF9ieSI6IkFSRyBUQVJHRVRBUkNIIiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAiLCJlbXB0eV9sYXllciI6dHJ1ZX0seyJjcmVhdGVkX2J5IjoiRU5WIEZPTz1iYXItYXJtNjQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCIsImVtcHR5X2xheWVyIjp0cnVlfSx7ImNyZWF0ZWRfYnkiOiJSVU4gfDEgVEFSR0VUQVJDSD1hcm02NCAvYmluL3NoIC1jIGVjaG8gXCJmb28gJFRBUkdFVEFSQ0hcIiBcdTAwM2UgL291dCAjIGJ1aWxka2l0IiwiY29tbWVudCI6ImJ1aWxka2l0LmRvY2tlcmZpbGUudjAifV0sImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iLCJGT089YmFyLWFybTY0Il0sIkNtZCI6WyIvYmluL3NoIl0sIk9uQnVpbGQiOm51bGx9fQ==\"}"),
"platform": stringPtr("linux/amd64,linux/arm64"),
},
want: map[string]*string{
"build-arg:bar": stringPtr("foo"),
"build-arg:foo": stringPtr("bar"),
"context:base": stringPtr("input:base"),
},
},
}
for _, tt := range cases {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, filterAttrs(tt.key, tt.attrs))
})
}
}
func TestFormat(t *testing.T) {
bi := binfotypes.BuildInfo{
Frontend: "dockerfile.v0",
@ -152,7 +278,7 @@ func TestFormat(t *testing.T) {
}
}
func TestReduceMap(t *testing.T) {
func TestReduceMapString(t *testing.T) {
cases := []struct {
name string
m1 map[string]*string
@ -193,7 +319,7 @@ func TestReduceMap(t *testing.T) {
for _, tt := range cases {
tt := tt
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, reduceMap(tt.m2, tt.m1))
require.Equal(t, tt.expected, reduceMapString(tt.m2, tt.m1))
})
}
}

View File

@ -10,7 +10,7 @@ const ImageConfigField = "moby.buildkit.buildinfo.v1"
// ImageConfig defines the structure of build dependencies
// inside image config.
type ImageConfig struct {
BuildInfo []byte `json:"moby.buildkit.buildinfo.v1,omitempty"`
BuildInfo string `json:"moby.buildkit.buildinfo.v1,omitempty"`
}
// BuildInfo defines the main structure added to image config as
@ -23,6 +23,8 @@ type BuildInfo struct {
Attrs map[string]*string `json:"attrs,omitempty"`
// Sources defines build dependencies.
Sources []Source `json:"sources,omitempty"`
// Deps defines context dependencies.
Deps map[string]BuildInfo `json:"deps,omitempty"`
}
// Source defines a build dependency.