Skip to content

Commit 5b9a9ce

Browse files
authored
Merge pull request moby#3161 from crazy-max/local-exporter-wrap
export(local): split opt
2 parents 402b1f8 + e6818cf commit 5b9a9ce

File tree

4 files changed

+248
-11
lines changed

4 files changed

+248
-11
lines changed

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,50 @@ COPY --from=builder /usr/src/app/testresult.xml .
293293
buildctl build ... --opt target=testresult --output type=local,dest=path/to/output-dir
294294
```
295295

296+
With a [multi-platform build](docs/multi-platform.md), a subfolder matching
297+
each target platform will be created in the destination directory:
298+
299+
```dockerfile
300+
FROM busybox AS build
301+
ARG TARGETOS
302+
ARG TARGETARCH
303+
RUN mkdir /out && echo foo > /out/hello-$TARGETOS-$TARGETARCH
304+
305+
FROM scratch
306+
COPY --from=build /out /
307+
```
308+
309+
```bash
310+
$ buildctl build \
311+
--frontend dockerfile.v0 \
312+
--opt platform=linux/amd64,linux/arm64 \
313+
--output type=local,dest=./bin/release
314+
315+
$ tree ./bin
316+
./bin/
317+
└── release
318+
├── linux_amd64
319+
│ └── hello-linux-amd64
320+
└── linux_arm64
321+
└── hello-linux-arm64
322+
```
323+
324+
You can set `platform-split=false` to merge files from all platforms together
325+
into same directory:
326+
327+
```bash
328+
$ buildctl build \
329+
--frontend dockerfile.v0 \
330+
--opt platform=linux/amd64,linux/arm64 \
331+
--output type=local,dest=./bin/release,platform-split=false
332+
333+
$ tree ./bin
334+
./bin/
335+
└── release
336+
├── hello-linux-amd64
337+
└── hello-linux-arm64
338+
```
339+
296340
Tar exporter is similar to local exporter but transfers the files through a tarball.
297341

298342
```bash

client/client_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ func TestIntegration(t *testing.T) {
203203
testLLBMountPerformance,
204204
testClientCustomGRPCOpts,
205205
testMultipleRecordsWithSameLayersCacheImportExport,
206+
testExportLocalNoPlatformSplit,
207+
testExportLocalNoPlatformSplitOverwrite,
206208
)
207209
}
208210

@@ -5440,6 +5442,152 @@ func testMultipleRecordsWithSameLayersCacheImportExport(t *testing.T, sb integra
54405442
ensurePruneAll(t, c, sb)
54415443
}
54425444

5445+
func testExportLocalNoPlatformSplit(t *testing.T, sb integration.Sandbox) {
5446+
integration.CheckFeatureCompat(t, sb, integration.FeatureOCIExporter, integration.FeatureMultiPlatform)
5447+
c, err := New(sb.Context(), sb.Address())
5448+
require.NoError(t, err)
5449+
defer c.Close()
5450+
5451+
platformsToTest := []string{"linux/amd64", "linux/arm64"}
5452+
frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
5453+
res := gateway.NewResult()
5454+
expPlatforms := &exptypes.Platforms{
5455+
Platforms: make([]exptypes.Platform, len(platformsToTest)),
5456+
}
5457+
for i, platform := range platformsToTest {
5458+
st := llb.Scratch().File(
5459+
llb.Mkfile("hello-"+strings.ReplaceAll(platform, "/", "-"), 0600, []byte(platform)),
5460+
)
5461+
5462+
def, err := st.Marshal(ctx)
5463+
if err != nil {
5464+
return nil, err
5465+
}
5466+
5467+
r, err := c.Solve(ctx, gateway.SolveRequest{
5468+
Definition: def.ToPB(),
5469+
})
5470+
if err != nil {
5471+
return nil, err
5472+
}
5473+
5474+
ref, err := r.SingleRef()
5475+
if err != nil {
5476+
return nil, err
5477+
}
5478+
5479+
_, err = ref.ToState()
5480+
if err != nil {
5481+
return nil, err
5482+
}
5483+
res.AddRef(platform, ref)
5484+
5485+
expPlatforms.Platforms[i] = exptypes.Platform{
5486+
ID: platform,
5487+
Platform: platforms.MustParse(platform),
5488+
}
5489+
}
5490+
dt, err := json.Marshal(expPlatforms)
5491+
if err != nil {
5492+
return nil, err
5493+
}
5494+
res.AddMeta(exptypes.ExporterPlatformsKey, dt)
5495+
5496+
return res, nil
5497+
}
5498+
5499+
destDir := t.TempDir()
5500+
_, err = c.Build(sb.Context(), SolveOpt{
5501+
Exports: []ExportEntry{
5502+
{
5503+
Type: ExporterLocal,
5504+
OutputDir: destDir,
5505+
Attrs: map[string]string{
5506+
"platform-split": "false",
5507+
},
5508+
},
5509+
},
5510+
}, "", frontend, nil)
5511+
require.NoError(t, err)
5512+
5513+
dt, err := os.ReadFile(filepath.Join(destDir, "hello-linux-amd64"))
5514+
require.NoError(t, err)
5515+
require.Equal(t, "linux/amd64", string(dt))
5516+
5517+
dt, err = os.ReadFile(filepath.Join(destDir, "hello-linux-arm64"))
5518+
require.NoError(t, err)
5519+
require.Equal(t, "linux/arm64", string(dt))
5520+
}
5521+
5522+
func testExportLocalNoPlatformSplitOverwrite(t *testing.T, sb integration.Sandbox) {
5523+
integration.CheckFeatureCompat(t, sb, integration.FeatureOCIExporter, integration.FeatureMultiPlatform)
5524+
c, err := New(sb.Context(), sb.Address())
5525+
require.NoError(t, err)
5526+
defer c.Close()
5527+
5528+
platformsToTest := []string{"linux/amd64", "linux/arm64"}
5529+
frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
5530+
res := gateway.NewResult()
5531+
expPlatforms := &exptypes.Platforms{
5532+
Platforms: make([]exptypes.Platform, len(platformsToTest)),
5533+
}
5534+
for i, platform := range platformsToTest {
5535+
st := llb.Scratch().File(
5536+
llb.Mkfile("hello-linux", 0600, []byte(platform)),
5537+
)
5538+
5539+
def, err := st.Marshal(ctx)
5540+
if err != nil {
5541+
return nil, err
5542+
}
5543+
5544+
r, err := c.Solve(ctx, gateway.SolveRequest{
5545+
Definition: def.ToPB(),
5546+
})
5547+
if err != nil {
5548+
return nil, err
5549+
}
5550+
5551+
ref, err := r.SingleRef()
5552+
if err != nil {
5553+
return nil, err
5554+
}
5555+
5556+
_, err = ref.ToState()
5557+
if err != nil {
5558+
return nil, err
5559+
}
5560+
res.AddRef(platform, ref)
5561+
5562+
expPlatforms.Platforms[i] = exptypes.Platform{
5563+
ID: platform,
5564+
Platform: platforms.MustParse(platform),
5565+
}
5566+
}
5567+
dt, err := json.Marshal(expPlatforms)
5568+
if err != nil {
5569+
return nil, err
5570+
}
5571+
res.AddMeta(exptypes.ExporterPlatformsKey, dt)
5572+
5573+
return res, nil
5574+
}
5575+
5576+
destDir := t.TempDir()
5577+
_, err = c.Build(sb.Context(), SolveOpt{
5578+
Exports: []ExportEntry{
5579+
{
5580+
Type: ExporterLocal,
5581+
OutputDir: destDir,
5582+
Attrs: map[string]string{
5583+
"platform-split": "false",
5584+
},
5585+
},
5586+
},
5587+
}, "", frontend, nil)
5588+
require.Error(t, err)
5589+
}
5590+
54435591
func readFileInImage(ctx context.Context, t *testing.T, c *Client, ref, path string) ([]byte, error) {
54445592
def, err := llb.Image(ref).Marshal(ctx)
54455593
if err != nil {

exporter/local/export.go

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"os"
66
"strings"
7+
"sync"
78
"time"
89

910
"github.com/moby/buildkit/cache"
@@ -92,6 +93,9 @@ func (e *localExporterInstance) Export(ctx context.Context, inp *exporter.Source
9293

9394
now := time.Now().Truncate(time.Second)
9495

96+
visitedPath := map[string]string{}
97+
var visitedMu sync.Mutex
98+
9599
export := func(ctx context.Context, k string, ref cache.ImmutableRef, attestations []exporter.Attestation) func() error {
96100
return func() error {
97101
outputFS, cleanup, err := CreateFS(ctx, sessionID, k, ref, attestations, now, e.opts)
@@ -102,20 +106,43 @@ func (e *localExporterInstance) Export(ctx context.Context, inp *exporter.Source
102106
defer cleanup()
103107
}
104108

109+
if !e.opts.PlatformSplit {
110+
// check for duplicate paths
111+
err = outputFS.Walk(ctx, func(p string, fi os.FileInfo, err error) error {
112+
if fi.IsDir() {
113+
return nil
114+
}
115+
if err != nil && !errors.Is(err, os.ErrNotExist) {
116+
return err
117+
}
118+
visitedMu.Lock()
119+
defer visitedMu.Unlock()
120+
if vp, ok := visitedPath[p]; ok {
121+
return errors.Errorf("cannot overwrite %s from %s with %s when split option is disabled", p, vp, k)
122+
}
123+
visitedPath[p] = k
124+
return nil
125+
})
126+
if err != nil {
127+
return err
128+
}
129+
}
130+
105131
lbl := "copying files"
106132
if isMap {
107133
lbl += " " + k
108-
st := fstypes.Stat{
109-
Mode: uint32(os.ModeDir | 0755),
110-
Path: strings.Replace(k, "/", "_", -1),
111-
}
112-
if e.opts.Epoch != nil {
113-
st.ModTime = e.opts.Epoch.UnixNano()
114-
}
115-
116-
outputFS, err = fsutil.SubDirFS([]fsutil.Dir{{FS: outputFS, Stat: st}})
117-
if err != nil {
118-
return err
134+
if e.opts.PlatformSplit {
135+
st := fstypes.Stat{
136+
Mode: uint32(os.ModeDir | 0755),
137+
Path: strings.Replace(k, "/", "_", -1),
138+
}
139+
if e.opts.Epoch != nil {
140+
st.ModTime = e.opts.Epoch.UnixNano()
141+
}
142+
outputFS, err = fsutil.SubDirFS([]fsutil.Dir{{FS: outputFS, Stat: st}})
143+
if err != nil {
144+
return err
145+
}
119146
}
120147
}
121148

exporter/local/fs.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ package local
33
import (
44
"context"
55
"encoding/json"
6+
"fmt"
67
"io"
78
"io/fs"
89
"os"
910
"path"
1011
"strconv"
12+
"strings"
1113
"time"
1214

1315
"github.com/docker/docker/pkg/idtools"
@@ -28,15 +30,20 @@ import (
2830

2931
const (
3032
keyAttestationPrefix = "attestation-prefix"
33+
// keyPlatformSplit is an exporter option which can be used to split result
34+
// in subfolders when multiple platform references are exported.
35+
keyPlatformSplit = "platform-split"
3136
)
3237

3338
type CreateFSOpts struct {
3439
Epoch *time.Time
3540
AttestationPrefix string
41+
PlatformSplit bool
3642
}
3743

3844
func (c *CreateFSOpts) Load(opt map[string]string) (map[string]string, error) {
3945
rest := make(map[string]string)
46+
c.PlatformSplit = true
4047

4148
var err error
4249
c.Epoch, opt, err = epoch.ParseExporterAttrs(opt)
@@ -48,6 +55,12 @@ func (c *CreateFSOpts) Load(opt map[string]string) (map[string]string, error) {
4855
switch k {
4956
case keyAttestationPrefix:
5057
c.AttestationPrefix = v
58+
case keyPlatformSplit:
59+
b, err := strconv.ParseBool(v)
60+
if err != nil {
61+
return nil, errors.Wrapf(err, "non-bool value for %s: %s", keyPlatformSplit, v)
62+
}
63+
c.PlatformSplit = b
5164
default:
5265
rest[k] = v
5366
}
@@ -164,6 +177,11 @@ func CreateFS(ctx context.Context, sessionID string, k string, ref cache.Immutab
164177
}
165178

166179
name := opt.AttestationPrefix + path.Base(attestations[i].Path)
180+
if !opt.PlatformSplit {
181+
nameExt := path.Ext(name)
182+
namBase := strings.TrimSuffix(name, nameExt)
183+
name = fmt.Sprintf("%s.%s%s", namBase, strings.Replace(k, "/", "_", -1), nameExt)
184+
}
167185
if _, ok := names[name]; ok {
168186
return nil, nil, errors.Errorf("duplicate attestation path name %s", name)
169187
}

0 commit comments

Comments
 (0)