github.com/cookieai-jar/moby@v17.12.1-ce-rc2+incompatible/api/server/router/build/build_routes.go (about)

     1  package build
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"os"
    11  	"runtime"
    12  	"strconv"
    13  	"strings"
    14  	"sync"
    15  
    16  	"github.com/docker/docker/api/server/httputils"
    17  	"github.com/docker/docker/api/types"
    18  	"github.com/docker/docker/api/types/backend"
    19  	"github.com/docker/docker/api/types/container"
    20  	"github.com/docker/docker/api/types/versions"
    21  	"github.com/docker/docker/pkg/ioutils"
    22  	"github.com/docker/docker/pkg/progress"
    23  	"github.com/docker/docker/pkg/streamformatter"
    24  	"github.com/docker/docker/pkg/system"
    25  	units "github.com/docker/go-units"
    26  	"github.com/pkg/errors"
    27  	"github.com/sirupsen/logrus"
    28  	"golang.org/x/net/context"
    29  )
    30  
    31  type invalidIsolationError string
    32  
    33  func (e invalidIsolationError) Error() string {
    34  	return fmt.Sprintf("Unsupported isolation: %q", string(e))
    35  }
    36  
    37  func (e invalidIsolationError) InvalidParameter() {}
    38  
    39  func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBuildOptions, error) {
    40  	version := httputils.VersionFromContext(ctx)
    41  	options := &types.ImageBuildOptions{}
    42  	if httputils.BoolValue(r, "forcerm") && versions.GreaterThanOrEqualTo(version, "1.12") {
    43  		options.Remove = true
    44  	} else if r.FormValue("rm") == "" && versions.GreaterThanOrEqualTo(version, "1.12") {
    45  		options.Remove = true
    46  	} else {
    47  		options.Remove = httputils.BoolValue(r, "rm")
    48  	}
    49  	if httputils.BoolValue(r, "pull") && versions.GreaterThanOrEqualTo(version, "1.16") {
    50  		options.PullParent = true
    51  	}
    52  
    53  	options.Dockerfile = r.FormValue("dockerfile")
    54  	options.SuppressOutput = httputils.BoolValue(r, "q")
    55  	options.NoCache = httputils.BoolValue(r, "nocache")
    56  	options.ForceRemove = httputils.BoolValue(r, "forcerm")
    57  	options.MemorySwap = httputils.Int64ValueOrZero(r, "memswap")
    58  	options.Memory = httputils.Int64ValueOrZero(r, "memory")
    59  	options.CPUShares = httputils.Int64ValueOrZero(r, "cpushares")
    60  	options.CPUPeriod = httputils.Int64ValueOrZero(r, "cpuperiod")
    61  	options.CPUQuota = httputils.Int64ValueOrZero(r, "cpuquota")
    62  	options.CPUSetCPUs = r.FormValue("cpusetcpus")
    63  	options.CPUSetMems = r.FormValue("cpusetmems")
    64  	options.CgroupParent = r.FormValue("cgroupparent")
    65  	options.NetworkMode = r.FormValue("networkmode")
    66  	options.Tags = r.Form["t"]
    67  	options.ExtraHosts = r.Form["extrahosts"]
    68  	options.SecurityOpt = r.Form["securityopt"]
    69  	options.Squash = httputils.BoolValue(r, "squash")
    70  	options.Target = r.FormValue("target")
    71  	options.RemoteContext = r.FormValue("remote")
    72  	if versions.GreaterThanOrEqualTo(version, "1.32") {
    73  		// TODO @jhowardmsft. The following environment variable is an interim
    74  		// measure to allow the daemon to have a default platform if omitted by
    75  		// the client. This allows LCOW and WCOW to work with a down-level CLI
    76  		// for a short period of time, as the CLI changes can't be merged
    77  		// until after the daemon changes have been merged. Once the CLI is
    78  		// updated, this can be removed. PR for CLI is currently in
    79  		// https://github.com/docker/cli/pull/474.
    80  		apiPlatform := r.FormValue("platform")
    81  		if system.LCOWSupported() && apiPlatform == "" {
    82  			apiPlatform = os.Getenv("LCOW_API_PLATFORM_IF_OMITTED")
    83  		}
    84  		p := system.ParsePlatform(apiPlatform)
    85  		if err := system.ValidatePlatform(p); err != nil {
    86  			return nil, validationError{fmt.Errorf("invalid platform: %s", err)}
    87  		}
    88  		options.Platform = p.OS
    89  	}
    90  
    91  	if r.Form.Get("shmsize") != "" {
    92  		shmSize, err := strconv.ParseInt(r.Form.Get("shmsize"), 10, 64)
    93  		if err != nil {
    94  			return nil, err
    95  		}
    96  		options.ShmSize = shmSize
    97  	}
    98  
    99  	if i := container.Isolation(r.FormValue("isolation")); i != "" {
   100  		if !container.Isolation.IsValid(i) {
   101  			return nil, invalidIsolationError(i)
   102  		}
   103  		options.Isolation = i
   104  	}
   105  
   106  	if runtime.GOOS != "windows" && options.SecurityOpt != nil {
   107  		return nil, validationError{fmt.Errorf("The daemon on this platform does not support setting security options on build")}
   108  	}
   109  
   110  	var buildUlimits = []*units.Ulimit{}
   111  	ulimitsJSON := r.FormValue("ulimits")
   112  	if ulimitsJSON != "" {
   113  		if err := json.Unmarshal([]byte(ulimitsJSON), &buildUlimits); err != nil {
   114  			return nil, errors.Wrap(validationError{err}, "error reading ulimit settings")
   115  		}
   116  		options.Ulimits = buildUlimits
   117  	}
   118  
   119  	// Note that there are two ways a --build-arg might appear in the
   120  	// json of the query param:
   121  	//     "foo":"bar"
   122  	// and "foo":nil
   123  	// The first is the normal case, ie. --build-arg foo=bar
   124  	// or  --build-arg foo
   125  	// where foo's value was picked up from an env var.
   126  	// The second ("foo":nil) is where they put --build-arg foo
   127  	// but "foo" isn't set as an env var. In that case we can't just drop
   128  	// the fact they mentioned it, we need to pass that along to the builder
   129  	// so that it can print a warning about "foo" being unused if there is
   130  	// no "ARG foo" in the Dockerfile.
   131  	buildArgsJSON := r.FormValue("buildargs")
   132  	if buildArgsJSON != "" {
   133  		var buildArgs = map[string]*string{}
   134  		if err := json.Unmarshal([]byte(buildArgsJSON), &buildArgs); err != nil {
   135  			return nil, errors.Wrap(validationError{err}, "error reading build args")
   136  		}
   137  		options.BuildArgs = buildArgs
   138  	}
   139  
   140  	labelsJSON := r.FormValue("labels")
   141  	if labelsJSON != "" {
   142  		var labels = map[string]string{}
   143  		if err := json.Unmarshal([]byte(labelsJSON), &labels); err != nil {
   144  			return nil, errors.Wrap(validationError{err}, "error reading labels")
   145  		}
   146  		options.Labels = labels
   147  	}
   148  
   149  	cacheFromJSON := r.FormValue("cachefrom")
   150  	if cacheFromJSON != "" {
   151  		var cacheFrom = []string{}
   152  		if err := json.Unmarshal([]byte(cacheFromJSON), &cacheFrom); err != nil {
   153  			return nil, err
   154  		}
   155  		options.CacheFrom = cacheFrom
   156  	}
   157  	options.SessionID = r.FormValue("session")
   158  
   159  	return options, nil
   160  }
   161  
   162  func (br *buildRouter) postPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   163  	report, err := br.backend.PruneCache(ctx)
   164  	if err != nil {
   165  		return err
   166  	}
   167  	return httputils.WriteJSON(w, http.StatusOK, report)
   168  }
   169  
   170  type validationError struct {
   171  	cause error
   172  }
   173  
   174  func (e validationError) Error() string {
   175  	return e.cause.Error()
   176  }
   177  
   178  func (e validationError) InvalidParameter() {}
   179  
   180  func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   181  	var (
   182  		notVerboseBuffer = bytes.NewBuffer(nil)
   183  		version          = httputils.VersionFromContext(ctx)
   184  	)
   185  
   186  	w.Header().Set("Content-Type", "application/json")
   187  
   188  	output := ioutils.NewWriteFlusher(w)
   189  	defer output.Close()
   190  	errf := func(err error) error {
   191  		if httputils.BoolValue(r, "q") && notVerboseBuffer.Len() > 0 {
   192  			output.Write(notVerboseBuffer.Bytes())
   193  		}
   194  		// Do not write the error in the http output if it's still empty.
   195  		// This prevents from writing a 200(OK) when there is an internal error.
   196  		if !output.Flushed() {
   197  			return err
   198  		}
   199  		_, err = w.Write(streamformatter.FormatError(err))
   200  		if err != nil {
   201  			logrus.Warnf("could not write error response: %v", err)
   202  		}
   203  		return nil
   204  	}
   205  
   206  	buildOptions, err := newImageBuildOptions(ctx, r)
   207  	if err != nil {
   208  		return errf(err)
   209  	}
   210  	buildOptions.AuthConfigs = getAuthConfigs(r.Header)
   211  
   212  	if buildOptions.Squash && !br.daemon.HasExperimental() {
   213  		return validationError{errors.New("squash is only supported with experimental mode")}
   214  	}
   215  
   216  	out := io.Writer(output)
   217  	if buildOptions.SuppressOutput {
   218  		out = notVerboseBuffer
   219  	}
   220  
   221  	// Currently, only used if context is from a remote url.
   222  	// Look at code in DetectContextFromRemoteURL for more information.
   223  	createProgressReader := func(in io.ReadCloser) io.ReadCloser {
   224  		progressOutput := streamformatter.NewJSONProgressOutput(out, true)
   225  		return progress.NewProgressReader(in, progressOutput, r.ContentLength, "Downloading context", buildOptions.RemoteContext)
   226  	}
   227  
   228  	wantAux := versions.GreaterThanOrEqualTo(version, "1.30")
   229  
   230  	imgID, err := br.backend.Build(ctx, backend.BuildConfig{
   231  		Source:         r.Body,
   232  		Options:        buildOptions,
   233  		ProgressWriter: buildProgressWriter(out, wantAux, createProgressReader),
   234  	})
   235  	if err != nil {
   236  		return errf(err)
   237  	}
   238  
   239  	// Everything worked so if -q was provided the output from the daemon
   240  	// should be just the image ID and we'll print that to stdout.
   241  	if buildOptions.SuppressOutput {
   242  		fmt.Fprintln(streamformatter.NewStdoutWriter(output), imgID)
   243  	}
   244  	return nil
   245  }
   246  
   247  func getAuthConfigs(header http.Header) map[string]types.AuthConfig {
   248  	authConfigs := map[string]types.AuthConfig{}
   249  	authConfigsEncoded := header.Get("X-Registry-Config")
   250  
   251  	if authConfigsEncoded == "" {
   252  		return authConfigs
   253  	}
   254  
   255  	authConfigsJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authConfigsEncoded))
   256  	// Pulling an image does not error when no auth is provided so to remain
   257  	// consistent with the existing api decode errors are ignored
   258  	json.NewDecoder(authConfigsJSON).Decode(&authConfigs)
   259  	return authConfigs
   260  }
   261  
   262  type syncWriter struct {
   263  	w  io.Writer
   264  	mu sync.Mutex
   265  }
   266  
   267  func (s *syncWriter) Write(b []byte) (count int, err error) {
   268  	s.mu.Lock()
   269  	count, err = s.w.Write(b)
   270  	s.mu.Unlock()
   271  	return
   272  }
   273  
   274  func buildProgressWriter(out io.Writer, wantAux bool, createProgressReader func(io.ReadCloser) io.ReadCloser) backend.ProgressWriter {
   275  	out = &syncWriter{w: out}
   276  
   277  	var aux *streamformatter.AuxFormatter
   278  	if wantAux {
   279  		aux = &streamformatter.AuxFormatter{Writer: out}
   280  	}
   281  
   282  	return backend.ProgressWriter{
   283  		Output:             out,
   284  		StdoutFormatter:    streamformatter.NewStdoutWriter(out),
   285  		StderrFormatter:    streamformatter.NewStderrWriter(out),
   286  		AuxFormatter:       aux,
   287  		ProgressReaderFunc: createProgressReader,
   288  	}
   289  }