-
Notifications
You must be signed in to change notification settings - Fork 510
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
- Loading branch information
1 parent
8b7c38e
commit 4b0c046
Showing
10 changed files
with
620 additions
and
88 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
package build | ||
|
||
import ( | ||
"context" | ||
"io" | ||
"os" | ||
"path/filepath" | ||
"strconv" | ||
"strings" | ||
|
||
"github.com/containerd/console" | ||
"github.com/containerd/containerd/platforms" | ||
"github.com/moby/buildkit/client" | ||
"github.com/moby/buildkit/session" | ||
"github.com/moby/buildkit/util/progress/progressui" | ||
specs "github.com/opencontainers/image-spec/specs-go/v1" | ||
"github.com/pkg/errors" | ||
"golang.org/x/sync/errgroup" | ||
) | ||
|
||
type Options struct { | ||
Inputs Inputs | ||
Tags []string | ||
Labels map[string]string | ||
BuildArgs map[string]string | ||
Pull bool | ||
|
||
NoCache bool | ||
Target string | ||
Platforms []specs.Platform | ||
Exports []client.ExportEntry | ||
Session []session.Attachable | ||
|
||
// DockerTarget | ||
} | ||
|
||
type Inputs struct { | ||
ContextPath string | ||
DockerfilePath string | ||
InStream io.Reader | ||
} | ||
|
||
func Build(ctx context.Context, c *client.Client, opt Options, pw *ProgressWriter) (*client.SolveResponse, error) { | ||
so := client.SolveOpt{ | ||
Frontend: "dockerfile.v0", | ||
FrontendAttrs: map[string]string{}, | ||
} | ||
|
||
if len(opt.Exports) > 1 { | ||
return nil, errors.Errorf("multiple outputs currently unsupported") | ||
} | ||
|
||
if len(opt.Tags) > 0 { | ||
for i, e := range opt.Exports { | ||
switch e.Type { | ||
case "image", "oci", "docker": | ||
opt.Exports[i].Attrs["name"] = strings.Join(opt.Tags, ",") | ||
} | ||
} | ||
} else { | ||
for _, e := range opt.Exports { | ||
if e.Type == "image" && e.Attrs["name"] == "" && e.Attrs["push"] != "" { | ||
if ok, _ := strconv.ParseBool(e.Attrs["push"]); ok { | ||
return nil, errors.Errorf("tag is needed when pushing to registry") | ||
} | ||
} | ||
} | ||
} | ||
// TODO: handle loading to docker daemon | ||
|
||
so.Exports = opt.Exports | ||
so.Session = opt.Session | ||
|
||
if err := LoadInputs(opt.Inputs, &so); err != nil { | ||
return nil, err | ||
} | ||
|
||
if opt.Pull { | ||
so.FrontendAttrs["image-resolve-mode"] = "pull" | ||
} | ||
if opt.Target != "" { | ||
so.FrontendAttrs["target"] = opt.Target | ||
} | ||
if opt.NoCache { | ||
so.FrontendAttrs["no-cache"] = "" | ||
} | ||
for k, v := range opt.BuildArgs { | ||
so.FrontendAttrs["build-arg:"+k] = v | ||
} | ||
for k, v := range opt.Labels { | ||
so.FrontendAttrs["label:"+k] = v | ||
} | ||
|
||
if len(opt.Platforms) != 0 { | ||
pp := make([]string, len(opt.Platforms)) | ||
for i, p := range opt.Platforms { | ||
pp[i] = platforms.Format(p) | ||
} | ||
so.FrontendAttrs["platform"] = strings.Join(pp, ",") | ||
} | ||
|
||
eg, ctx := errgroup.WithContext(ctx) | ||
|
||
var statusCh chan *client.SolveStatus | ||
if pw != nil { | ||
statusCh = pw.Status() | ||
eg.Go(func() error { | ||
<-pw.Done() | ||
return pw.Err() | ||
}) | ||
} | ||
|
||
var resp *client.SolveResponse | ||
eg.Go(func() error { | ||
var err error | ||
resp, err = c.Solve(ctx, nil, so, statusCh) | ||
if err != nil { | ||
return err | ||
} | ||
return nil | ||
}) | ||
|
||
if err := eg.Wait(); err != nil { | ||
return nil, err | ||
} | ||
|
||
return resp, nil | ||
} | ||
|
||
type ProgressWriter struct { | ||
status chan *client.SolveStatus | ||
done <-chan struct{} | ||
err error | ||
} | ||
|
||
func (pw *ProgressWriter) Done() <-chan struct{} { | ||
return pw.done | ||
} | ||
|
||
func (pw *ProgressWriter) Err() error { | ||
return pw.err | ||
} | ||
|
||
func (pw *ProgressWriter) Status() chan *client.SolveStatus { | ||
return pw.status | ||
} | ||
|
||
func NewProgressWriter(ctx context.Context, out *os.File, mode string) *ProgressWriter { | ||
statusCh := make(chan *client.SolveStatus) | ||
doneCh := make(chan struct{}) | ||
|
||
pw := &ProgressWriter{ | ||
status: statusCh, | ||
done: doneCh, | ||
} | ||
|
||
go func() { | ||
var c console.Console | ||
if cons, err := console.ConsoleFromFile(out); err == nil && (mode == "auto" || mode == "tty") { | ||
c = cons | ||
} | ||
// not using shared context to not disrupt display but let is finish reporting errors | ||
pw.err = progressui.DisplaySolveStatus(ctx, "", c, out, statusCh) | ||
close(doneCh) | ||
}() | ||
return pw | ||
} | ||
|
||
func LoadInputs(inp Inputs, target *client.SolveOpt) error { | ||
if inp.ContextPath == "" { | ||
return errors.New("please specify build context (e.g. \".\" for the current directory)") | ||
} | ||
|
||
// TODO: handle stdin, symlinks, remote contexts, check files exist | ||
|
||
if inp.DockerfilePath == "" { | ||
inp.DockerfilePath = filepath.Join(inp.ContextPath, "Dockerfile") | ||
} | ||
|
||
if target.LocalDirs == nil { | ||
target.LocalDirs = map[string]string{} | ||
} | ||
|
||
target.LocalDirs["context"] = inp.ContextPath | ||
target.LocalDirs["dockerfile"] = filepath.Dir(inp.DockerfilePath) | ||
|
||
if target.FrontendAttrs == nil { | ||
target.FrontendAttrs = map[string]string{} | ||
} | ||
|
||
target.FrontendAttrs["filename"] = filepath.Base(inp.DockerfilePath) | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package build | ||
|
||
import ( | ||
"encoding/csv" | ||
"os" | ||
"strings" | ||
|
||
"github.com/moby/buildkit/client" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
func ParseOutputs(inp []string) ([]client.ExportEntry, error) { | ||
var outs []client.ExportEntry | ||
if len(inp) == 0 { | ||
return nil, nil | ||
} | ||
for _, s := range inp { | ||
csvReader := csv.NewReader(strings.NewReader(s)) | ||
fields, err := csvReader.Read() | ||
if err != nil { | ||
return nil, err | ||
} | ||
if len(fields) == 1 && fields[0] == s { | ||
outs = append(outs, client.ExportEntry{ | ||
Type: "local", | ||
OutputDir: s, | ||
}) | ||
continue | ||
} | ||
|
||
out := client.ExportEntry{ | ||
Attrs: map[string]string{}, | ||
} | ||
for _, field := range fields { | ||
parts := strings.SplitN(field, "=", 2) | ||
if len(parts) != 2 { | ||
return nil, errors.Errorf("invalid value %s", field) | ||
} | ||
key := strings.ToLower(parts[0]) | ||
value := parts[1] | ||
switch key { | ||
case "type": | ||
out.Type = value | ||
default: | ||
out.Attrs[key] = value | ||
} | ||
} | ||
if out.Type == "" { | ||
return nil, errors.Errorf("type is required for output") | ||
} | ||
|
||
// handle client side | ||
switch out.Type { | ||
case "local": | ||
dest, ok := out.Attrs["dest"] | ||
if !ok { | ||
return nil, errors.Errorf("dest is required for local output") | ||
} | ||
out.OutputDir = dest | ||
delete(out.Attrs, "dest") | ||
case "oci", "dest": | ||
dest, ok := out.Attrs["dest"] | ||
if !ok { | ||
if out.Type != "docker" { | ||
return nil, errors.Errorf("dest is required for %s output", out.Type) | ||
} | ||
} else { | ||
if dest == "-" { | ||
out.Output = os.Stdout | ||
} else { | ||
f, err := os.Open(dest) | ||
if err != nil { | ||
out.Output = f | ||
} | ||
} | ||
delete(out.Attrs, "dest") | ||
} | ||
case "registry": | ||
out.Type = "iamge" | ||
out.Attrs["push"] = "true" | ||
} | ||
|
||
outs = append(outs, out) | ||
} | ||
return outs, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package build | ||
|
||
import ( | ||
"strings" | ||
|
||
"github.com/containerd/containerd/platforms" | ||
specs "github.com/opencontainers/image-spec/specs-go/v1" | ||
) | ||
|
||
func ParsePlatformSpecs(platformsStr []string) ([]specs.Platform, error) { | ||
if len(platformsStr) == 0 { | ||
return nil, nil | ||
} | ||
out := make([]specs.Platform, 0, len(platformsStr)) | ||
for _, s := range platformsStr { | ||
parts := strings.Split(s, ",") | ||
if len(parts) > 1 { | ||
p, err := ParsePlatformSpecs(parts) | ||
if err != nil { | ||
return nil, err | ||
} | ||
out = append(out, p...) | ||
continue | ||
} | ||
p, err := platforms.Parse(s) | ||
if err != nil { | ||
return nil, err | ||
} | ||
out = append(out, platforms.Normalize(p)) | ||
} | ||
return out, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package build | ||
|
||
import ( | ||
"encoding/csv" | ||
"strings" | ||
|
||
"github.com/moby/buildkit/session" | ||
"github.com/moby/buildkit/session/secrets/secretsprovider" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
func ParseSecretSpecs(sl []string) (session.Attachable, error) { | ||
fs := make([]secretsprovider.FileSource, 0, len(sl)) | ||
for _, v := range sl { | ||
s, err := parseSecret(v) | ||
if err != nil { | ||
return nil, err | ||
} | ||
fs = append(fs, *s) | ||
} | ||
store, err := secretsprovider.NewFileStore(fs) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return secretsprovider.NewSecretProvider(store), nil | ||
} | ||
|
||
func parseSecret(value string) (*secretsprovider.FileSource, error) { | ||
csvReader := csv.NewReader(strings.NewReader(value)) | ||
fields, err := csvReader.Read() | ||
if err != nil { | ||
return nil, errors.Wrap(err, "failed to parse csv secret") | ||
} | ||
|
||
fs := secretsprovider.FileSource{} | ||
|
||
for _, field := range fields { | ||
parts := strings.SplitN(field, "=", 2) | ||
key := strings.ToLower(parts[0]) | ||
|
||
if len(parts) != 2 { | ||
return nil, errors.Errorf("invalid field '%s' must be a key=value pair", field) | ||
} | ||
|
||
value := parts[1] | ||
switch key { | ||
case "type": | ||
if value != "file" { | ||
return nil, errors.Errorf("unsupported secret type %q", value) | ||
} | ||
case "id": | ||
fs.ID = value | ||
case "source", "src": | ||
fs.FilePath = value | ||
default: | ||
return nil, errors.Errorf("unexpected key '%s' in '%s'", key, field) | ||
} | ||
} | ||
return &fs, nil | ||
} |
Oops, something went wrong.