github.com/jwhonce/docker@v0.6.7-0.20190327063223-da823cf3a5a3/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/versions" 23 "github.com/docker/docker/errdefs" 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 invalidIsolationError string 33 34 func (e invalidIsolationError) Error() string { 35 return fmt.Sprintf("Unsupported isolation: %q", string(e)) 36 } 37 38 func (e invalidIsolationError) InvalidParameter() {} 39 40 func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBuildOptions, error) { 41 version := httputils.VersionFromContext(ctx) 42 options := &types.ImageBuildOptions{} 43 if httputils.BoolValue(r, "forcerm") && versions.GreaterThanOrEqualTo(version, "1.12") { 44 options.Remove = true 45 } else if r.FormValue("rm") == "" && versions.GreaterThanOrEqualTo(version, "1.12") { 46 options.Remove = true 47 } else { 48 options.Remove = httputils.BoolValue(r, "rm") 49 } 50 if httputils.BoolValue(r, "pull") && versions.GreaterThanOrEqualTo(version, "1.16") { 51 options.PullParent = true 52 } 53 54 options.Dockerfile = r.FormValue("dockerfile") 55 options.SuppressOutput = httputils.BoolValue(r, "q") 56 options.NoCache = httputils.BoolValue(r, "nocache") 57 options.ForceRemove = httputils.BoolValue(r, "forcerm") 58 options.MemorySwap = httputils.Int64ValueOrZero(r, "memswap") 59 options.Memory = httputils.Int64ValueOrZero(r, "memory") 60 options.CPUShares = httputils.Int64ValueOrZero(r, "cpushares") 61 options.CPUPeriod = httputils.Int64ValueOrZero(r, "cpuperiod") 62 options.CPUQuota = httputils.Int64ValueOrZero(r, "cpuquota") 63 options.CPUSetCPUs = r.FormValue("cpusetcpus") 64 options.CPUSetMems = r.FormValue("cpusetmems") 65 options.CgroupParent = r.FormValue("cgroupparent") 66 options.NetworkMode = r.FormValue("networkmode") 67 options.Tags = r.Form["t"] 68 options.ExtraHosts = r.Form["extrahosts"] 69 options.SecurityOpt = r.Form["securityopt"] 70 options.Squash = httputils.BoolValue(r, "squash") 71 options.Target = r.FormValue("target") 72 options.RemoteContext = r.FormValue("remote") 73 if versions.GreaterThanOrEqualTo(version, "1.32") { 74 options.Platform = r.FormValue("platform") 75 } 76 77 if r.Form.Get("shmsize") != "" { 78 shmSize, err := strconv.ParseInt(r.Form.Get("shmsize"), 10, 64) 79 if err != nil { 80 return nil, err 81 } 82 options.ShmSize = shmSize 83 } 84 85 if i := container.Isolation(r.FormValue("isolation")); i != "" { 86 if !container.Isolation.IsValid(i) { 87 return nil, invalidIsolationError(i) 88 } 89 options.Isolation = i 90 } 91 92 if runtime.GOOS != "windows" && options.SecurityOpt != nil { 93 return nil, errdefs.InvalidParameter(errors.New("The daemon on this platform does not support setting security options on build")) 94 } 95 96 var buildUlimits = []*units.Ulimit{} 97 ulimitsJSON := r.FormValue("ulimits") 98 if ulimitsJSON != "" { 99 if err := json.Unmarshal([]byte(ulimitsJSON), &buildUlimits); err != nil { 100 return nil, errors.Wrap(errdefs.InvalidParameter(err), "error reading ulimit settings") 101 } 102 options.Ulimits = buildUlimits 103 } 104 105 // Note that there are two ways a --build-arg might appear in the 106 // json of the query param: 107 // "foo":"bar" 108 // and "foo":nil 109 // The first is the normal case, ie. --build-arg foo=bar 110 // or --build-arg foo 111 // where foo's value was picked up from an env var. 112 // The second ("foo":nil) is where they put --build-arg foo 113 // but "foo" isn't set as an env var. In that case we can't just drop 114 // the fact they mentioned it, we need to pass that along to the builder 115 // so that it can print a warning about "foo" being unused if there is 116 // no "ARG foo" in the Dockerfile. 117 buildArgsJSON := r.FormValue("buildargs") 118 if buildArgsJSON != "" { 119 var buildArgs = map[string]*string{} 120 if err := json.Unmarshal([]byte(buildArgsJSON), &buildArgs); err != nil { 121 return nil, errors.Wrap(errdefs.InvalidParameter(err), "error reading build args") 122 } 123 options.BuildArgs = buildArgs 124 } 125 126 labelsJSON := r.FormValue("labels") 127 if labelsJSON != "" { 128 var labels = map[string]string{} 129 if err := json.Unmarshal([]byte(labelsJSON), &labels); err != nil { 130 return nil, errors.Wrap(errdefs.InvalidParameter(err), "error reading labels") 131 } 132 options.Labels = labels 133 } 134 135 cacheFromJSON := r.FormValue("cachefrom") 136 if cacheFromJSON != "" { 137 var cacheFrom = []string{} 138 if err := json.Unmarshal([]byte(cacheFromJSON), &cacheFrom); err != nil { 139 return nil, err 140 } 141 options.CacheFrom = cacheFrom 142 } 143 options.SessionID = r.FormValue("session") 144 options.BuildID = r.FormValue("buildid") 145 builderVersion, err := parseVersion(r.FormValue("version")) 146 if err != nil { 147 return nil, err 148 } 149 options.Version = builderVersion 150 151 if versions.GreaterThanOrEqualTo(version, "1.40") { 152 outputsJSON := r.FormValue("outputs") 153 if outputsJSON != "" { 154 var outputs []types.ImageBuildOutput 155 if err := json.Unmarshal([]byte(outputsJSON), &outputs); err != nil { 156 return nil, err 157 } 158 options.Outputs = outputs 159 } 160 } 161 162 return options, nil 163 } 164 165 func parseVersion(s string) (types.BuilderVersion, error) { 166 if s == "" || s == string(types.BuilderV1) { 167 return types.BuilderV1, nil 168 } 169 if s == string(types.BuilderBuildKit) { 170 return types.BuilderBuildKit, nil 171 } 172 return "", errors.Errorf("invalid version %s", s) 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 filters, err := filters.FromJSON(r.Form.Get("filters")) 180 if err != nil { 181 return errors.Wrap(err, "could not parse filters") 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 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: filters, 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 errors.Errorf("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 output.Close() 238 239 errf := func(err error) error { 240 241 if httputils.BoolValue(r, "q") && notVerboseBuffer.Len() > 0 { 242 output.Write(notVerboseBuffer.Bytes()) 243 } 244 245 // Do not write the error in the http output if it's still empty. 246 // This prevents from writing a 200(OK) when there is an internal error. 247 if !output.Flushed() { 248 return err 249 } 250 _, err = output.Write(streamformatter.FormatError(err)) 251 if err != nil { 252 logrus.Warnf("could not write error response: %v", err) 253 } 254 return nil 255 } 256 257 buildOptions, err := newImageBuildOptions(ctx, r) 258 if err != nil { 259 return errf(err) 260 } 261 buildOptions.AuthConfigs = getAuthConfigs(r.Header) 262 263 if buildOptions.Squash && !br.daemon.HasExperimental() { 264 return errdefs.InvalidParameter(errors.New("squash is only supported with experimental mode")) 265 } 266 267 out := io.Writer(output) 268 if buildOptions.SuppressOutput { 269 out = notVerboseBuffer 270 } 271 272 // Currently, only used if context is from a remote url. 273 // Look at code in DetectContextFromRemoteURL for more information. 274 createProgressReader := func(in io.ReadCloser) io.ReadCloser { 275 progressOutput := streamformatter.NewJSONProgressOutput(out, true) 276 return progress.NewProgressReader(in, progressOutput, r.ContentLength, "Downloading context", buildOptions.RemoteContext) 277 } 278 279 wantAux := versions.GreaterThanOrEqualTo(version, "1.30") 280 281 imgID, err := br.backend.Build(ctx, backend.BuildConfig{ 282 Source: body, 283 Options: buildOptions, 284 ProgressWriter: buildProgressWriter(out, wantAux, createProgressReader), 285 }) 286 if err != nil { 287 return errf(err) 288 } 289 290 // Everything worked so if -q was provided the output from the daemon 291 // should be just the image ID and we'll print that to stdout. 292 if buildOptions.SuppressOutput { 293 fmt.Fprintln(streamformatter.NewStdoutWriter(output), imgID) 294 } 295 return nil 296 } 297 298 func getAuthConfigs(header http.Header) map[string]types.AuthConfig { 299 authConfigs := map[string]types.AuthConfig{} 300 authConfigsEncoded := header.Get("X-Registry-Config") 301 302 if authConfigsEncoded == "" { 303 return authConfigs 304 } 305 306 authConfigsJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authConfigsEncoded)) 307 // Pulling an image does not error when no auth is provided so to remain 308 // consistent with the existing api decode errors are ignored 309 json.NewDecoder(authConfigsJSON).Decode(&authConfigs) 310 return authConfigs 311 } 312 313 type syncWriter struct { 314 w io.Writer 315 mu sync.Mutex 316 } 317 318 func (s *syncWriter) Write(b []byte) (count int, err error) { 319 s.mu.Lock() 320 count, err = s.w.Write(b) 321 s.mu.Unlock() 322 return 323 } 324 325 func buildProgressWriter(out io.Writer, wantAux bool, createProgressReader func(io.ReadCloser) io.ReadCloser) backend.ProgressWriter { 326 out = &syncWriter{w: out} 327 328 var aux *streamformatter.AuxFormatter 329 if wantAux { 330 aux = &streamformatter.AuxFormatter{Writer: out} 331 } 332 333 return backend.ProgressWriter{ 334 Output: out, 335 StdoutFormatter: streamformatter.NewStdoutWriter(out), 336 StderrFormatter: streamformatter.NewStderrWriter(out), 337 AuxFormatter: aux, 338 ProgressReaderFunc: createProgressReader, 339 } 340 } 341 342 type flusher interface { 343 Flush() 344 } 345 346 func wrapOutputBufferedUntilRequestRead(rc io.ReadCloser, out io.Writer) (io.ReadCloser, io.Writer) { 347 var fl flusher = &ioutils.NopFlusher{} 348 if f, ok := out.(flusher); ok { 349 fl = f 350 } 351 352 w := &wcf{ 353 buf: bytes.NewBuffer(nil), 354 Writer: out, 355 flusher: fl, 356 } 357 r := bufio.NewReader(rc) 358 _, err := r.Peek(1) 359 if err != nil { 360 return rc, out 361 } 362 rc = &rcNotifier{ 363 Reader: r, 364 Closer: rc, 365 notify: w.notify, 366 } 367 return rc, w 368 } 369 370 type rcNotifier struct { 371 io.Reader 372 io.Closer 373 notify func() 374 } 375 376 func (r *rcNotifier) Read(b []byte) (int, error) { 377 n, err := r.Reader.Read(b) 378 if err != nil { 379 r.notify() 380 } 381 return n, err 382 } 383 384 func (r *rcNotifier) Close() error { 385 r.notify() 386 return r.Closer.Close() 387 } 388 389 type wcf struct { 390 io.Writer 391 flusher 392 mu sync.Mutex 393 ready bool 394 buf *bytes.Buffer 395 flushed bool 396 } 397 398 func (w *wcf) Flush() { 399 w.mu.Lock() 400 w.flushed = true 401 if !w.ready { 402 w.mu.Unlock() 403 return 404 } 405 w.mu.Unlock() 406 w.flusher.Flush() 407 } 408 409 func (w *wcf) Flushed() bool { 410 w.mu.Lock() 411 b := w.flushed 412 w.mu.Unlock() 413 return b 414 } 415 416 func (w *wcf) Write(b []byte) (int, error) { 417 w.mu.Lock() 418 if !w.ready { 419 n, err := w.buf.Write(b) 420 w.mu.Unlock() 421 return n, err 422 } 423 w.mu.Unlock() 424 return w.Writer.Write(b) 425 } 426 427 func (w *wcf) notify() { 428 w.mu.Lock() 429 if !w.ready { 430 if w.buf.Len() > 0 { 431 io.Copy(w.Writer, w.buf) 432 } 433 if w.flushed { 434 w.flusher.Flush() 435 } 436 w.ready = true 437 } 438 w.mu.Unlock() 439 }