build: basis of build command

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi
2019-03-23 21:30:29 -07:00
parent 8b7c38e61a
commit 4b0c0468d0
10 changed files with 620 additions and 88 deletions

193
build/build.go Normal file
View File

@ -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
}

86
build/output.go Normal file
View File

@ -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
}

32
build/platform.go Normal file
View File

@ -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
}

60
build/secrets.go Normal file
View File

@ -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
}

31
build/ssh.go Normal file
View File

@ -0,0 +1,31 @@
package build
import (
"strings"
"github.com/moby/buildkit/session"
"github.com/moby/buildkit/session/sshforward/sshprovider"
)
func ParseSSHSpecs(sl []string) (session.Attachable, error) {
configs := make([]sshprovider.AgentConfig, 0, len(sl))
for _, v := range sl {
c, err := parseSSH(v)
if err != nil {
return nil, err
}
configs = append(configs, *c)
}
return sshprovider.NewSSHAgentProvider(configs)
}
func parseSSH(value string) (*sshprovider.AgentConfig, error) {
parts := strings.SplitN(value, "=", 2)
cfg := sshprovider.AgentConfig{
ID: parts[0],
}
if len(parts) > 1 {
cfg.Paths = strings.Split(parts[1], ",")
}
return &cfg, nil
}