-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enable multiple exporters #2760
Changes from all commits
6d6f505
7a9405e
64704b3
75b1237
7b845fd
9741d62
d1192f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ import ( | |
"context" | ||
"encoding/base64" | ||
"encoding/json" | ||
"fmt" | ||
"io" | ||
"os" | ||
"path/filepath" | ||
|
@@ -55,8 +56,8 @@ type SolveOpt struct { | |
type ExportEntry struct { | ||
Type string | ||
Attrs map[string]string | ||
Output func(map[string]string) (io.WriteCloser, error) // for ExporterOCI and ExporterDocker | ||
OutputDir string // for ExporterLocal | ||
Output filesync.FileOutputFunc // for ExporterOCI and ExporterDocker | ||
OutputDir string // for ExporterLocal | ||
} | ||
|
||
type CacheOptionsEntry struct { | ||
|
@@ -125,12 +126,23 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG | |
return nil, err | ||
} | ||
|
||
var ex ExportEntry | ||
if len(opt.Exports) > 1 { | ||
return nil, errors.New("currently only single Exports can be specified") | ||
type exporter struct { | ||
ExportEntry | ||
id string | ||
} | ||
if len(opt.Exports) == 1 { | ||
ex = opt.Exports[0] | ||
|
||
var exporters []exporter | ||
ids := make(map[string]int) | ||
for _, exp := range opt.Exports { | ||
if id, ok := ids[exp.Type]; !ok { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A naive way to auto-id the exporters. Potentially could also be provided |
||
ids[exp.Type] = 1 | ||
} else { | ||
ids[exp.Type] = id + 1 | ||
} | ||
exporters = append(exporters, exporter{ | ||
ExportEntry: exp, | ||
id: fmt.Sprint(exp.Type, ids[exp.Type]), | ||
}) | ||
} | ||
|
||
storesToUpdate := []string{} | ||
|
@@ -156,58 +168,70 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG | |
contentStores[key2] = store | ||
} | ||
|
||
var supportFile bool | ||
var supportDir bool | ||
switch ex.Type { | ||
case ExporterLocal: | ||
supportDir = true | ||
case ExporterTar: | ||
supportFile = true | ||
case ExporterOCI, ExporterDocker: | ||
supportDir = ex.OutputDir != "" | ||
supportFile = ex.Output != nil | ||
var exporterConfig struct { | ||
outputDirs []string | ||
outputs map[string]filesync.FileOutputFunc | ||
} | ||
|
||
if supportFile && supportDir { | ||
return nil, errors.Errorf("both file and directory output is not supported by %s exporter", ex.Type) | ||
} | ||
if !supportFile && ex.Output != nil { | ||
return nil, errors.Errorf("output file writer is not supported by %s exporter", ex.Type) | ||
} | ||
if !supportDir && ex.OutputDir != "" { | ||
return nil, errors.Errorf("output directory is not supported by %s exporter", ex.Type) | ||
} | ||
|
||
if supportFile { | ||
if ex.Output == nil { | ||
return nil, errors.Errorf("output file writer is required for %s exporter", ex.Type) | ||
} | ||
s.Allow(filesync.NewFSSyncTarget(ex.Output)) | ||
} | ||
if supportDir { | ||
if ex.OutputDir == "" { | ||
return nil, errors.Errorf("output directory is required for %s exporter", ex.Type) | ||
} | ||
for _, ex := range exporters { | ||
var supportFile bool | ||
var supportDir bool | ||
switch ex.Type { | ||
case ExporterLocal: | ||
supportDir = true | ||
case ExporterTar: | ||
supportFile = true | ||
case ExporterOCI, ExporterDocker: | ||
if err := os.MkdirAll(ex.OutputDir, 0755); err != nil { | ||
return nil, err | ||
supportDir = ex.OutputDir != "" | ||
supportFile = ex.Output != nil | ||
} | ||
if supportFile && supportDir { | ||
return nil, errors.Errorf("both file and directory output is not supported by %s exporter", ex.Type) | ||
} | ||
if !supportFile && ex.Output != nil { | ||
return nil, errors.Errorf("output file writer is not supported by %s exporter", ex.Type) | ||
} | ||
if !supportDir && ex.OutputDir != "" { | ||
return nil, errors.Errorf("output directory is not supported by %s exporter", ex.Type) | ||
} | ||
if supportFile { | ||
if ex.Output == nil { | ||
return nil, errors.Errorf("output file writer is required for %s exporter", ex.Type) | ||
} | ||
cs, err := contentlocal.NewStore(ex.OutputDir) | ||
if err != nil { | ||
return nil, err | ||
if exporterConfig.outputs == nil { | ||
exporterConfig.outputs = make(map[string]filesync.FileOutputFunc) | ||
} | ||
exporterConfig.outputs[ex.id] = ex.Output | ||
} | ||
if supportDir { | ||
if ex.OutputDir == "" { | ||
return nil, errors.Errorf("output directory is required for %s exporter", ex.Type) | ||
} | ||
switch ex.Type { | ||
case ExporterOCI, ExporterDocker: | ||
if err := os.MkdirAll(ex.OutputDir, 0755); err != nil { | ||
return nil, err | ||
} | ||
cs, err := contentlocal.NewStore(ex.OutputDir) | ||
if err != nil { | ||
return nil, err | ||
} | ||
contentStores["export"] = cs | ||
storesToUpdate = append(storesToUpdate, ex.OutputDir) | ||
default: | ||
exporterConfig.outputDirs = append(exporterConfig.outputDirs, ex.OutputDir) | ||
} | ||
contentStores["export"] = cs | ||
storesToUpdate = append(storesToUpdate, ex.OutputDir) | ||
default: | ||
s.Allow(filesync.NewFSSyncTargetDir(ex.OutputDir)) | ||
} | ||
} | ||
|
||
if len(contentStores) > 0 { | ||
s.Allow(sessioncontent.NewAttachable(contentStores)) | ||
} | ||
|
||
if len(exporterConfig.outputDirs) > 0 || len(exporterConfig.outputs) > 0 { | ||
s.Allow(filesync.NewFSSyncTarget(exporterConfig.outputs, exporterConfig.outputDirs)) | ||
} | ||
|
||
eg.Go(func() error { | ||
sd := c.sessionDialer | ||
if sd == nil { | ||
|
@@ -255,11 +279,33 @@ func (c *Client) solve(ctx context.Context, def *llb.Definition, runGateway runG | |
frontendInputs[key] = def.ToPB() | ||
} | ||
|
||
exports := make([]*controlapi.Exporter, 0, len(opt.Exports)) | ||
var localExporter *controlapi.Exporter | ||
for _, exp := range exporters { | ||
if exp.Type != ExporterLocal { | ||
exports = append(exports, &controlapi.Exporter{ | ||
ID: exp.id, | ||
Type: exp.Type, | ||
Attrs: exp.Attrs, | ||
}) | ||
} else if localExporter == nil { | ||
// TODO(dima): different options per local exporter? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is still unclear how to deal with potentially different options for multiple local exporters as currently (as implemented in this PR), the local export outputs are duplicated solely on the client, which means that any configuration taken into account on the server (for example, the attestation file prefix) will have to come from a single exporter configuration - e.g. the first (as is also currently implemented). Would appreciate feedback on this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think if we do my suggestion below for multi-plexing between multiple output directories, this issue should go away? We then shouldn't need to do the de-duplication. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I would need more context on this. It was a good pointer to avoid a new gRPC service - I did this with metadata instead, but it does not remove the option for users to specify multiple outputs of type local (including options which may vary) - this was my confusion at this specific point. |
||
localExporter = &controlapi.Exporter{ | ||
Type: ExporterLocal, | ||
Attrs: exp.Attrs, | ||
} | ||
} | ||
} | ||
if localExporter != nil { | ||
// Add a single instance of the local exporter | ||
// since it's replicated entirely on the client | ||
exports = append(exports, localExporter) | ||
} | ||
|
||
resp, err := c.ControlClient().Solve(ctx, &controlapi.SolveRequest{ | ||
Ref: ref, | ||
Definition: pbd, | ||
Exporter: ex.Type, | ||
ExporterAttrs: ex.Attrs, | ||
Exporters: exports, | ||
Session: s.ID(), | ||
Frontend: opt.Frontend, | ||
FrontendAttrs: frontendAttrs, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add
tar
exporters here as well for good measure.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, just realized that this could be a bigger issue with the multiple file sender implementations like local and tar exporters. There needs to be some sort of fan-out for the multiple file exporters on the gRPC level.