Skip to content

Commit

Permalink
dockerfile: support Dockerfile.sum for pinning sources
Browse files Browse the repository at this point in the history
Dockerfile.sum is an equivalent of go.sum but s/go/Dockerfile/ .
The content is a subset of BuildInfo:
```json
{
    "sources": [
      {
        "type": "docker-image",
        "ref": "docker.io/library/alpine:latest",
        "pin": "sha256:4edbd2beb5f78b1014028f4fbb99f3237d9561100b6881aabbf5acce2c4f9454"
      },
      {
        "type": "http",
        "ref": "https://raw.githubusercontent.com/moby/buildkit/v0.10.1/README.md",
        "pin": "sha256:6e4b94fc270e708e1068be28bd3551dc6917a4fc5a61293d51bb36e6b75c4b53"
      }
    ]
}
```

When Dockerfile.sum exists in the context, the Dockerfile builder does:
- Pinning the digest of `docker-image` sources (`FROM ...`)
- Pinning the digest of `http` sources (`ADD https://...`)
- Recording the consumed Dockerfile.sum entries to the exporter response `containerimage.pin.consumption`

In the future, Dockerfile should also support `ADD git://...` and pinning its commit hash. (PR 2799)

Closes issue 2794

Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
  • Loading branch information
AkihiroSuda committed Apr 20, 2022
1 parent 65f4948 commit 9959409
Show file tree
Hide file tree
Showing 8 changed files with 375 additions and 10 deletions.
30 changes: 30 additions & 0 deletions docs/build-repro.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,33 @@ jq '.' metadata.json
"containerimage.digest": "sha256:..."
}
```

### Reproducible build with `Dockerfile.sum`

`Dockerfile.sum` is an equivalent of `go.sum` but s/go/Dockerfile/ .
`Dockerfile.sum` was introduced in the `docker/dockerfile:1.5.0` syntax.

The content of `Dockerfile.sum` is a subset of the build info structure:
```json
{
"sources": [
{
"type": "docker-image",
"ref": "docker.io/library/alpine:latest",
"pin": "sha256:4edbd2beb5f78b1014028f4fbb99f3237d9561100b6881aabbf5acce2c4f9454"
},
{
"type": "http",
"ref": "https://raw.githubusercontent.com/moby/buildkit/v0.10.1/README.md",
"pin": "sha256:6e4b94fc270e708e1068be28bd3551dc6917a4fc5a61293d51bb36e6b75c4b53"
}
]
}
```

When `Dockerfile.sum` exists in the context, the Dockerfile builder does:
- Pinning the digest of `docker-image` sources (`FROM ...`)
- Pinning the digest of `http` sources (`ADD https://...`)
- Recording the consumed entries to the build info structure (`["containerimage.buildinfo"].consumedPin`)

In the future, Dockerfile should also support `ADD git://...` and pinning its commit hash.
8 changes: 8 additions & 0 deletions exporter/containerimage/exptypes/types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package exptypes

import (
pintypes "github.com/moby/buildkit/util/pin/types"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
)

Expand All @@ -12,6 +13,7 @@ const (
ExporterImageDescriptorKey = "containerimage.descriptor"
ExporterInlineCache = "containerimage.inlinecache"
ExporterBuildInfo = "containerimage.buildinfo"
ExporterPinConsumption = "containerimage.pin.consumption"
ExporterPlatformsKey = "refs.platforms"
)

Expand All @@ -23,3 +25,9 @@ type Platform struct {
ID string
Platform ocispecs.Platform
}

// PinConsumption contains the consumed pin data.
type PinConsumption struct {
// Sources consumed.
Sources []pintypes.Source `json:"sources,omitempty"`
}
56 changes: 48 additions & 8 deletions frontend/dockerfile/builder/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
"github.com/moby/buildkit/solver/errdefs"
"github.com/moby/buildkit/solver/pb"
binfotypes "github.com/moby/buildkit/util/buildinfo/types"
"github.com/moby/buildkit/util/pin"
pintypes "github.com/moby/buildkit/util/pin/types"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
Expand Down Expand Up @@ -163,7 +165,7 @@ func Build(ctx context.Context, c client.Client) (*client.Result, error) {

name := "load build definition from " + filename

filenames := []string{filename, filename + ".dockerignore"}
filenames := []string{filename, filename + ".dockerignore", filename + ".sum"}

// dockerfile is also supported casing moby/moby#10858
if path.Base(filename) == defaultDockerfileName {
Expand Down Expand Up @@ -270,6 +272,7 @@ func Build(ctx context.Context, c client.Client) (*client.Result, error) {
var dtDockerfile []byte
var dtDockerignore []byte
var dtDockerignoreDefault []byte
var dtDockerfileSum []byte
eg.Go(func() error {
res, err := c.Solve(ctx2, client.SolveRequest{
Definition: def.ToPB(),
Expand Down Expand Up @@ -311,6 +314,17 @@ func Build(ctx context.Context, c client.Client) (*client.Result, error) {
if err == nil {
dtDockerignore = dt
}
dockerfileSumFilename := filename + ".sum"
if _, err := ref.StatFile(ctx, client.StatRequest{
Path: dockerfileSumFilename,
}); err == nil {
dtDockerfileSum, err = ref.ReadFile(ctx2, client.ReadRequest{
Filename: dockerfileSumFilename,
})
if err != nil {
return err
}
}
return nil
})
var excludes []string
Expand Down Expand Up @@ -427,9 +441,23 @@ func Build(ctx context.Context, c client.Client) (*client.Result, error) {
}
}()

var metaResolver llb.ImageMetaResolver = c
var pinApplier *pin.Applier
if dtDockerfileSum != nil {
var pinData pintypes.Pin
if err := json.Unmarshal(dtDockerfileSum, &pinData); err != nil {
return err
}
pinApplier = &pin.Applier{
Pin: pinData,
ImageMetaResolver: metaResolver,
}
metaResolver = pinApplier
}

st, img, bi, err := dockerfile2llb.Dockerfile2LLB(ctx, dtDockerfile, dockerfile2llb.ConvertOpt{
Target: opts[keyTarget],
MetaResolver: c,
MetaResolver: metaResolver,
BuildArgs: filter(opts, buildArgPrefix),
Labels: filter(opts, labelPrefix),
CacheIDNamespace: opts[keyCacheNSArg],
Expand All @@ -455,7 +483,8 @@ func Build(ctx context.Context, c client.Client) (*client.Result, error) {
}
c.Warn(ctx, defVtx, msg, warnOpts(sourceMap, location, detail, url))
},
ContextByName: contextByNameFunc(c, tp),
ContextByName: contextByNameFunc(c, metaResolver, tp),
PinApplier: pinApplier,
})

if err != nil {
Expand Down Expand Up @@ -511,6 +540,17 @@ func Build(ctx context.Context, c client.Client) (*client.Result, error) {
return err
}

if len(dtDockerfileSum) != 0 {
pinConsumed := &exptypes.PinConsumption{
Sources: pinApplier.Consumed().Sources,
}
pinConsumedJSON, err := json.Marshal(pinConsumed)
if err != nil {
return err
}
res.AddMeta(exptypes.ExporterPinConsumption, pinConsumedJSON)
}

buildinfo, err := json.Marshal(bi)
if err != nil {
return errors.Wrapf(err, "failed to marshal build info")
Expand Down Expand Up @@ -787,7 +827,7 @@ func warnOpts(sm *llb.SourceMap, r *parser.Range, detail [][]byte, url string) c
return opts
}

func contextByNameFunc(c client.Client, p *ocispecs.Platform) func(context.Context, string) (*llb.State, *dockerfile2llb.Image, *binfotypes.BuildInfo, error) {
func contextByNameFunc(c client.Client, metaResolver llb.ImageMetaResolver, p *ocispecs.Platform) func(context.Context, string) (*llb.State, *dockerfile2llb.Image, *binfotypes.BuildInfo, error) {
return func(ctx context.Context, name string) (*llb.State, *dockerfile2llb.Image, *binfotypes.BuildInfo, error) {
named, err := reference.ParseNormalizedNamed(name)
if err != nil {
Expand All @@ -801,19 +841,19 @@ func contextByNameFunc(c client.Client, p *ocispecs.Platform) func(context.Conte
}
if p != nil {
name := name + "::" + platforms.Format(platforms.Normalize(*p))
st, img, bi, err := contextByName(ctx, c, name, p)
st, img, bi, err := contextByName(ctx, c, metaResolver, name, p)
if err != nil {
return nil, nil, nil, err
}
if st != nil {
return st, img, bi, nil
}
}
return contextByName(ctx, c, name, p)
return contextByName(ctx, c, metaResolver, name, p)
}
}

func contextByName(ctx context.Context, c client.Client, name string, platform *ocispecs.Platform) (*llb.State, *dockerfile2llb.Image, *binfotypes.BuildInfo, error) {
func contextByName(ctx context.Context, c client.Client, metaResolver llb.ImageMetaResolver, name string, platform *ocispecs.Platform) (*llb.State, *dockerfile2llb.Image, *binfotypes.BuildInfo, error) {
opts := c.BuildOpts().Opts
v, ok := opts["context:"+name]
if !ok {
Expand All @@ -829,7 +869,7 @@ func contextByName(ctx context.Context, c client.Client, name string, platform *
ref := strings.TrimPrefix(vv[1], "//")
imgOpt := []llb.ImageOption{
llb.WithCustomName("[context " + name + "] " + ref),
llb.WithMetaResolver(c),
llb.WithMetaResolver(metaResolver),
}
if platform != nil {
imgOpt = append(imgOpt, llb.Platform(*platform))
Expand Down
19 changes: 18 additions & 1 deletion frontend/dockerfile/dockerfile2llb/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ import (
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/apicaps"
binfotypes "github.com/moby/buildkit/util/buildinfo/types"
"github.com/moby/buildkit/util/pin"
"github.com/moby/buildkit/util/suggest"
"github.com/moby/buildkit/util/system"
"github.com/moby/sys/signal"
digest "github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"golang.org/x/sync/errgroup"
Expand Down Expand Up @@ -68,6 +70,8 @@ type ConvertOpt struct {
Hostname string
Warn func(short, url string, detail [][]byte, location *parser.Range)
ContextByName func(context.Context, string) (*llb.State, *Image, *binfotypes.BuildInfo, error)
// PinApplier must correspond to MetaResolver when PinApplier is non-nil.
PinApplier *pin.Applier
}

func Dockerfile2LLB(ctx context.Context, dt []byte, opt ConvertOpt) (*llb.State, *Image, *binfotypes.BuildInfo, error) {
Expand Down Expand Up @@ -136,6 +140,10 @@ func Dockerfile2LLB(ctx context.Context, dt []byte, opt ConvertOpt) (*llb.State,
}
}

if opt.PinApplier != nil && opt.MetaResolver != nil && opt.PinApplier != opt.MetaResolver {
return nil, nil, nil, fmt.Errorf("opt.PinApplier must correspond to opt.MetaResolver when non-nil")
}

metaResolver := opt.MetaResolver
if metaResolver == nil {
metaResolver = imagemetaresolver.Default()
Expand Down Expand Up @@ -473,6 +481,7 @@ func Dockerfile2LLB(ctx context.Context, dt []byte, opt ConvertOpt) (*llb.State,
cgroupParent: opt.CgroupParent,
llbCaps: opt.LLBCaps,
sourceMap: opt.SourceMap,
pinApplier: opt.PinApplier,
}

if err = dispatchOnBuildTriggers(d, d.image.Config.OnBuild, opt); err != nil {
Expand Down Expand Up @@ -597,6 +606,7 @@ type dispatchOpt struct {
cgroupParent string
llbCaps *apicaps.CapSet
sourceMap *llb.SourceMap
pinApplier *pin.Applier
}

func dispatch(d *dispatchState, cmd command, opt dispatchOpt) error {
Expand Down Expand Up @@ -1019,7 +1029,14 @@ func dispatchCopy(d *dispatchState, cfg copyConfig) error {
}
}

st := llb.HTTP(src, llb.Filename(f), dfCmd(cfg.params))
var checksum digest.Digest
if cfg.opt.pinApplier != nil {
checksum, err = cfg.opt.pinApplier.HTTPChecksum(src)
if err != nil {
return err
}
}
st := llb.HTTP(src, llb.Filename(f), dfCmd(cfg.params), llb.Checksum(checksum))

opts := append([]llb.CopyOption{&llb.CopyInfo{
Mode: mode,
Expand Down
137 changes: 137 additions & 0 deletions frontend/dockerfile/dockerfile_pin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package dockerfile

import (
"encoding/base64"
"encoding/json"
"os"
"testing"

"github.com/containerd/continuity/fs/fstest"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/moby/buildkit/frontend/dockerfile/builder"
"github.com/moby/buildkit/util/testutil/integration"
"github.com/stretchr/testify/require"
)

var pinTests = integration.TestFuncs(
testPin,
)

func init() {
allTests = append(allTests, pinTests...)
}

func testPin(t *testing.T, sb integration.Sandbox) {
f := getFrontend(t, sb)
dockerfile := `
FROM alpine
ADD https://raw.githubusercontent.com/moby/buildkit/v0.10.1/README.md /README.md
`
type testCase struct {
title string
dockerfileSum string
valid bool
validateResp func(*testing.T, *client.SolveResponse)
}
testCases := []testCase{
{
title: "Valid",
dockerfileSum: `{
"sources": [
{
"type": "docker-image",
"ref": "docker.io/library/alpine:latest",
"pin": "sha256:4edbd2beb5f78b1014028f4fbb99f3237d9561100b6881aabbf5acce2c4f9454"
},
{
"type": "http",
"ref": "https://raw.githubusercontent.com/moby/buildkit/v0.10.1/README.md",
"pin": "sha256:6e4b94fc270e708e1068be28bd3551dc6917a4fc5a61293d51bb36e6b75c4b53"
}
]
}
`,
valid: true,
validateResp: func(t *testing.T, resp *client.SolveResponse) {
require.Contains(t, resp.ExporterResponse, exptypes.ExporterPinConsumption)
dtpc, err := base64.StdEncoding.DecodeString(resp.ExporterResponse[exptypes.ExporterPinConsumption])
require.NoError(t, err)
var pc exptypes.PinConsumption
err = json.Unmarshal(dtpc, &pc)
require.NoError(t, err)
require.Len(t, pc.Sources, 2)
require.Equal(t, "sha256:4edbd2beb5f78b1014028f4fbb99f3237d9561100b6881aabbf5acce2c4f9454", pc.Sources[0].Pin)
require.Equal(t, "sha256:6e4b94fc270e708e1068be28bd3551dc6917a4fc5a61293d51bb36e6b75c4b53", pc.Sources[1].Pin)
},
},
{
title: "InvalidDockerImagePin",
dockerfileSum: `{
"sources": [
{
"type": "docker-image",
"ref": "docker.io/library/alpine:latest",
"pin": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
},
{
"type": "http",
"ref": "https://raw.githubusercontent.com/moby/buildkit/v0.10.1/README.md",
"pin": "sha256:6e4b94fc270e708e1068be28bd3551dc6917a4fc5a61293d51bb36e6b75c4b53"
}
]
}
`,
valid: false,
},
{
title: "InvalidHTTPPin",
dockerfileSum: `{
"sources": [
{
"type": "docker-image",
"ref": "docker.io/library/alpine:latest",
"pin": "sha256:4edbd2beb5f78b1014028f4fbb99f3237d9561100b6881aabbf5acce2c4f9454"
},
{
"type": "http",
"ref": "https://raw.githubusercontent.com/moby/buildkit/v0.10.1/README.md",
"pin": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
]
}
`,
valid: false,
},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.title, func(t *testing.T) {
dir, err := tmpdir(
fstest.CreateFile("Dockerfile", []byte(dockerfile), 0600),
fstest.CreateFile("Dockerfile.sum", []byte(tc.dockerfileSum), 0600),
)
require.NoError(t, err)
defer os.RemoveAll(dir)

c, err := client.New(sb.Context(), sb.Address())
require.NoError(t, err)
defer c.Close()

res, err := f.Solve(sb.Context(), c, client.SolveOpt{
LocalDirs: map[string]string{
builder.DefaultLocalNameDockerfile: dir,
builder.DefaultLocalNameContext: dir,
},
}, nil)
if !tc.valid {
require.Error(t, err)
return
}
require.NoError(t, err)
if tc.validateResp != nil {
tc.validateResp(t, res)
}
})
}
}
Loading

0 comments on commit 9959409

Please sign in to comment.