Skip to content

Commit aa003be

Browse files
committed
llb: add checksum option to llb.Image
This allows images to be pulled by tag and then checked against the digest. If digest is added directly to the image reference, then tag is ignored. Signed-off-by: Tonis Tiigi <[email protected]>
1 parent f319a77 commit aa003be

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)