Skip to content

Commit

Permalink
build: basis of build command
Browse files Browse the repository at this point in the history
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
  • Loading branch information
tonistiigi committed Mar 24, 2019
1 parent 8b7c38e commit 4b0c046
Show file tree
Hide file tree
Showing 10 changed files with 620 additions and 88 deletions.
9 changes: 8 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,19 @@ COPY --from=buildx-build /usr/bin/buildx /buildx.exe
FROM binaries-$TARGETOS AS binaries

FROM alpine AS demo-env
RUN apk add --no-cache iptables tmux
RUN apk add --no-cache iptables tmux git
RUN mkdir -p /usr/local/lib/docker/cli-plugins && ln -s /usr/local/bin/buildx /usr/local/lib/docker/cli-plugins/docker-buildx
COPY ./hack/demo-env/entrypoint.sh /usr/local/bin
COPY ./hack/demo-env/tmux.conf /root/.tmux.conf
COPY --from=dockerd-release /usr/local/bin /usr/local/bin
COPY --from=docker-cli-build /go/src/github.com/docker/cli/build/docker /usr/local/bin

# Temporary buildkitd binaries. To be removed.
COPY --from=moby/buildkit /usr/bin/build* /usr/local/bin
VOLUME /var/lib/buildkit

WORKDIR /work
COPY ./hack/demo-env/examples .
COPY --from=binaries / /usr/local/bin/
VOLUME /var/lib/docker
ENTRYPOINT ["entrypoint.sh"]
193 changes: 193 additions & 0 deletions build/build.go
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
}
86 changes: 86 additions & 0 deletions build/output.go
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
}
32 changes: 32 additions & 0 deletions build/platform.go
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
}
60 changes: 60 additions & 0 deletions build/secrets.go
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
}
Loading

0 comments on commit 4b0c046

Please sign in to comment.