dotcloud/docker

View on GitHub
api/server/router/build/build_routes.go

Summary

Maintainability
C
1 day
Test Coverage
package build // import "github.com/docker/docker/api/server/router/build"

import (
    "bufio"
    "bytes"
    "context"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "runtime"
    "strconv"
    "strings"
    "sync"

    "github.com/containerd/log"
    "github.com/docker/docker/api/server/httputils"
    "github.com/docker/docker/api/types"
    "github.com/docker/docker/api/types/backend"
    "github.com/docker/docker/api/types/container"
    "github.com/docker/docker/api/types/filters"
    "github.com/docker/docker/api/types/registry"
    "github.com/docker/docker/api/types/versions"
    "github.com/docker/docker/pkg/ioutils"
    "github.com/docker/docker/pkg/progress"
    "github.com/docker/docker/pkg/streamformatter"
    "github.com/pkg/errors"
)

type invalidParam struct {
    error
}

func (e invalidParam) InvalidParameter() {}

func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBuildOptions, error) {
    options := &types.ImageBuildOptions{
        Version:        types.BuilderV1, // Builder V1 is the default, but can be overridden
        Dockerfile:     r.FormValue("dockerfile"),
        SuppressOutput: httputils.BoolValue(r, "q"),
        NoCache:        httputils.BoolValue(r, "nocache"),
        ForceRemove:    httputils.BoolValue(r, "forcerm"),
        PullParent:     httputils.BoolValue(r, "pull"),
        MemorySwap:     httputils.Int64ValueOrZero(r, "memswap"),
        Memory:         httputils.Int64ValueOrZero(r, "memory"),
        CPUShares:      httputils.Int64ValueOrZero(r, "cpushares"),
        CPUPeriod:      httputils.Int64ValueOrZero(r, "cpuperiod"),
        CPUQuota:       httputils.Int64ValueOrZero(r, "cpuquota"),
        CPUSetCPUs:     r.FormValue("cpusetcpus"),
        CPUSetMems:     r.FormValue("cpusetmems"),
        CgroupParent:   r.FormValue("cgroupparent"),
        NetworkMode:    r.FormValue("networkmode"),
        Tags:           r.Form["t"],
        ExtraHosts:     r.Form["extrahosts"],
        SecurityOpt:    r.Form["securityopt"],
        Squash:         httputils.BoolValue(r, "squash"),
        Target:         r.FormValue("target"),
        RemoteContext:  r.FormValue("remote"),
        SessionID:      r.FormValue("session"),
        BuildID:        r.FormValue("buildid"),
    }

    if runtime.GOOS != "windows" && options.SecurityOpt != nil {
        // SecurityOpt only supports "credentials-spec" on Windows, and not used on other platforms.
        return nil, invalidParam{errors.New("security options are not supported on " + runtime.GOOS)}
    }

    if httputils.BoolValue(r, "forcerm") {
        options.Remove = true
    } else if r.FormValue("rm") == "" {
        options.Remove = true
    } else {
        options.Remove = httputils.BoolValue(r, "rm")
    }
    version := httputils.VersionFromContext(ctx)
    if versions.GreaterThanOrEqualTo(version, "1.32") {
        options.Platform = r.FormValue("platform")
    }
    if versions.GreaterThanOrEqualTo(version, "1.40") {
        outputsJSON := r.FormValue("outputs")
        if outputsJSON != "" {
            var outputs []types.ImageBuildOutput
            if err := json.Unmarshal([]byte(outputsJSON), &outputs); err != nil {
                return nil, invalidParam{errors.Wrap(err, "invalid outputs specified")}
            }
            options.Outputs = outputs
        }
    }

    if s := r.Form.Get("shmsize"); s != "" {
        shmSize, err := strconv.ParseInt(s, 10, 64)
        if err != nil {
            return nil, err
        }
        options.ShmSize = shmSize
    }

    if i := r.FormValue("isolation"); i != "" {
        options.Isolation = container.Isolation(i)
        if !options.Isolation.IsValid() {
            return nil, invalidParam{errors.Errorf("unsupported isolation: %q", i)}
        }
    }

    if ulimitsJSON := r.FormValue("ulimits"); ulimitsJSON != "" {
        buildUlimits := []*container.Ulimit{}
        if err := json.Unmarshal([]byte(ulimitsJSON), &buildUlimits); err != nil {
            return nil, invalidParam{errors.Wrap(err, "error reading ulimit settings")}
        }
        options.Ulimits = buildUlimits
    }

    // Note that there are two ways a --build-arg might appear in the
    // json of the query param:
    //     "foo":"bar"
    // and "foo":nil
    // The first is the normal case, ie. --build-arg foo=bar
    // or  --build-arg foo
    // where foo's value was picked up from an env var.
    // The second ("foo":nil) is where they put --build-arg foo
    // but "foo" isn't set as an env var. In that case we can't just drop
    // the fact they mentioned it, we need to pass that along to the builder
    // so that it can print a warning about "foo" being unused if there is
    // no "ARG foo" in the Dockerfile.
    if buildArgsJSON := r.FormValue("buildargs"); buildArgsJSON != "" {
        buildArgs := map[string]*string{}
        if err := json.Unmarshal([]byte(buildArgsJSON), &buildArgs); err != nil {
            return nil, invalidParam{errors.Wrap(err, "error reading build args")}
        }
        options.BuildArgs = buildArgs
    }

    if labelsJSON := r.FormValue("labels"); labelsJSON != "" {
        labels := map[string]string{}
        if err := json.Unmarshal([]byte(labelsJSON), &labels); err != nil {
            return nil, invalidParam{errors.Wrap(err, "error reading labels")}
        }
        options.Labels = labels
    }

    if cacheFromJSON := r.FormValue("cachefrom"); cacheFromJSON != "" {
        cacheFrom := []string{}
        if err := json.Unmarshal([]byte(cacheFromJSON), &cacheFrom); err != nil {
            return nil, invalidParam{errors.Wrap(err, "error reading cache-from")}
        }
        options.CacheFrom = cacheFrom
    }

    if bv := r.FormValue("version"); bv != "" {
        v, err := parseVersion(bv)
        if err != nil {
            return nil, err
        }
        options.Version = v
    }

    return options, nil
}

func parseVersion(s string) (types.BuilderVersion, error) {
    switch types.BuilderVersion(s) {
    case types.BuilderV1:
        return types.BuilderV1, nil
    case types.BuilderBuildKit:
        return types.BuilderBuildKit, nil
    default:
        return "", invalidParam{errors.Errorf("invalid version %q", s)}
    }
}

func (br *buildRouter) postPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    if err := httputils.ParseForm(r); err != nil {
        return err
    }
    fltrs, err := filters.FromJSON(r.Form.Get("filters"))
    if err != nil {
        return err
    }
    ksfv := r.FormValue("keep-storage")
    if ksfv == "" {
        ksfv = "0"
    }
    ks, err := strconv.Atoi(ksfv)
    if err != nil {
        return invalidParam{errors.Wrapf(err, "keep-storage is in bytes and expects an integer, got %v", ksfv)}
    }

    opts := types.BuildCachePruneOptions{
        All:         httputils.BoolValue(r, "all"),
        Filters:     fltrs,
        KeepStorage: int64(ks),
    }

    report, err := br.backend.PruneCache(ctx, opts)
    if err != nil {
        return err
    }
    return httputils.WriteJSON(w, http.StatusOK, report)
}

func (br *buildRouter) postCancel(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    w.Header().Set("Content-Type", "application/json")

    id := r.FormValue("id")
    if id == "" {
        return invalidParam{errors.New("build ID not provided")}
    }

    return br.backend.Cancel(ctx, id)
}

func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
    var (
        notVerboseBuffer = bytes.NewBuffer(nil)
        version          = httputils.VersionFromContext(ctx)
    )

    w.Header().Set("Content-Type", "application/json")

    body := r.Body
    var ww io.Writer = w
    if body != nil {
        // there is a possibility that output is written before request body
        // has been fully read so we need to protect against it.
        // this can be removed when
        // https://github.com/golang/go/issues/15527
        // https://github.com/golang/go/issues/22209
        // has been fixed
        body, ww = wrapOutputBufferedUntilRequestRead(body, ww)
    }

    output := ioutils.NewWriteFlusher(ww)
    defer func() { _ = output.Close() }()

    errf := func(err error) error {
        if httputils.BoolValue(r, "q") && notVerboseBuffer.Len() > 0 {
            _, _ = output.Write(notVerboseBuffer.Bytes())
        }

        // Do not write the error in the http output if it's still empty.
        // This prevents from writing a 200(OK) when there is an internal error.
        if !output.Flushed() {
            return err
        }
        _, err = output.Write(streamformatter.FormatError(err))
        if err != nil {
            log.G(ctx).Warnf("could not write error response: %v", err)
        }
        return nil
    }

    buildOptions, err := newImageBuildOptions(ctx, r)
    if err != nil {
        return errf(err)
    }
    buildOptions.AuthConfigs = getAuthConfigs(r.Header)

    if buildOptions.Squash && !br.daemon.HasExperimental() {
        return invalidParam{errors.New("squash is only supported with experimental mode")}
    }

    out := io.Writer(output)
    if buildOptions.SuppressOutput {
        out = notVerboseBuffer
    }

    // Currently, only used if context is from a remote url.
    // Look at code in DetectContextFromRemoteURL for more information.
    createProgressReader := func(in io.ReadCloser) io.ReadCloser {
        progressOutput := streamformatter.NewJSONProgressOutput(out, true)
        return progress.NewProgressReader(in, progressOutput, r.ContentLength, "Downloading context", buildOptions.RemoteContext)
    }

    wantAux := versions.GreaterThanOrEqualTo(version, "1.30")

    imgID, err := br.backend.Build(ctx, backend.BuildConfig{
        Source:         body,
        Options:        buildOptions,
        ProgressWriter: buildProgressWriter(out, wantAux, createProgressReader),
    })
    if err != nil {
        return errf(err)
    }

    // Everything worked so if -q was provided the output from the daemon
    // should be just the image ID and we'll print that to stdout.
    if buildOptions.SuppressOutput {
        _, _ = fmt.Fprintln(streamformatter.NewStdoutWriter(output), imgID)
    }
    return nil
}

func getAuthConfigs(header http.Header) map[string]registry.AuthConfig {
    authConfigs := map[string]registry.AuthConfig{}
    authConfigsEncoded := header.Get("X-Registry-Config")

    if authConfigsEncoded == "" {
        return authConfigs
    }

    authConfigsJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authConfigsEncoded))
    // Pulling an image does not error when no auth is provided so to remain
    // consistent with the existing api decode errors are ignored
    _ = json.NewDecoder(authConfigsJSON).Decode(&authConfigs)
    return authConfigs
}

type syncWriter struct {
    w  io.Writer
    mu sync.Mutex
}

func (s *syncWriter) Write(b []byte) (count int, err error) {
    s.mu.Lock()
    count, err = s.w.Write(b)
    s.mu.Unlock()
    return
}

func buildProgressWriter(out io.Writer, wantAux bool, createProgressReader func(io.ReadCloser) io.ReadCloser) backend.ProgressWriter {
    out = &syncWriter{w: out}

    var aux *streamformatter.AuxFormatter
    if wantAux {
        aux = &streamformatter.AuxFormatter{Writer: out}
    }

    return backend.ProgressWriter{
        Output:             out,
        StdoutFormatter:    streamformatter.NewStdoutWriter(out),
        StderrFormatter:    streamformatter.NewStderrWriter(out),
        AuxFormatter:       aux,
        ProgressReaderFunc: createProgressReader,
    }
}

type flusher interface {
    Flush()
}

func wrapOutputBufferedUntilRequestRead(rc io.ReadCloser, out io.Writer) (io.ReadCloser, io.Writer) {
    var fl flusher = &ioutils.NopFlusher{}
    if f, ok := out.(flusher); ok {
        fl = f
    }

    w := &wcf{
        buf:     bytes.NewBuffer(nil),
        Writer:  out,
        flusher: fl,
    }
    r := bufio.NewReader(rc)
    _, err := r.Peek(1)
    if err != nil {
        return rc, out
    }
    rc = &rcNotifier{
        Reader: r,
        Closer: rc,
        notify: w.notify,
    }
    return rc, w
}

type rcNotifier struct {
    io.Reader
    io.Closer
    notify func()
}

func (r *rcNotifier) Read(b []byte) (int, error) {
    n, err := r.Reader.Read(b)
    if err != nil {
        r.notify()
    }
    return n, err
}

func (r *rcNotifier) Close() error {
    r.notify()
    return r.Closer.Close()
}

type wcf struct {
    io.Writer
    flusher
    mu      sync.Mutex
    ready   bool
    buf     *bytes.Buffer
    flushed bool
}

func (w *wcf) Flush() {
    w.mu.Lock()
    w.flushed = true
    if !w.ready {
        w.mu.Unlock()
        return
    }
    w.mu.Unlock()
    w.flusher.Flush()
}

func (w *wcf) Flushed() bool {
    w.mu.Lock()
    b := w.flushed
    w.mu.Unlock()
    return b
}

func (w *wcf) Write(b []byte) (int, error) {
    w.mu.Lock()
    if !w.ready {
        n, err := w.buf.Write(b)
        w.mu.Unlock()
        return n, err
    }
    w.mu.Unlock()
    return w.Writer.Write(b)
}

func (w *wcf) notify() {
    w.mu.Lock()
    if !w.ready {
        if w.buf.Len() > 0 {
            _, _ = io.Copy(w.Writer, w.buf)
        }
        if w.flushed {
            w.flusher.Flush()
        }
        w.ready = true
    }
    w.mu.Unlock()
}