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