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