github.com/Heebron/moby@v0.0.0-20221111184709-6eab4f55faf7/api/server/router/build/build_routes.go (about)

     1  package build // import "github.com/docker/docker/api/server/router/build"
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"encoding/base64"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"net/http"
    12  	"runtime"
    13  	"strconv"
    14  	"strings"
    15  	"sync"
    16  
    17  	"github.com/docker/docker/api/server/httputils"
    18  	"github.com/docker/docker/api/types"
    19  	"github.com/docker/docker/api/types/backend"
    20  	"github.com/docker/docker/api/types/container"
    21  	"github.com/docker/docker/api/types/filters"
    22  	"github.com/docker/docker/api/types/registry"
    23  	"github.com/docker/docker/api/types/versions"
    24  	"github.com/docker/docker/pkg/ioutils"
    25  	"github.com/docker/docker/pkg/progress"
    26  	"github.com/docker/docker/pkg/streamformatter"
    27  	units "github.com/docker/go-units"
    28  	"github.com/pkg/errors"
    29  	"github.com/sirupsen/logrus"
    30  )
    31  
    32  type invalidParam struct {
    33  	error
    34  }
    35  
    36  func (e invalidParam) InvalidParameter() {}
    37  
    38  func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBuildOptions, error) {
    39  	options := &types.ImageBuildOptions{
    40  		Version:        types.BuilderV1, // Builder V1 is the default, but can be overridden
    41  		Dockerfile:     r.FormValue("dockerfile"),
    42  		SuppressOutput: httputils.BoolValue(r, "q"),
    43  		NoCache:        httputils.BoolValue(r, "nocache"),
    44  		ForceRemove:    httputils.BoolValue(r, "forcerm"),
    45  		MemorySwap:     httputils.Int64ValueOrZero(r, "memswap"),
    46  		Memory:         httputils.Int64ValueOrZero(r, "memory"),
    47  		CPUShares:      httputils.Int64ValueOrZero(r, "cpushares"),
    48  		CPUPeriod:      httputils.Int64ValueOrZero(r, "cpuperiod"),
    49  		CPUQuota:       httputils.Int64ValueOrZero(r, "cpuquota"),
    50  		CPUSetCPUs:     r.FormValue("cpusetcpus"),
    51  		CPUSetMems:     r.FormValue("cpusetmems"),
    52  		CgroupParent:   r.FormValue("cgroupparent"),
    53  		NetworkMode:    r.FormValue("networkmode"),
    54  		Tags:           r.Form["t"],
    55  		ExtraHosts:     r.Form["extrahosts"],
    56  		SecurityOpt:    r.Form["securityopt"],
    57  		Squash:         httputils.BoolValue(r, "squash"),
    58  		Target:         r.FormValue("target"),
    59  		RemoteContext:  r.FormValue("remote"),
    60  		SessionID:      r.FormValue("session"),
    61  		BuildID:        r.FormValue("buildid"),
    62  	}
    63  
    64  	if runtime.GOOS != "windows" && options.SecurityOpt != nil {
    65  		// SecurityOpt only supports "credentials-spec" on Windows, and not used on other platforms.
    66  		return nil, invalidParam{errors.New("security options are not supported on " + runtime.GOOS)}
    67  	}
    68  
    69  	version := httputils.VersionFromContext(ctx)
    70  	if httputils.BoolValue(r, "forcerm") && versions.GreaterThanOrEqualTo(version, "1.12") {
    71  		options.Remove = true
    72  	} else if r.FormValue("rm") == "" && versions.GreaterThanOrEqualTo(version, "1.12") {
    73  		options.Remove = true
    74  	} else {
    75  		options.Remove = httputils.BoolValue(r, "rm")
    76  	}
    77  	if httputils.BoolValue(r, "pull") && versions.GreaterThanOrEqualTo(version, "1.16") {
    78  		options.PullParent = true
    79  	}
    80  	if versions.GreaterThanOrEqualTo(version, "1.32") {
    81  		options.Platform = r.FormValue("platform")
    82  	}
    83  	if versions.GreaterThanOrEqualTo(version, "1.40") {
    84  		outputsJSON := r.FormValue("outputs")
    85  		if outputsJSON != "" {
    86  			var outputs []types.ImageBuildOutput
    87  			if err := json.Unmarshal([]byte(outputsJSON), &outputs); err != nil {
    88  				return nil, invalidParam{errors.Wrap(err, "invalid outputs specified")}
    89  			}
    90  			options.Outputs = outputs
    91  		}
    92  	}
    93  
    94  	if s := r.Form.Get("shmsize"); s != "" {
    95  		shmSize, err := strconv.ParseInt(s, 10, 64)
    96  		if err != nil {
    97  			return nil, err
    98  		}
    99  		options.ShmSize = shmSize
   100  	}
   101  
   102  	if i := r.FormValue("isolation"); i != "" {
   103  		options.Isolation = container.Isolation(i)
   104  		if !options.Isolation.IsValid() {
   105  			return nil, invalidParam{errors.Errorf("unsupported isolation: %q", i)}
   106  		}
   107  	}
   108  
   109  	if ulimitsJSON := r.FormValue("ulimits"); ulimitsJSON != "" {
   110  		var buildUlimits = []*units.Ulimit{}
   111  		if err := json.Unmarshal([]byte(ulimitsJSON), &buildUlimits); err != nil {
   112  			return nil, invalidParam{errors.Wrap(err, "error reading ulimit settings")}
   113  		}
   114  		options.Ulimits = buildUlimits
   115  	}
   116  
   117  	// Note that there are two ways a --build-arg might appear in the
   118  	// json of the query param:
   119  	//     "foo":"bar"
   120  	// and "foo":nil
   121  	// The first is the normal case, ie. --build-arg foo=bar
   122  	// or  --build-arg foo
   123  	// where foo's value was picked up from an env var.
   124  	// The second ("foo":nil) is where they put --build-arg foo
   125  	// but "foo" isn't set as an env var. In that case we can't just drop
   126  	// the fact they mentioned it, we need to pass that along to the builder
   127  	// so that it can print a warning about "foo" being unused if there is
   128  	// no "ARG foo" in the Dockerfile.
   129  	if buildArgsJSON := r.FormValue("buildargs"); buildArgsJSON != "" {
   130  		var buildArgs = map[string]*string{}
   131  		if err := json.Unmarshal([]byte(buildArgsJSON), &buildArgs); err != nil {
   132  			return nil, invalidParam{errors.Wrap(err, "error reading build args")}
   133  		}
   134  		options.BuildArgs = buildArgs
   135  	}
   136  
   137  	if labelsJSON := r.FormValue("labels"); labelsJSON != "" {
   138  		var labels = map[string]string{}
   139  		if err := json.Unmarshal([]byte(labelsJSON), &labels); err != nil {
   140  			return nil, invalidParam{errors.Wrap(err, "error reading labels")}
   141  		}
   142  		options.Labels = labels
   143  	}
   144  
   145  	if cacheFromJSON := r.FormValue("cachefrom"); cacheFromJSON != "" {
   146  		var cacheFrom = []string{}
   147  		if err := json.Unmarshal([]byte(cacheFromJSON), &cacheFrom); err != nil {
   148  			return nil, invalidParam{errors.Wrap(err, "error reading cache-from")}
   149  		}
   150  		options.CacheFrom = cacheFrom
   151  	}
   152  
   153  	if bv := r.FormValue("version"); bv != "" {
   154  		v, err := parseVersion(bv)
   155  		if err != nil {
   156  			return nil, err
   157  		}
   158  		options.Version = v
   159  	}
   160  
   161  	return options, nil
   162  }
   163  
   164  func parseVersion(s string) (types.BuilderVersion, error) {
   165  	switch types.BuilderVersion(s) {
   166  	case types.BuilderV1:
   167  		return types.BuilderV1, nil
   168  	case types.BuilderBuildKit:
   169  		return types.BuilderBuildKit, nil
   170  	default:
   171  		return "", invalidParam{errors.Errorf("invalid version %q", s)}
   172  	}
   173  }
   174  
   175  func (br *buildRouter) postPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   176  	if err := httputils.ParseForm(r); err != nil {
   177  		return err
   178  	}
   179  	fltrs, err := filters.FromJSON(r.Form.Get("filters"))
   180  	if err != nil {
   181  		return err
   182  	}
   183  	ksfv := r.FormValue("keep-storage")
   184  	if ksfv == "" {
   185  		ksfv = "0"
   186  	}
   187  	ks, err := strconv.Atoi(ksfv)
   188  	if err != nil {
   189  		return invalidParam{errors.Wrapf(err, "keep-storage is in bytes and expects an integer, got %v", ksfv)}
   190  	}
   191  
   192  	opts := types.BuildCachePruneOptions{
   193  		All:         httputils.BoolValue(r, "all"),
   194  		Filters:     fltrs,
   195  		KeepStorage: int64(ks),
   196  	}
   197  
   198  	report, err := br.backend.PruneCache(ctx, opts)
   199  	if err != nil {
   200  		return err
   201  	}
   202  	return httputils.WriteJSON(w, http.StatusOK, report)
   203  }
   204  
   205  func (br *buildRouter) postCancel(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   206  	w.Header().Set("Content-Type", "application/json")
   207  
   208  	id := r.FormValue("id")
   209  	if id == "" {
   210  		return invalidParam{errors.New("build ID not provided")}
   211  	}
   212  
   213  	return br.backend.Cancel(ctx, id)
   214  }
   215  
   216  func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
   217  	var (
   218  		notVerboseBuffer = bytes.NewBuffer(nil)
   219  		version          = httputils.VersionFromContext(ctx)
   220  	)
   221  
   222  	w.Header().Set("Content-Type", "application/json")
   223  
   224  	body := r.Body
   225  	var ww io.Writer = w
   226  	if body != nil {
   227  		// there is a possibility that output is written before request body
   228  		// has been fully read so we need to protect against it.
   229  		// this can be removed when
   230  		// https://github.com/golang/go/issues/15527
   231  		// https://github.com/golang/go/issues/22209
   232  		// has been fixed
   233  		body, ww = wrapOutputBufferedUntilRequestRead(body, ww)
   234  	}
   235  
   236  	output := ioutils.NewWriteFlusher(ww)
   237  	defer func() { _ = output.Close() }()
   238  
   239  	errf := func(err error) error {
   240  		if httputils.BoolValue(r, "q") && notVerboseBuffer.Len() > 0 {
   241  			_, _ = output.Write(notVerboseBuffer.Bytes())
   242  		}
   243  
   244  		// Do not write the error in the http output if it's still empty.
   245  		// This prevents from writing a 200(OK) when there is an internal error.
   246  		if !output.Flushed() {
   247  			return err
   248  		}
   249  		_, err = output.Write(streamformatter.FormatError(err))
   250  		if err != nil {
   251  			logrus.Warnf("could not write error response: %v", err)
   252  		}
   253  		return nil
   254  	}
   255  
   256  	buildOptions, err := newImageBuildOptions(ctx, r)
   257  	if err != nil {
   258  		return errf(err)
   259  	}
   260  	buildOptions.AuthConfigs = getAuthConfigs(r.Header)
   261  
   262  	if buildOptions.Squash && !br.daemon.HasExperimental() {
   263  		return invalidParam{errors.New("squash is only supported with experimental mode")}
   264  	}
   265  
   266  	out := io.Writer(output)
   267  	if buildOptions.SuppressOutput {
   268  		out = notVerboseBuffer
   269  	}
   270  
   271  	// Currently, only used if context is from a remote url.
   272  	// Look at code in DetectContextFromRemoteURL for more information.
   273  	createProgressReader := func(in io.ReadCloser) io.ReadCloser {
   274  		progressOutput := streamformatter.NewJSONProgressOutput(out, true)
   275  		return progress.NewProgressReader(in, progressOutput, r.ContentLength, "Downloading context", buildOptions.RemoteContext)
   276  	}
   277  
   278  	wantAux := versions.GreaterThanOrEqualTo(version, "1.30")
   279  
   280  	imgID, err := br.backend.Build(ctx, backend.BuildConfig{
   281  		Source:         body,
   282  		Options:        buildOptions,
   283  		ProgressWriter: buildProgressWriter(out, wantAux, createProgressReader),
   284  	})
   285  	if err != nil {
   286  		return errf(err)
   287  	}
   288  
   289  	// Everything worked so if -q was provided the output from the daemon
   290  	// should be just the image ID and we'll print that to stdout.
   291  	if buildOptions.SuppressOutput {
   292  		_, _ = fmt.Fprintln(streamformatter.NewStdoutWriter(output), imgID)
   293  	}
   294  	return nil
   295  }
   296  
   297  func getAuthConfigs(header http.Header) map[string]registry.AuthConfig {
   298  	authConfigs := map[string]registry.AuthConfig{}
   299  	authConfigsEncoded := header.Get("X-Registry-Config")
   300  
   301  	if authConfigsEncoded == "" {
   302  		return authConfigs
   303  	}
   304  
   305  	authConfigsJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authConfigsEncoded))
   306  	// Pulling an image does not error when no auth is provided so to remain
   307  	// consistent with the existing api decode errors are ignored
   308  	_ = json.NewDecoder(authConfigsJSON).Decode(&authConfigs)
   309  	return authConfigs
   310  }
   311  
   312  type syncWriter struct {
   313  	w  io.Writer
   314  	mu sync.Mutex
   315  }
   316  
   317  func (s *syncWriter) Write(b []byte) (count int, err error) {
   318  	s.mu.Lock()
   319  	count, err = s.w.Write(b)
   320  	s.mu.Unlock()
   321  	return
   322  }
   323  
   324  func buildProgressWriter(out io.Writer, wantAux bool, createProgressReader func(io.ReadCloser) io.ReadCloser) backend.ProgressWriter {
   325  	out = &syncWriter{w: out}
   326  
   327  	var aux *streamformatter.AuxFormatter
   328  	if wantAux {
   329  		aux = &streamformatter.AuxFormatter{Writer: out}
   330  	}
   331  
   332  	return backend.ProgressWriter{
   333  		Output:             out,
   334  		StdoutFormatter:    streamformatter.NewStdoutWriter(out),
   335  		StderrFormatter:    streamformatter.NewStderrWriter(out),
   336  		AuxFormatter:       aux,
   337  		ProgressReaderFunc: createProgressReader,
   338  	}
   339  }
   340  
   341  type flusher interface {
   342  	Flush()
   343  }
   344  
   345  func wrapOutputBufferedUntilRequestRead(rc io.ReadCloser, out io.Writer) (io.ReadCloser, io.Writer) {
   346  	var fl flusher = &ioutils.NopFlusher{}
   347  	if f, ok := out.(flusher); ok {
   348  		fl = f
   349  	}
   350  
   351  	w := &wcf{
   352  		buf:     bytes.NewBuffer(nil),
   353  		Writer:  out,
   354  		flusher: fl,
   355  	}
   356  	r := bufio.NewReader(rc)
   357  	_, err := r.Peek(1)
   358  	if err != nil {
   359  		return rc, out
   360  	}
   361  	rc = &rcNotifier{
   362  		Reader: r,
   363  		Closer: rc,
   364  		notify: w.notify,
   365  	}
   366  	return rc, w
   367  }
   368  
   369  type rcNotifier struct {
   370  	io.Reader
   371  	io.Closer
   372  	notify func()
   373  }
   374  
   375  func (r *rcNotifier) Read(b []byte) (int, error) {
   376  	n, err := r.Reader.Read(b)
   377  	if err != nil {
   378  		r.notify()
   379  	}
   380  	return n, err
   381  }
   382  
   383  func (r *rcNotifier) Close() error {
   384  	r.notify()
   385  	return r.Closer.Close()
   386  }
   387  
   388  type wcf struct {
   389  	io.Writer
   390  	flusher
   391  	mu      sync.Mutex
   392  	ready   bool
   393  	buf     *bytes.Buffer
   394  	flushed bool
   395  }
   396  
   397  func (w *wcf) Flush() {
   398  	w.mu.Lock()
   399  	w.flushed = true
   400  	if !w.ready {
   401  		w.mu.Unlock()
   402  		return
   403  	}
   404  	w.mu.Unlock()
   405  	w.flusher.Flush()
   406  }
   407  
   408  func (w *wcf) Flushed() bool {
   409  	w.mu.Lock()
   410  	b := w.flushed
   411  	w.mu.Unlock()
   412  	return b
   413  }
   414  
   415  func (w *wcf) Write(b []byte) (int, error) {
   416  	w.mu.Lock()
   417  	if !w.ready {
   418  		n, err := w.buf.Write(b)
   419  		w.mu.Unlock()
   420  		return n, err
   421  	}
   422  	w.mu.Unlock()
   423  	return w.Writer.Write(b)
   424  }
   425  
   426  func (w *wcf) notify() {
   427  	w.mu.Lock()
   428  	if !w.ready {
   429  		if w.buf.Len() > 0 {
   430  			_, _ = io.Copy(w.Writer, w.buf)
   431  		}
   432  		if w.flushed {
   433  			w.flusher.Flush()
   434  		}
   435  		w.ready = true
   436  	}
   437  	w.mu.Unlock()
   438  }