Skip to content

Commit 15b7b54

Browse files
authored
Merge pull request #4134 from jedevc/enable-multi-exporters
Enable multiple exporters (alternative)
2 parents 5490361 + cfd320c commit 15b7b54

File tree

22 files changed

+1002
-432
lines changed

22 files changed

+1002
-432
lines changed

api/services/control/control.pb.go

Lines changed: 427 additions & 195 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/services/control/control.proto

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,11 @@ message UsageRecord {
6060
message SolveRequest {
6161
string Ref = 1;
6262
pb.Definition Definition = 2;
63-
string Exporter = 3;
64-
map<string, string> ExporterAttrs = 4;
63+
// ExporterDeprecated and ExporterAttrsDeprecated are deprecated in favor
64+
// of the new Exporters. If these fields are set, then they will be
65+
// appended to the Exporters field if Exporters was not explicitly set.
66+
string ExporterDeprecated = 3;
67+
map<string, string> ExporterAttrsDeprecated = 4;
6568
string Session = 5;
6669
string Frontend = 6;
6770
map<string, string> FrontendAttrs = 7;
@@ -70,6 +73,7 @@ message SolveRequest {
7073
map<string, pb.Definition> FrontendInputs = 10;
7174
bool Internal = 11; // Internal builds are not recorded in build history
7275
moby.buildkit.v1.sourcepolicy.Policy SourcePolicy = 12;
76+
repeated Exporter Exporters = 13;
7377
}
7478

7579
message CacheOptions {
@@ -227,11 +231,15 @@ message Descriptor {
227231
}
228232

229233
message BuildResultInfo {
230-
Descriptor Result = 1;
234+
Descriptor ResultDeprecated = 1;
231235
repeated Descriptor Attestations = 2;
236+
map<int64, Descriptor> Results = 3;
232237
}
233238

239+
// Exporter describes the output exporter
234240
message Exporter {
241+
// Type identifies the exporter
235242
string Type = 1;
243+
// Attrs specifies exporter configuration
236244
map<string, string> Attrs = 2;
237245
}

client/client_test.go

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import (
4646
gatewaypb "github.com/moby/buildkit/frontend/gateway/pb"
4747
"github.com/moby/buildkit/identity"
4848
"github.com/moby/buildkit/session"
49+
"github.com/moby/buildkit/session/filesync"
4950
"github.com/moby/buildkit/session/secrets/secretsprovider"
5051
"github.com/moby/buildkit/session/sshforward/sshprovider"
5152
"github.com/moby/buildkit/solver/errdefs"
@@ -152,6 +153,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
152153
testTarExporterWithSocketCopy,
153154
testTarExporterSymlink,
154155
testMultipleRegistryCacheImportExport,
156+
testMultipleExporters,
155157
testSourceMap,
156158
testSourceMapFromRef,
157159
testLazyImagePush,
@@ -2569,6 +2571,106 @@ func testUser(t *testing.T, sb integration.Sandbox) {
25692571
checkAllReleasable(t, c, sb, true)
25702572
}
25712573

2574+
func testMultipleExporters(t *testing.T, sb integration.Sandbox) {
2575+
requiresLinux(t)
2576+
2577+
c, err := New(sb.Context(), sb.Address())
2578+
require.NoError(t, err)
2579+
defer c.Close()
2580+
2581+
def, err := llb.Scratch().File(llb.Mkfile("foo.txt", 0o755, nil)).Marshal(context.TODO())
2582+
require.NoError(t, err)
2583+
2584+
destDir, destDir2 := t.TempDir(), t.TempDir()
2585+
out := filepath.Join(destDir, "out.tar")
2586+
outW, err := os.Create(out)
2587+
require.NoError(t, err)
2588+
defer outW.Close()
2589+
2590+
out2 := filepath.Join(destDir, "out2.tar")
2591+
outW2, err := os.Create(out2)
2592+
require.NoError(t, err)
2593+
defer outW2.Close()
2594+
2595+
registry, err := sb.NewRegistry()
2596+
if errors.Is(err, integration.ErrRequirements) {
2597+
t.Skip(err.Error())
2598+
}
2599+
require.NoError(t, err)
2600+
2601+
target1, target2 := registry+"/buildkit/build/exporter:image",
2602+
registry+"/buildkit/build/alternative:image"
2603+
2604+
imageExporter := ExporterImage
2605+
if workers.IsTestDockerd() {
2606+
imageExporter = "moby"
2607+
}
2608+
2609+
ref := identity.NewID()
2610+
resp, err := c.Solve(sb.Context(), def, SolveOpt{
2611+
Ref: ref,
2612+
Exports: []ExportEntry{
2613+
{
2614+
Type: imageExporter,
2615+
Attrs: map[string]string{
2616+
"name": target1,
2617+
},
2618+
},
2619+
{
2620+
Type: imageExporter,
2621+
Attrs: map[string]string{
2622+
"name": target2,
2623+
"oci-mediatypes": "true",
2624+
},
2625+
},
2626+
// Ensure that multiple local exporter destinations are written properly
2627+
{
2628+
Type: ExporterLocal,
2629+
OutputDir: destDir,
2630+
},
2631+
{
2632+
Type: ExporterLocal,
2633+
OutputDir: destDir2,
2634+
},
2635+
// Ensure that multiple instances of the same exporter are possible
2636+
{
2637+
Type: ExporterTar,
2638+
Output: fixedWriteCloser(outW),
2639+
},
2640+
{
2641+
Type: ExporterTar,
2642+
Output: fixedWriteCloser(outW2),
2643+
},
2644+
},
2645+
}, nil)
2646+
require.NoError(t, err)
2647+
2648+
require.Equal(t, resp.ExporterResponse["image.name"], target2)
2649+
require.FileExists(t, filepath.Join(destDir, "out.tar"))
2650+
require.FileExists(t, filepath.Join(destDir, "out2.tar"))
2651+
require.FileExists(t, filepath.Join(destDir, "foo.txt"))
2652+
require.FileExists(t, filepath.Join(destDir2, "foo.txt"))
2653+
2654+
history, err := c.ControlClient().ListenBuildHistory(sb.Context(), &controlapi.BuildHistoryRequest{
2655+
Ref: ref,
2656+
EarlyExit: true,
2657+
})
2658+
require.NoError(t, err)
2659+
for {
2660+
ev, err := history.Recv()
2661+
if err != nil {
2662+
require.Equal(t, io.EOF, err)
2663+
break
2664+
}
2665+
require.Equal(t, ref, ev.Record.Ref)
2666+
2667+
require.Len(t, ev.Record.Result.Results, 2)
2668+
require.Equal(t, images.MediaTypeDockerSchema2Manifest, ev.Record.Result.Results[0].MediaType)
2669+
require.Equal(t, ocispecs.MediaTypeImageManifest, ev.Record.Result.Results[1].MediaType)
2670+
require.Equal(t, ev.Record.Result.Results[0], ev.Record.Result.ResultDeprecated)
2671+
}
2672+
}
2673+
25722674
func testOCIExporter(t *testing.T, sb integration.Sandbox) {
25732675
workers.CheckFeatureCompat(t, sb, workers.FeatureOCIExporter)
25742676
requiresLinux(t)
@@ -6979,7 +7081,7 @@ func testMergeOpCache(t *testing.T, sb integration.Sandbox, mode string) {
69797081

69807082
for i, layer := range manifest.Layers {
69817083
_, err = contentStore.Info(ctx, layer.Digest)
6982-
require.ErrorIs(t, err, ctderrdefs.ErrNotFound, "unexpected error %v for index %d", err, i)
7084+
require.ErrorIs(t, err, ctderrdefs.ErrNotFound, "unexpected error %v for index %d (%s)", err, i, layer.Digest)
69837085
}
69847086

69857087
// re-run the build with a change only to input1 using the remote cache
@@ -9659,7 +9761,7 @@ var hostNetwork integration.ConfigUpdater = &netModeHost{}
96599761
var defaultNetwork integration.ConfigUpdater = &netModeDefault{}
96609762
var bridgeDNSNetwork integration.ConfigUpdater = &netModeBridgeDNS{}
96619763

9662-
func fixedWriteCloser(wc io.WriteCloser) func(map[string]string) (io.WriteCloser, error) {
9764+
func fixedWriteCloser(wc io.WriteCloser) filesync.FileOutputFunc {
96639765
return func(map[string]string) (io.WriteCloser, error) {
96649766
return wc, nil
96659767
}

client/solve.go

Lines changed: 75 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ type SolveOpt struct {
5656
type ExportEntry struct {
5757
Type string
5858
Attrs map[string]string
59-
Output func(map[string]string) (io.WriteCloser, error) // for ExporterOCI and ExporterDocker
60-
OutputDir string // for ExporterLocal
59+
Output filesync.FileOutputFunc // for ExporterOCI and ExporterDocker
60+
OutputDir string // for ExporterLocal
6161
}
6262

6363
type CacheOptionsEntry struct {
@@ -130,14 +130,6 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG
130130
return nil, err
131131
}
132132

133-
var ex ExportEntry
134-
if len(opt.Exports) > 1 {
135-
return nil, errors.New("currently only single Exports can be specified")
136-
}
137-
if len(opt.Exports) == 1 {
138-
ex = opt.Exports[0]
139-
}
140-
141133
storesToUpdate := []string{}
142134

143135
if !opt.SessionPreInitialized {
@@ -161,58 +153,63 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG
161153
contentStores[key2] = store
162154
}
163155

164-
var supportFile bool
165-
var supportDir bool
166-
switch ex.Type {
167-
case ExporterLocal:
168-
supportDir = true
169-
case ExporterTar:
170-
supportFile = true
171-
case ExporterOCI, ExporterDocker:
172-
supportDir = ex.OutputDir != ""
173-
supportFile = ex.Output != nil
174-
}
175-
176-
if supportFile && supportDir {
177-
return nil, errors.Errorf("both file and directory output is not supported by %s exporter", ex.Type)
178-
}
179-
if !supportFile && ex.Output != nil {
180-
return nil, errors.Errorf("output file writer is not supported by %s exporter", ex.Type)
181-
}
182-
if !supportDir && ex.OutputDir != "" {
183-
return nil, errors.Errorf("output directory is not supported by %s exporter", ex.Type)
184-
}
185-
186-
if supportFile {
187-
if ex.Output == nil {
188-
return nil, errors.Errorf("output file writer is required for %s exporter", ex.Type)
189-
}
190-
s.Allow(filesync.NewFSSyncTarget(ex.Output))
191-
}
192-
if supportDir {
193-
if ex.OutputDir == "" {
194-
return nil, errors.Errorf("output directory is required for %s exporter", ex.Type)
195-
}
156+
var syncTargets []filesync.FSSyncTarget
157+
for exID, ex := range opt.Exports {
158+
var supportFile bool
159+
var supportDir bool
196160
switch ex.Type {
161+
case ExporterLocal:
162+
supportDir = true
163+
case ExporterTar:
164+
supportFile = true
197165
case ExporterOCI, ExporterDocker:
198-
if err := os.MkdirAll(ex.OutputDir, 0755); err != nil {
199-
return nil, err
166+
supportDir = ex.OutputDir != ""
167+
supportFile = ex.Output != nil
168+
}
169+
if supportFile && supportDir {
170+
return nil, errors.Errorf("both file and directory output is not supported by %s exporter", ex.Type)
171+
}
172+
if !supportFile && ex.Output != nil {
173+
return nil, errors.Errorf("output file writer is not supported by %s exporter", ex.Type)
174+
}
175+
if !supportDir && ex.OutputDir != "" {
176+
return nil, errors.Errorf("output directory is not supported by %s exporter", ex.Type)
177+
}
178+
if supportFile {
179+
if ex.Output == nil {
180+
return nil, errors.Errorf("output file writer is required for %s exporter", ex.Type)
200181
}
201-
cs, err := contentlocal.NewStore(ex.OutputDir)
202-
if err != nil {
203-
return nil, err
182+
syncTargets = append(syncTargets, filesync.WithFSSync(exID, ex.Output))
183+
}
184+
if supportDir {
185+
if ex.OutputDir == "" {
186+
return nil, errors.Errorf("output directory is required for %s exporter", ex.Type)
187+
}
188+
switch ex.Type {
189+
case ExporterOCI, ExporterDocker:
190+
if err := os.MkdirAll(ex.OutputDir, 0755); err != nil {
191+
return nil, err
192+
}
193+
cs, err := contentlocal.NewStore(ex.OutputDir)
194+
if err != nil {
195+
return nil, err
196+
}
197+
contentStores["export"] = cs
198+
storesToUpdate = append(storesToUpdate, ex.OutputDir)
199+
default:
200+
syncTargets = append(syncTargets, filesync.WithFSSyncDir(exID, ex.OutputDir))
204201
}
205-
contentStores["export"] = cs
206-
storesToUpdate = append(storesToUpdate, ex.OutputDir)
207-
default:
208-
s.Allow(filesync.NewFSSyncTargetDir(ex.OutputDir))
209202
}
210203
}
211204

212205
if len(contentStores) > 0 {
213206
s.Allow(sessioncontent.NewAttachable(contentStores))
214207
}
215208

209+
if len(syncTargets) > 0 {
210+
s.Allow(filesync.NewFSSyncTarget(syncTargets...))
211+
}
212+
216213
eg.Go(func() error {
217214
sd := c.sessionDialer
218215
if sd == nil {
@@ -260,19 +257,34 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG
260257
frontendInputs[key] = def.ToPB()
261258
}
262259

260+
exports := make([]*controlapi.Exporter, 0, len(opt.Exports))
261+
exportDeprecated := ""
262+
exportAttrDeprecated := map[string]string{}
263+
for i, exp := range opt.Exports {
264+
if i == 0 {
265+
exportDeprecated = exp.Type
266+
exportAttrDeprecated = exp.Attrs
267+
}
268+
exports = append(exports, &controlapi.Exporter{
269+
Type: exp.Type,
270+
Attrs: exp.Attrs,
271+
})
272+
}
273+
263274
resp, err := c.ControlClient().Solve(ctx, &controlapi.SolveRequest{
264-
Ref: ref,
265-
Definition: pbd,
266-
Exporter: ex.Type,
267-
ExporterAttrs: ex.Attrs,
268-
Session: s.ID(),
269-
Frontend: opt.Frontend,
270-
FrontendAttrs: frontendAttrs,
271-
FrontendInputs: frontendInputs,
272-
Cache: cacheOpt.options,
273-
Entitlements: opt.AllowedEntitlements,
274-
Internal: opt.Internal,
275-
SourcePolicy: opt.SourcePolicy,
275+
Ref: ref,
276+
Definition: pbd,
277+
Exporters: exports,
278+
ExporterDeprecated: exportDeprecated,
279+
ExporterAttrsDeprecated: exportAttrDeprecated,
280+
Session: s.ID(),
281+
Frontend: opt.Frontend,
282+
FrontendAttrs: frontendAttrs,
283+
FrontendInputs: frontendInputs,
284+
Cache: cacheOpt.options,
285+
Entitlements: opt.AllowedEntitlements,
286+
Internal: opt.Internal,
287+
SourcePolicy: opt.SourcePolicy,
276288
})
277289
if err != nil {
278290
return errors.Wrap(err, "failed to solve")

cmd/buildctl/build/output.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/containerd/console"
1111
"github.com/moby/buildkit/client"
12+
"github.com/moby/buildkit/session/filesync"
1213
"github.com/pkg/errors"
1314
)
1415

@@ -66,7 +67,7 @@ func ParseOutput(exports []string) ([]client.ExportEntry, error) {
6667
}
6768

6869
// resolveExporterDest returns at most either one of io.WriteCloser (single file) or a string (directory path).
69-
func resolveExporterDest(exporter, dest string, attrs map[string]string) (func(map[string]string) (io.WriteCloser, error), string, error) {
70+
func resolveExporterDest(exporter, dest string, attrs map[string]string) (filesync.FileOutputFunc, string, error) {
7071
wrapWriter := func(wc io.WriteCloser) func(map[string]string) (io.WriteCloser, error) {
7172
return func(m map[string]string) (io.WriteCloser, error) {
7273
return wc, nil

0 commit comments

Comments
 (0)