Skip to content

Commit 278daa0

Browse files
committed
source: imageblob source implementation
Image blob source in LLB allows addressing a single blob from a container image registry. The difference from the image source is that image source needs to point to a manifest that internally points to an array of layer blobs that are all extracted on top of each other to form a root FS. Contrary, image blob points to a single blob that is not extracted but downloaded as a single file into an empty snapshot, similarily how the HTTP source works. The main use case for this source is to pin snapshots of HTTP URLs, upload the downloaded blob into container registry, and then use a source policy to map a HTTP URL (whose content might be changed) to the copy of the source as image blob to ensure immutability. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
1 parent 4c89091 commit 278daa0

11 files changed

Lines changed: 711 additions & 23 deletions

File tree

client/client_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,15 @@ import (
4949
"github.com/moby/buildkit/session/secrets/secretsprovider"
5050
"github.com/moby/buildkit/session/sshforward/sshprovider"
5151
"github.com/moby/buildkit/solver/errdefs"
52+
"github.com/moby/buildkit/solver/llbsolver/provenance"
5253
"github.com/moby/buildkit/solver/pb"
5354
"github.com/moby/buildkit/solver/result"
5455
"github.com/moby/buildkit/sourcepolicy"
5556
sourcepolicypb "github.com/moby/buildkit/sourcepolicy/pb"
5657
"github.com/moby/buildkit/util/attestation"
5758
"github.com/moby/buildkit/util/contentutil"
5859
"github.com/moby/buildkit/util/entitlements"
60+
"github.com/moby/buildkit/util/purl"
5961
"github.com/moby/buildkit/util/testutil"
6062
containerdutil "github.com/moby/buildkit/util/testutil/containerd"
6163
"github.com/moby/buildkit/util/testutil/echoserver"
@@ -209,6 +211,7 @@ func TestIntegration(t *testing.T) {
209211
testSnapshotWithMultipleBlobs,
210212
testExportLocalNoPlatformSplit,
211213
testExportLocalNoPlatformSplitOverwrite,
214+
testImageBlobSource,
212215
)
213216
}
214217

@@ -9406,6 +9409,107 @@ func testMountStubsTimestamp(t *testing.T, sb integration.Sandbox) {
94069409
}
94079410
}
94089411

9412+
func testImageBlobSource(t *testing.T, sb integration.Sandbox) {
9413+
workers.CheckFeatureCompat(t, sb, workers.FeatureDirectPush)
9414+
requiresLinux(t)
9415+
c, err := New(sb.Context(), sb.Address())
9416+
require.NoError(t, err)
9417+
defer c.Close()
9418+
9419+
registry, err := sb.NewRegistry()
9420+
if errors.Is(err, integration.ErrRequirements) {
9421+
t.Skip(err.Error())
9422+
}
9423+
require.NoError(t, err)
9424+
9425+
st := llb.Image("alpine")
9426+
9427+
def, err := st.Marshal(sb.Context())
9428+
require.NoError(t, err)
9429+
9430+
name := registry + "/foo/blobtest:img"
9431+
9432+
_, err = c.Solve(sb.Context(), def, SolveOpt{
9433+
Exports: []ExportEntry{
9434+
{
9435+
Type: "image",
9436+
Attrs: map[string]string{
9437+
"name": name,
9438+
"push": "true",
9439+
},
9440+
},
9441+
},
9442+
}, nil)
9443+
require.NoError(t, err)
9444+
9445+
desc, provider, err := contentutil.ProviderFromRef(name)
9446+
require.NoError(t, err)
9447+
9448+
imgs, err := testutil.ReadImages(sb.Context(), provider, desc)
9449+
require.NoError(t, err)
9450+
9451+
require.Equal(t, 1, len(imgs.Images))
9452+
mfst := imgs.Images[0].Manifest
9453+
require.GreaterOrEqual(t, len(mfst.Layers), 1)
9454+
9455+
l := mfst.Layers[0]
9456+
9457+
blob := llb.ImageBlob(registry+"/foo/blobtest@"+l.Digest.String(), llb.Filename("layer.tar.gz"), llb.Chown(123, 456))
9458+
st = llb.Image("alpine").Run(llb.Shlex(`sh -c 'sha256sum /layers/layer.tar.gz | cut -d" " -f0 > /out/checksum && stat -c "%u-%g-%s" /layers/layer.tar.gz > /out/stat'`), llb.AddMount("/layers", blob, llb.Readonly)).AddMount("/out", llb.Scratch())
9459+
9460+
def, err = st.Marshal(sb.Context())
9461+
require.NoError(t, err)
9462+
9463+
destDir := t.TempDir()
9464+
9465+
_, err = c.Solve(sb.Context(), def, SolveOpt{
9466+
FrontendAttrs: map[string]string{
9467+
"attest:provenance": "",
9468+
},
9469+
Exports: []ExportEntry{
9470+
{
9471+
Type: ExporterLocal,
9472+
OutputDir: destDir,
9473+
},
9474+
},
9475+
}, nil)
9476+
require.NoError(t, err)
9477+
9478+
dt, err := os.ReadFile(filepath.Join(destDir, "stat"))
9479+
require.NoError(t, err)
9480+
9481+
require.Equal(t, "123-456-"+strconv.FormatInt(l.Size, 10), strings.TrimSpace(string(dt)))
9482+
9483+
dt, err = os.ReadFile(filepath.Join(destDir, "checksum"))
9484+
require.NoError(t, err)
9485+
9486+
require.Equal(t, l.Digest.Hex(), strings.TrimSpace(string(dt)))
9487+
9488+
provDt, err := os.ReadFile(filepath.Join(destDir, "provenance.json"))
9489+
require.NoError(t, err)
9490+
9491+
type stmtT struct {
9492+
Predicate provenance.ProvenancePredicate `json:"predicate"`
9493+
}
9494+
var stmt stmtT
9495+
9496+
err = json.Unmarshal(provDt, &stmt)
9497+
require.NoError(t, err)
9498+
9499+
expectedName, err := purl.RefToPURL("docker-blob", registry+"/foo/blobtest@"+l.Digest.String(), nil)
9500+
require.NoError(t, err)
9501+
9502+
found := false
9503+
for _, m := range stmt.Predicate.Materials {
9504+
if m.URI == expectedName {
9505+
found = true
9506+
require.Equal(t, m.Digest["sha256"], l.Digest.Hex())
9507+
break
9508+
}
9509+
}
9510+
require.True(t, found, "expected to find %q in %+v", expectedName, stmt.Predicate.Materials)
9511+
}
9512+
94099513
func ensureFile(t *testing.T, path string) {
94109514
st, err := os.Stat(path)
94119515
require.NoError(t, err, "expected file at %s", path)

client/llb/source.go

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,67 @@ func (s *SourceOp) Inputs() []Output {
9292
return nil
9393
}
9494

95+
type ImageBlobInfo struct {
96+
constraintsWrapper
97+
fileinfoWrapper
98+
}
99+
100+
type ImageBlobOption interface {
101+
SetImageBlobOption(*ImageBlobInfo)
102+
}
103+
104+
type FileInfoOption interface {
105+
HTTPOption
106+
ImageBlobOption
107+
}
108+
109+
func ImageBlob(ref string, opts ...ImageBlobOption) State {
110+
bi := &ImageBlobInfo{}
111+
for _, o := range opts {
112+
o.SetImageBlobOption(bi)
113+
}
114+
attrs := map[string]string{}
115+
116+
if bi.Filename != "" {
117+
attrs[pb.AttrHTTPFilename] = bi.Filename
118+
}
119+
if bi.Perm != 0 {
120+
attrs[pb.AttrHTTPPerm] = "0" + strconv.FormatInt(int64(bi.Perm), 8)
121+
}
122+
if bi.UID != 0 {
123+
attrs[pb.AttrHTTPUID] = strconv.Itoa(bi.UID)
124+
}
125+
if bi.GID != 0 {
126+
attrs[pb.AttrHTTPGID] = strconv.Itoa(bi.GID)
127+
}
128+
129+
addCap(&bi.Constraints, pb.CapSourceImageBlob)
130+
131+
var digested reference.Digested
132+
133+
r, err := reference.ParseNormalizedNamed(ref)
134+
if err == nil {
135+
if _, tagged := r.(reference.Tagged); tagged {
136+
err = errors.Errorf("tagged image reference not allowed for blob reference")
137+
} else if ref, ok := r.(reference.Digested); !ok {
138+
err = errors.Errorf("checksum required in blob reference")
139+
} else {
140+
digested = ref
141+
}
142+
}
143+
144+
repoName := "invalid"
145+
if digested != nil {
146+
repoName = digested.String()
147+
}
148+
149+
source := NewSource("docker-image-blob://"+repoName, attrs, bi.Constraints)
150+
if err != nil {
151+
source.err = err
152+
}
153+
return NewState(source.Output())
154+
}
155+
95156
// Image returns a state that represents a docker image in a registry.
96157
// Example:
97158
//
@@ -591,15 +652,33 @@ func HTTP(url string, opts ...HTTPOption) State {
591652
return NewState(source.Output())
592653
}
593654

594-
type HTTPInfo struct {
595-
constraintsWrapper
596-
Checksum digest.Digest
655+
type fileInfo struct {
597656
Filename string
598657
Perm int
599658
UID int
600659
GID int
601660
}
602661

662+
type fileinfoWrapper struct {
663+
fileInfo
664+
}
665+
666+
type fileInfoOptFunc func(f *fileInfo)
667+
668+
func (fn fileInfoOptFunc) SetHTTPOption(hi *HTTPInfo) {
669+
fn(&hi.fileInfo)
670+
}
671+
672+
func (fn fileInfoOptFunc) SetImageBlobOption(ib *ImageBlobInfo) {
673+
fn(&ib.fileInfo)
674+
}
675+
676+
type HTTPInfo struct {
677+
constraintsWrapper
678+
fileinfoWrapper
679+
Checksum digest.Digest
680+
}
681+
603682
type HTTPOption interface {
604683
SetHTTPOption(*HTTPInfo)
605684
}
@@ -616,22 +695,22 @@ func Checksum(dgst digest.Digest) HTTPOption {
616695
})
617696
}
618697

619-
func Chmod(perm os.FileMode) HTTPOption {
620-
return httpOptionFunc(func(hi *HTTPInfo) {
621-
hi.Perm = int(perm) & 0777
698+
func Chmod(perm os.FileMode) FileInfoOption {
699+
return fileInfoOptFunc(func(fi *fileInfo) {
700+
fi.Perm = int(perm) & 0777
622701
})
623702
}
624703

625-
func Filename(name string) HTTPOption {
626-
return httpOptionFunc(func(hi *HTTPInfo) {
627-
hi.Filename = name
704+
func Filename(name string) FileInfoOption {
705+
return fileInfoOptFunc(func(fi *fileInfo) {
706+
fi.Filename = name
628707
})
629708
}
630709

631-
func Chown(uid, gid int) HTTPOption {
632-
return httpOptionFunc(func(hi *HTTPInfo) {
633-
hi.UID = uid
634-
hi.GID = gid
710+
func Chown(uid, gid int) FileInfoOption {
711+
return fileInfoOptFunc(func(fi *fileInfo) {
712+
fi.UID = uid
713+
fi.GID = gid
635714
})
636715
}
637716

client/llb/state_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,55 @@ func TestFormattingPatterns(t *testing.T) {
5555
assert.Equal(t, "/foo/bar1", getDirHelper(t, s2))
5656
}
5757

58+
func TestImageBlobInvalid(t *testing.T) {
59+
t.Parallel()
60+
ctx := context.TODO()
61+
62+
dgst := digest.FromBytes([]byte("foo"))
63+
64+
s := ImageBlob("myuser/myrepo:foo@" + string(dgst))
65+
_, err := s.Marshal(ctx)
66+
require.Error(t, err)
67+
require.Contains(t, err.Error(), "tagged image reference not allowed")
68+
69+
s = ImageBlob("myuser/myrepo")
70+
_, err = s.Marshal(ctx)
71+
require.Error(t, err)
72+
require.Contains(t, err.Error(), "checksum required in blob reference")
73+
74+
s = ImageBlob("myuser/myrepo@sha256:invalid")
75+
_, err = s.Marshal(ctx)
76+
require.Error(t, err)
77+
require.Contains(t, err.Error(), "invalid reference format")
78+
}
79+
80+
func TestImageBlobSource(t *testing.T) {
81+
t.Parallel()
82+
ctx := context.TODO()
83+
84+
blobDgst := digest.FromBytes([]byte("foo"))
85+
86+
s := ImageBlob("myuser/myrepo@" + string(blobDgst))
87+
def, err := s.Marshal(ctx)
88+
require.NoError(t, err)
89+
90+
m, arr := parseDef(t, def.Def)
91+
_ = m
92+
require.Equal(t, 2, len(arr))
93+
94+
dgst, idx := last(t, arr)
95+
require.Equal(t, 0, idx)
96+
97+
vtx, ok := m[dgst]
98+
require.Equal(t, true, ok)
99+
100+
src, ok := vtx.Op.(*pb.Op_Source)
101+
require.Equal(t, true, ok)
102+
require.Nil(t, vtx.Platform)
103+
104+
require.Equal(t, "docker-image-blob://docker.io/myuser/myrepo@"+string(blobDgst), src.Source.Identifier)
105+
}
106+
58107
func TestStateSourceMapMarshal(t *testing.T) {
59108
t.Parallel()
60109

solver/llbsolver/provenance/capture.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ type ImageSource struct {
2020
Local bool
2121
}
2222

23+
type ImageBlobSource struct {
24+
Ref string
25+
Digest digest.Digest
26+
}
27+
2328
type GitSource struct {
2429
URL string
2530
Commit string
@@ -45,10 +50,11 @@ type SSH struct {
4550
}
4651

4752
type Sources struct {
48-
Images []ImageSource
49-
Git []GitSource
50-
HTTP []HTTPSource
51-
Local []LocalSource
53+
Images []ImageSource
54+
ImageBlobs []ImageBlobSource
55+
Git []GitSource
56+
HTTP []HTTPSource
57+
Local []LocalSource
5258
}
5359

5460
type Capture struct {
@@ -97,6 +103,9 @@ func (c *Capture) Sort() {
97103
sort.Slice(c.Sources.Images, func(i, j int) bool {
98104
return c.Sources.Images[i].Ref < c.Sources.Images[j].Ref
99105
})
106+
sort.Slice(c.Sources.ImageBlobs, func(i, j int) bool {
107+
return c.Sources.ImageBlobs[i].Ref < c.Sources.ImageBlobs[j].Ref
108+
})
100109
sort.Slice(c.Sources.Local, func(i, j int) bool {
101110
return c.Sources.Local[i].Name < c.Sources.Local[j].Name
102111
})
@@ -161,6 +170,15 @@ func (c *Capture) AddImage(i ImageSource) {
161170
c.Sources.Images = append(c.Sources.Images, i)
162171
}
163172

173+
func (c *Capture) AddImageBlob(i ImageBlobSource) {
174+
for _, v := range c.Sources.ImageBlobs {
175+
if v.Ref == i.Ref {
176+
return
177+
}
178+
}
179+
c.Sources.ImageBlobs = append(c.Sources.ImageBlobs, i)
180+
}
181+
164182
func (c *Capture) AddLocal(l LocalSource) {
165183
for _, v := range c.Sources.Local {
166184
if v.Name == l.Name {

0 commit comments

Comments
 (0)