mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-07-12 22:47:09 +08:00
build: basis of build command
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
193
build/build.go
Normal file
193
build/build.go
Normal 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
86
build/output.go
Normal 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
32
build/platform.go
Normal 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
60
build/secrets.go
Normal 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
31
build/ssh.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user