mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
04203d13fb
- construct the initial options as a literal - move validation for windows up, and fail early - move all API-version handling together Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
440 lines
12 KiB
Go
440 lines
12 KiB
Go
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/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/versions"
|
|
"github.com/docker/docker/errdefs"
|
|
"github.com/docker/docker/pkg/ioutils"
|
|
"github.com/docker/docker/pkg/progress"
|
|
"github.com/docker/docker/pkg/streamformatter"
|
|
units "github.com/docker/go-units"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type invalidIsolationError string
|
|
|
|
func (e invalidIsolationError) Error() string {
|
|
return fmt.Sprintf("Unsupported isolation: %q", string(e))
|
|
}
|
|
|
|
func (e invalidIsolationError) 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"),
|
|
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 {
|
|
return nil, errdefs.InvalidParameter(errors.New("The daemon on this platform does not support setting security options on build"))
|
|
}
|
|
|
|
version := httputils.VersionFromContext(ctx)
|
|
if httputils.BoolValue(r, "forcerm") && versions.GreaterThanOrEqualTo(version, "1.12") {
|
|
options.Remove = true
|
|
} else if r.FormValue("rm") == "" && versions.GreaterThanOrEqualTo(version, "1.12") {
|
|
options.Remove = true
|
|
} else {
|
|
options.Remove = httputils.BoolValue(r, "rm")
|
|
}
|
|
if httputils.BoolValue(r, "pull") && versions.GreaterThanOrEqualTo(version, "1.16") {
|
|
options.PullParent = true
|
|
}
|
|
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, err
|
|
}
|
|
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, invalidIsolationError(options.Isolation)
|
|
}
|
|
}
|
|
|
|
if ulimitsJSON := r.FormValue("ulimits"); ulimitsJSON != "" {
|
|
var buildUlimits = []*units.Ulimit{}
|
|
if err := json.Unmarshal([]byte(ulimitsJSON), &buildUlimits); err != nil {
|
|
return nil, errors.Wrap(errdefs.InvalidParameter(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 != "" {
|
|
var buildArgs = map[string]*string{}
|
|
if err := json.Unmarshal([]byte(buildArgsJSON), &buildArgs); err != nil {
|
|
return nil, errors.Wrap(errdefs.InvalidParameter(err), "error reading build args")
|
|
}
|
|
options.BuildArgs = buildArgs
|
|
}
|
|
|
|
if labelsJSON := r.FormValue("labels"); labelsJSON != "" {
|
|
var labels = map[string]string{}
|
|
if err := json.Unmarshal([]byte(labelsJSON), &labels); err != nil {
|
|
return nil, errors.Wrap(errdefs.InvalidParameter(err), "error reading labels")
|
|
}
|
|
options.Labels = labels
|
|
}
|
|
|
|
if cacheFromJSON := r.FormValue("cachefrom"); cacheFromJSON != "" {
|
|
var cacheFrom = []string{}
|
|
if err := json.Unmarshal([]byte(cacheFromJSON), &cacheFrom); err != nil {
|
|
return nil, err
|
|
}
|
|
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 "", 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 errors.Wrap(err, "could not parse filters")
|
|
}
|
|
ksfv := r.FormValue("keep-storage")
|
|
if ksfv == "" {
|
|
ksfv = "0"
|
|
}
|
|
ks, err := strconv.Atoi(ksfv)
|
|
if err != nil {
|
|
return 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 errors.Errorf("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 {
|
|
logrus.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 errdefs.InvalidParameter(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]types.AuthConfig {
|
|
authConfigs := map[string]types.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()
|
|
}
|