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 }