Skip to content

Commit eb49527

Browse files
authored
Merge pull request #6234 from tonistiigi/llb-image-checksum
llb: add checksum option to llb.Image
2 parents 4159cde + aa003be commit eb49527

File tree

8 files changed

+157
-0
lines changed

8 files changed

+157
-0
lines changed

client/client_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
158158
testHostnameLookup,
159159
testHostnameSpecifying,
160160
testPushByDigest,
161+
testPullWithDigestCheck,
161162
testBasicInlineCacheImportExport,
162163
testExportBusyboxLocal,
163164
testBridgeNetworking,
@@ -1126,6 +1127,119 @@ func testPushByDigest(t *testing.T, sb integration.Sandbox) {
11261127
require.Greater(t, desc.Size, int64(0))
11271128
}
11281129

1130+
func testPullWithDigestCheck(t *testing.T, sb integration.Sandbox) {
1131+
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
1132+
requiresLinux(t)
1133+
c, err := New(sb.Context(), sb.Address())
1134+
require.NoError(t, err)
1135+
defer c.Close()
1136+
1137+
registry, err := sb.NewRegistry()
1138+
if errors.Is(err, integration.ErrRequirements) {
1139+
t.Skip(err.Error())
1140+
}
1141+
require.NoError(t, err)
1142+
1143+
st := llb.Scratch().File(llb.Mkfile("foo", 0600, []byte("data1")))
1144+
1145+
def, err := st.Marshal(sb.Context())
1146+
require.NoError(t, err)
1147+
1148+
name1 := registry + "/foo/bar:v1.0.0"
1149+
1150+
resp, err := c.Solve(sb.Context(), def, SolveOpt{
1151+
Exports: []ExportEntry{
1152+
{
1153+
Type: "image",
1154+
Attrs: map[string]string{
1155+
"name": name1,
1156+
"push": "true",
1157+
},
1158+
},
1159+
},
1160+
}, nil)
1161+
require.NoError(t, err)
1162+
1163+
dgst1Str := resp.ExporterResponse[exptypes.ExporterImageDigestKey]
1164+
dgst1, err := digest.Parse(dgst1Str)
1165+
require.NoError(t, err)
1166+
1167+
st = llb.Scratch().File(llb.Mkfile("foo", 0600, []byte("data2")))
1168+
1169+
def, err = st.Marshal(sb.Context())
1170+
require.NoError(t, err)
1171+
1172+
name2 := registry + "/foo/bar:v2.0.0"
1173+
1174+
resp, err = c.Solve(sb.Context(), def, SolveOpt{
1175+
Exports: []ExportEntry{
1176+
{
1177+
Type: "image",
1178+
Attrs: map[string]string{
1179+
"name": name2,
1180+
"push": "true",
1181+
},
1182+
},
1183+
},
1184+
}, nil)
1185+
require.NoError(t, err)
1186+
1187+
dgst2str := resp.ExporterResponse[exptypes.ExporterImageDigestKey]
1188+
dgst2, err := digest.Parse(dgst2str)
1189+
require.NoError(t, err)
1190+
1191+
require.NotEqual(t, dgst1, dgst2)
1192+
1193+
// if digest is set in ref then pull happens only by the digest
1194+
st = llb.Image(name2 + "@" + dgst1.String())
1195+
def, err = st.Marshal(sb.Context())
1196+
require.NoError(t, err)
1197+
1198+
destDir := t.TempDir()
1199+
1200+
_, err = c.Solve(sb.Context(), def, SolveOpt{
1201+
Exports: []ExportEntry{
1202+
{
1203+
Type: ExporterLocal,
1204+
OutputDir: destDir,
1205+
},
1206+
},
1207+
}, nil)
1208+
require.NoError(t, err)
1209+
1210+
dt, err := os.ReadFile(filepath.Join(destDir, "foo"))
1211+
require.NoError(t, err)
1212+
require.Equal(t, "data1", string(dt))
1213+
1214+
// if digest is set by checksum then pull happens by tag
1215+
st = llb.Image(name2, llb.WithImageChecksum(dgst2))
1216+
def, err = st.Marshal(sb.Context())
1217+
require.NoError(t, err)
1218+
destDir = t.TempDir()
1219+
_, err = c.Solve(sb.Context(), def, SolveOpt{
1220+
Exports: []ExportEntry{
1221+
{
1222+
Type: ExporterLocal,
1223+
OutputDir: destDir,
1224+
},
1225+
},
1226+
}, nil)
1227+
require.NoError(t, err)
1228+
1229+
dt, err = os.ReadFile(filepath.Join(destDir, "foo"))
1230+
require.NoError(t, err)
1231+
require.Equal(t, "data2", string(dt))
1232+
1233+
// if checksum doesn't match then pull fails
1234+
st = llb.Image(name2, llb.WithImageChecksum(dgst1))
1235+
def, err = st.Marshal(sb.Context())
1236+
require.NoError(t, err)
1237+
1238+
_, err = c.Solve(sb.Context(), def, SolveOpt{}, nil)
1239+
require.Error(t, err)
1240+
require.Contains(t, err.Error(), fmt.Sprintf("image digest %s for %s does not match expected checksum %s", dgst2, name2, dgst1))
1241+
}
1242+
11291243
func testSecurityMode(t *testing.T, sb integration.Sandbox) {
11301244
integration.SkipOnPlatform(t, "windows")
11311245
workers.CheckFeatureCompat(t, sb, workers.FeatureSecurityMode)

client/llb/resolver.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package llb
22

33
import (
44
"github.com/moby/buildkit/client/llb/sourceresolver"
5+
digest "github.com/opencontainers/go-digest"
56
)
67

78
// WithMetaResolver adds a metadata resolver to an image
@@ -26,5 +27,11 @@ func WithLayerLimit(l int) ImageOption {
2627
})
2728
}
2829

30+
func WithImageChecksum(dgst digest.Digest) ImageOption {
31+
return imageOptionFunc(func(ii *ImageInfo) {
32+
ii.checksum = dgst
33+
})
34+
}
35+
2936
// ImageMetaResolver can resolve image config metadata from a reference
3037
type ImageMetaResolver = sourceresolver.ImageMetaResolver

client/llb/source.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ func Image(ref string, opts ...ImageOption) State {
130130
addCap(&info.Constraints, pb.CapSourceImageLayerLimit)
131131
}
132132

133+
if info.checksum != "" {
134+
attrs[pb.AttrImageChecksum] = info.checksum.String()
135+
addCap(&info.Constraints, pb.CapSourceImageChecksum)
136+
}
137+
133138
src := NewSource("docker-image://"+ref, attrs, info.Constraints) // controversial
134139
if err != nil {
135140
src.err = err
@@ -227,6 +232,7 @@ type ImageInfo struct {
227232
resolveDigest bool
228233
resolveMode ResolveMode
229234
layerLimit *int
235+
checksum digest.Digest
230236
RecordType string
231237
}
232238

@@ -623,11 +629,18 @@ func OCILayerLimit(limit int) OCILayoutOption {
623629
})
624630
}
625631

632+
func OCIChecksum(dgst digest.Digest) OCILayoutOption {
633+
return ociLayoutOptionFunc(func(oi *OCILayoutInfo) {
634+
oi.checksum = dgst
635+
})
636+
}
637+
626638
type OCILayoutInfo struct {
627639
constraintsWrapper
628640
sessionID string
629641
storeID string
630642
layerLimit *int
643+
checksum digest.Digest
631644
}
632645

633646
type DiffType string

solver/pb/attr.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const AttrImageResolveModeForcePull = "pull"
3434
const AttrImageResolveModePreferLocal = "local"
3535
const AttrImageRecordType = "image.recordtype"
3636
const AttrImageLayerLimit = "image.layerlimit"
37+
const AttrImageChecksum = "image.checksum"
3738

3839
const AttrOCILayoutSessionID = "oci.session"
3940
const AttrOCILayoutStoreID = "oci.store"

solver/pb/caps.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const (
1212
CapSourceImage apicaps.CapID = "source.image"
1313
CapSourceImageResolveMode apicaps.CapID = "source.image.resolvemode"
1414
CapSourceImageLayerLimit apicaps.CapID = "source.image.layerlimit"
15+
CapSourceImageChecksum apicaps.CapID = "source.image.checksum"
1516

1617
CapSourceLocal apicaps.CapID = "source.local"
1718
CapSourceLocalUnique apicaps.CapID = "source.local.unique"
@@ -128,6 +129,12 @@ func init() {
128129
Status: apicaps.CapStatusExperimental,
129130
})
130131

132+
Caps.Init(apicaps.Cap{
133+
ID: CapSourceImageChecksum,
134+
Enabled: true,
135+
Status: apicaps.CapStatusExperimental,
136+
})
137+
131138
Caps.Init(apicaps.Cap{
132139
ID: CapSourceLocal,
133140
Enabled: true,

source/containerimage/identifier.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ type ImageIdentifier struct {
1919
ResolveMode resolver.ResolveMode
2020
RecordType client.UsageRecordType
2121
LayerLimit *int
22+
Checksum digest.Digest
2223
}
2324

2425
func NewImageIdentifier(str string) (*ImageIdentifier, error) {

source/containerimage/pull.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type puller struct {
4646
Ref string
4747
SessionManager *session.Manager
4848
layerLimit *int
49+
checksum digest.Digest
4950
vtx solver.Vertex
5051
ResolverType
5152
store sourceresolver.ResolveImageConfigOptStore
@@ -130,6 +131,10 @@ func (p *puller) CacheKey(ctx context.Context, g session.Group, index int) (cach
130131
return struct{}{}, err
131132
}
132133

134+
if p.checksum != "" && p.manifest.MainManifestDesc.Digest != p.checksum {
135+
return struct{}{}, errors.Errorf("image digest %s for %s does not match expected checksum %s", p.manifest.MainManifestDesc.Digest, p.Ref, p.checksum)
136+
}
137+
133138
if ll := p.layerLimit; ll != nil {
134139
if *ll > len(p.manifest.Descriptors) {
135140
return struct{}{}, errors.Errorf("layer limit %d is greater than the number of layers in the image %d", *ll, len(p.manifest.Descriptors))

source/containerimage/source.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ func (is *Source) Resolve(ctx context.Context, id source.Identifier, sm *session
9393
ref reference.Spec
9494
store sourceresolver.ResolveImageConfigOptStore
9595
layerLimit *int
96+
checksum digest.Digest
9697
)
9798
switch is.ResolverType {
9899
case ResolverTypeRegistry:
@@ -108,6 +109,7 @@ func (is *Source) Resolve(ctx context.Context, id source.Identifier, sm *session
108109
recordType = imageIdentifier.RecordType
109110
ref = imageIdentifier.Reference
110111
layerLimit = imageIdentifier.LayerLimit
112+
checksum = imageIdentifier.Checksum
111113
case ResolverTypeOCILayout:
112114
ociIdentifier, ok := id.(*OCIIdentifier)
113115
if !ok {
@@ -146,6 +148,7 @@ func (is *Source) Resolve(ctx context.Context, id source.Identifier, sm *session
146148
vtx: vtx,
147149
store: store,
148150
layerLimit: layerLimit,
151+
checksum: checksum,
149152
}
150153
return p, nil
151154
}
@@ -245,6 +248,12 @@ func (is *Source) registryIdentifier(ref string, attrs map[string]string, platfo
245248
return nil, errors.Errorf("invalid layer limit %s", v)
246249
}
247250
id.LayerLimit = &l
251+
case pb.AttrImageChecksum:
252+
dgst, err := digest.Parse(v)
253+
if err != nil {
254+
return nil, errors.Wrapf(err, "invalid image checksum %s", v)
255+
}
256+
id.Checksum = dgst
248257
}
249258
}
250259

0 commit comments

Comments
 (0)