Skip to content

Commit e1967bb

Browse files
committed
dockerfile2llb: emit base image config
The base image config will be used later for avoiding applying `SOURCE_DATE_EPOCH` to the base image layers (issue 4614). The exporter stores this as the `ExporterImageBaseConfigKey` metadata. NOTE: For a multi-stage Dockerfile like below, the base image refers to `busybox`, not to `foo`: ```dockerfile FROM busybox AS foo FROM foo AS bar ``` Signed-off-by: Akihiro Suda <[email protected]>
1 parent 85e9df3 commit e1967bb

File tree

7 files changed

+76
-25
lines changed

7 files changed

+76
-25
lines changed

examples/dockerfile2llb/main.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
type buildOpt struct {
2020
target string
2121
partialImageConfigFile string
22+
baseImageConfigFile string
2223
}
2324

2425
func main() {
@@ -31,6 +32,7 @@ func xmain() error {
3132
var opt buildOpt
3233
flag.StringVar(&opt.target, "target", "", "target stage")
3334
flag.StringVar(&opt.partialImageConfigFile, "partial-image-config-file", "", "Output partial image config as a JSON file")
35+
flag.StringVar(&opt.baseImageConfigFile, "base-image-config-file", "", "Output base image config as a JSON file")
3436
flag.Parse()
3537

3638
df, err := io.ReadAll(os.Stdin)
@@ -40,7 +42,7 @@ func xmain() error {
4042

4143
caps := pb.Caps.CapSet(pb.Caps.All())
4244

43-
state, img, _, err := dockerfile2llb.Dockerfile2LLB(appcontext.Context(), df, dockerfile2llb.ConvertOpt{
45+
state, img, baseImg, _, err := dockerfile2llb.Dockerfile2LLB(appcontext.Context(), df, dockerfile2llb.ConvertOpt{
4446
MetaResolver: imagemetaresolver.Default(),
4547
LLBCaps: &caps,
4648
Config: dockerui.Config{
@@ -63,6 +65,12 @@ func xmain() error {
6365
return err
6466
}
6567
}
68+
if opt.baseImageConfigFile != "" {
69+
if err := writeJSON(opt.baseImageConfigFile, baseImg); err != nil {
70+
return err
71+
}
72+
}
73+
6674
return nil
6775
}
6876

exporter/containerimage/exptypes/types.go

+2
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ const (
1313
ExporterImageConfigKey = "containerimage.config"
1414
ExporterImageConfigDigestKey = "containerimage.config.digest"
1515
ExporterImageDescriptorKey = "containerimage.descriptor"
16+
ExporterImageBaseConfigKey = "containerimage.base.config"
1617
ExporterPlatformsKey = "refs.platforms"
1718
)
1819

1920
// KnownRefMetadataKeys are the subset of exporter keys that can be suffixed by
2021
// a platform to become platform specific
2122
var KnownRefMetadataKeys = []string{
2223
ExporterImageConfigKey,
24+
ExporterImageBaseConfigKey,
2325
}
2426

2527
type Platforms struct {

frontend/dockerfile/builder/build.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -115,34 +115,34 @@ func Build(ctx context.Context, c client.Client) (_ *client.Result, err error) {
115115

116116
scanTargets := sync.Map{}
117117

118-
rb, err := bc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (client.Reference, *dockerspec.DockerOCIImage, error) {
118+
rb, err := bc.Build(ctx, func(ctx context.Context, platform *ocispecs.Platform, idx int) (client.Reference, *dockerspec.DockerOCIImage, *dockerspec.DockerOCIImage, error) {
119119
opt := convertOpt
120120
opt.TargetPlatform = platform
121121
if idx != 0 {
122122
opt.Warn = nil
123123
}
124124

125-
st, img, scanTarget, err := dockerfile2llb.Dockerfile2LLB(ctx, src.Data, opt)
125+
st, img, baseImg, scanTarget, err := dockerfile2llb.Dockerfile2LLB(ctx, src.Data, opt)
126126
if err != nil {
127-
return nil, nil, err
127+
return nil, nil, nil, err
128128
}
129129

130130
def, err := st.Marshal(ctx)
131131
if err != nil {
132-
return nil, nil, errors.Wrapf(err, "failed to marshal LLB definition")
132+
return nil, nil, nil, errors.Wrapf(err, "failed to marshal LLB definition")
133133
}
134134

135135
r, err := c.Solve(ctx, client.SolveRequest{
136136
Definition: def.ToPB(),
137137
CacheImports: bc.CacheImports,
138138
})
139139
if err != nil {
140-
return nil, nil, err
140+
return nil, nil, nil, err
141141
}
142142

143143
ref, err := r.SingleRef()
144144
if err != nil {
145-
return nil, nil, err
145+
return nil, nil, nil, err
146146
}
147147

148148
p := platforms.DefaultSpec()
@@ -151,7 +151,7 @@ func Build(ctx context.Context, c client.Client) (_ *client.Result, err error) {
151151
}
152152
scanTargets.Store(platforms.Format(platforms.Normalize(p)), scanTarget)
153153

154-
return ref, img, nil
154+
return ref, img, baseImg, nil
155155
})
156156
if err != nil {
157157
return nil, err

frontend/dockerfile/dockerfile2llb/convert.go

+7-4
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,13 @@ type SBOMTargets struct {
7272
IgnoreCache bool
7373
}
7474

75-
func Dockerfile2LLB(ctx context.Context, dt []byte, opt ConvertOpt) (*llb.State, *dockerspec.DockerOCIImage, *SBOMTargets, error) {
75+
func Dockerfile2LLB(ctx context.Context, dt []byte, opt ConvertOpt) (st *llb.State, img, baseImg *dockerspec.DockerOCIImage, sbom *SBOMTargets, err error) {
7676
ds, err := toDispatchState(ctx, dt, opt)
7777
if err != nil {
78-
return nil, nil, nil, err
78+
return nil, nil, nil, nil, err
7979
}
8080

81-
sbom := SBOMTargets{
81+
sbom = &SBOMTargets{
8282
Core: ds.state,
8383
Extras: map[string]llb.State{},
8484
}
@@ -97,7 +97,7 @@ func Dockerfile2LLB(ctx context.Context, dt []byte, opt ConvertOpt) (*llb.State,
9797
}
9898
}
9999

100-
return &ds.state, &ds.image, &sbom, nil
100+
return &ds.state, &ds.image, ds.baseImg, sbom, nil
101101
}
102102

103103
func Dockefile2Outline(ctx context.Context, dt []byte, opt ConvertOpt) (*outline.Outline, error) {
@@ -445,6 +445,7 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
445445
if err := json.Unmarshal(dt, &img); err != nil {
446446
return errors.Wrap(err, "failed to parse image config")
447447
}
448+
d.baseImg = cloneX(&img) // immutable
448449
img.Created = nil
449450
// if there is no explicit target platform, try to match based on image config
450451
if d.platform == nil && platformOpt.implicitTarget {
@@ -507,6 +508,7 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS
507508
d.state = d.base.state
508509
d.platform = d.base.platform
509510
d.image = clone(d.base.image)
511+
d.baseImg = cloneX(d.base.baseImg)
510512
// Utilize the same path index as our base image so we propagate
511513
// the paths we use back to the base image.
512514
d.paths = d.base.paths
@@ -834,6 +836,7 @@ type dispatchState struct {
834836
platform *ocispecs.Platform
835837
stage instructions.Stage
836838
base *dispatchState
839+
baseImg *dockerspec.DockerOCIImage // immutable, unlike image
837840
noinit bool
838841
deps map[*dispatchState]instructions.Command
839842
buildArgs []instructions.KeyValuePairOptional

frontend/dockerfile/dockerfile2llb/convert_test.go

+27-11
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/moby/buildkit/frontend/dockerfile/shell"
99
"github.com/moby/buildkit/frontend/dockerui"
1010
"github.com/moby/buildkit/util/appcontext"
11+
digest "github.com/opencontainers/go-digest"
1112
"github.com/stretchr/testify/assert"
1213
)
1314

@@ -33,7 +34,7 @@ ENV FOO bar
3334
COPY f1 f2 /sub/
3435
RUN ls -l
3536
`
36-
_, _, _, err := Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
37+
_, _, _, _, err := Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
3738
assert.NoError(t, err)
3839

3940
df = `FROM scratch AS foo
@@ -42,7 +43,7 @@ FROM foo
4243
COPY --from=foo f1 /
4344
COPY --from=0 f2 /
4445
`
45-
_, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
46+
_, _, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
4647
assert.NoError(t, err)
4748

4849
df = `FROM scratch AS foo
@@ -51,14 +52,14 @@ FROM foo
5152
COPY --from=foo f1 /
5253
COPY --from=0 f2 /
5354
`
54-
_, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{
55+
_, _, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{
5556
Config: dockerui.Config{
5657
Target: "Foo",
5758
},
5859
})
5960
assert.NoError(t, err)
6061

61-
_, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{
62+
_, _, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{
6263
Config: dockerui.Config{
6364
Target: "nosuch",
6465
},
@@ -68,21 +69,21 @@ COPY --from=0 f2 /
6869
df = `FROM scratch
6970
ADD http://github.com/moby/buildkit/blob/master/README.md /
7071
`
71-
_, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
72+
_, _, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
7273
assert.NoError(t, err)
7374

7475
df = `FROM scratch
7576
COPY http://github.com/moby/buildkit/blob/master/README.md /
7677
`
77-
_, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
78+
_, _, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
7879
assert.EqualError(t, err, "source can't be a URL for COPY")
7980

8081
df = `FROM "" AS foo`
81-
_, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
82+
_, _, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
8283
assert.Error(t, err)
8384

8485
df = `FROM ${BLANK} AS foo`
85-
_, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
86+
_, _, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
8687
assert.Error(t, err)
8788
}
8889

@@ -93,7 +94,7 @@ ENV FOO bar
9394
COPY f1 f2 /sub/
9495
RUN ls -l
9596
`
96-
state, _, _, err := Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
97+
state, _, _, _, err := Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
9798
assert.NoError(t, err)
9899

99100
_, err = state.Marshal(context.TODO())
@@ -194,7 +195,7 @@ func TestDockerfileCircularDependencies(t *testing.T) {
194195
df := `FROM busybox AS stage0
195196
COPY --from=stage0 f1 /sub/
196197
`
197-
_, _, _, err := Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
198+
_, _, _, _, err := Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
198199
assert.EqualError(t, err, "circular dependency detected on stage: stage0")
199200

200201
// multiple stages with circular dependency
@@ -205,6 +206,21 @@ COPY --from=stage0 f2 /sub/
205206
FROM busybox AS stage2
206207
COPY --from=stage1 f2 /sub/
207208
`
208-
_, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
209+
_, _, _, _, err = Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
209210
assert.EqualError(t, err, "circular dependency detected on stage: stage0")
210211
}
212+
213+
func TestBaseImageConfig(t *testing.T) {
214+
df := `FROM --platform=linux/amd64 busybox:1.36.1@sha256:6d9ac9237a84afe1516540f40a0fafdc86859b2141954b4d643af7066d598b74 AS foo
215+
RUN echo foo
216+
217+
# the source image of bar is busybox, not foo
218+
FROM foo AS bar
219+
RUN echo bar
220+
`
221+
_, _, baseImg, _, err := Dockerfile2LLB(appcontext.Context(), []byte(df), ConvertOpt{})
222+
assert.NoError(t, err)
223+
t.Logf("baseImg=%+v", baseImg)
224+
assert.Equal(t, []digest.Digest{"sha256:2e112031b4b923a873c8b3d685d48037e4d5ccd967b658743d93a6e56c3064b9"}, baseImg.RootFS.DiffIDs)
225+
assert.Equal(t, "2024-01-17 21:49:12 +0000 UTC", baseImg.Created.String())
226+
}

frontend/dockerfile/dockerfile2llb/image.go

+8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ func clone(src dockerspec.DockerOCIImage) dockerspec.DockerOCIImage {
1515
return img
1616
}
1717

18+
func cloneX(src *dockerspec.DockerOCIImage) *dockerspec.DockerOCIImage {
19+
if src == nil {
20+
return nil
21+
}
22+
img := clone(*src)
23+
return &img
24+
}
25+
1826
func emptyImage(platform ocispecs.Platform) dockerspec.DockerOCIImage {
1927
img := dockerspec.DockerOCIImage{}
2028
img.Architecture = platform.Architecture

frontend/dockerui/build.go

+16-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
"golang.org/x/sync/errgroup"
1515
)
1616

17-
type BuildFunc func(ctx context.Context, platform *ocispecs.Platform, idx int) (client.Reference, *dockerspec.DockerOCIImage, error)
17+
type BuildFunc func(ctx context.Context, platform *ocispecs.Platform, idx int) (r client.Reference, img, baseImg *dockerspec.DockerOCIImage, err error)
1818

1919
func (bc *Client) Build(ctx context.Context, fn BuildFunc) (*ResultBuilder, error) {
2020
res := client.NewResult()
@@ -36,7 +36,7 @@ func (bc *Client) Build(ctx context.Context, fn BuildFunc) (*ResultBuilder, erro
3636
for i, tp := range targets {
3737
i, tp := i, tp
3838
eg.Go(func() error {
39-
ref, img, err := fn(ctx, tp, i)
39+
ref, img, baseImg, err := fn(ctx, tp, i)
4040
if err != nil {
4141
return err
4242
}
@@ -46,6 +46,14 @@ func (bc *Client) Build(ctx context.Context, fn BuildFunc) (*ResultBuilder, erro
4646
return errors.Wrapf(err, "failed to marshal image config")
4747
}
4848

49+
var baseConfig []byte
50+
if baseImg != nil {
51+
baseConfig, err = json.Marshal(baseImg)
52+
if err != nil {
53+
return errors.Wrapf(err, "failed to marshal source image config")
54+
}
55+
}
56+
4957
p := platforms.DefaultSpec()
5058
if tp != nil {
5159
p = *tp
@@ -67,9 +75,15 @@ func (bc *Client) Build(ctx context.Context, fn BuildFunc) (*ResultBuilder, erro
6775
if bc.MultiPlatformRequested {
6876
res.AddRef(k, ref)
6977
res.AddMeta(fmt.Sprintf("%s/%s", exptypes.ExporterImageConfigKey, k), config)
78+
if len(baseConfig) > 0 {
79+
res.AddMeta(fmt.Sprintf("%s/%s", exptypes.ExporterImageBaseConfigKey, k), baseConfig)
80+
}
7081
} else {
7182
res.SetRef(ref)
7283
res.AddMeta(exptypes.ExporterImageConfigKey, config)
84+
if len(baseConfig) > 0 {
85+
res.AddMeta(exptypes.ExporterImageBaseConfigKey, baseConfig)
86+
}
7387
}
7488
expPlatforms.Platforms[i] = exptypes.Platform{
7589
ID: k,

0 commit comments

Comments
 (0)