github.com/cookieai-jar/moby@v17.12.1-ce-rc2+incompatible/api/server/router/build/build_routes.go (about) 1 package build 2 3 import ( 4 "bytes" 5 "encoding/base64" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "os" 11 "runtime" 12 "strconv" 13 "strings" 14 "sync" 15 16 "github.com/docker/docker/api/server/httputils" 17 "github.com/docker/docker/api/types" 18 "github.com/docker/docker/api/types/backend" 19 "github.com/docker/docker/api/types/container" 20 "github.com/docker/docker/api/types/versions" 21 "github.com/docker/docker/pkg/ioutils" 22 "github.com/docker/docker/pkg/progress" 23 "github.com/docker/docker/pkg/streamformatter" 24 "github.com/docker/docker/pkg/system" 25 units "github.com/docker/go-units" 26 "github.com/pkg/errors" 27 "github.com/sirupsen/logrus" 28 "golang.org/x/net/context" 29 ) 30 31 type invalidIsolationError string 32 33 func (e invalidIsolationError) Error() string { 34 return fmt.Sprintf("Unsupported isolation: %q", string(e)) 35 } 36 37 func (e invalidIsolationError) InvalidParameter() {} 38 39 func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBuildOptions, error) { 40 version := httputils.VersionFromContext(ctx) 41 options := &types.ImageBuildOptions{} 42 if httputils.BoolValue(r, "forcerm") && versions.GreaterThanOrEqualTo(version, "1.12") { 43 options.Remove = true 44 } else if r.FormValue("rm") == "" && versions.GreaterThanOrEqualTo(version, "1.12") { 45 options.Remove = true 46 } else { 47 options.Remove = httputils.BoolValue(r, "rm") 48 } 49 if httputils.BoolValue(r, "pull") && versions.GreaterThanOrEqualTo(version, "1.16") { 50 options.PullParent = true 51 } 52 53 options.Dockerfile = r.FormValue("dockerfile") 54 options.SuppressOutput = httputils.BoolValue(r, "q") 55 options.NoCache = httputils.BoolValue(r, "nocache") 56 options.ForceRemove = httputils.BoolValue(r, "forcerm") 57 options.MemorySwap = httputils.Int64ValueOrZero(r, "memswap") 58 options.Memory = httputils.Int64ValueOrZero(r, "memory") 59 options.CPUShares = httputils.Int64ValueOrZero(r, "cpushares") 60 options.CPUPeriod = httputils.Int64ValueOrZero(r, "cpuperiod") 61 options.CPUQuota = httputils.Int64ValueOrZero(r, "cpuquota") 62 options.CPUSetCPUs = r.FormValue("cpusetcpus") 63 options.CPUSetMems = r.FormValue("cpusetmems") 64 options.CgroupParent = r.FormValue("cgroupparent") 65 options.NetworkMode = r.FormValue("networkmode") 66 options.Tags = r.Form["t"] 67 options.ExtraHosts = r.Form["extrahosts"] 68 options.SecurityOpt = r.Form["securityopt"] 69 options.Squash = httputils.BoolValue(r, "squash") 70 options.Target = r.FormValue("target") 71 options.RemoteContext = r.FormValue("remote") 72 if versions.GreaterThanOrEqualTo(version, "1.32") { 73 // TODO @jhowardmsft. The following environment variable is an interim 74 // measure to allow the daemon to have a default platform if omitted by 75 // the client. This allows LCOW and WCOW to work with a down-level CLI 76 // for a short period of time, as the CLI changes can't be merged 77 // until after the daemon changes have been merged. Once the CLI is 78 // updated, this can be removed. PR for CLI is currently in 79 // https://github.com/docker/cli/pull/474. 80 apiPlatform := r.FormValue("platform") 81 if system.LCOWSupported() && apiPlatform == "" { 82 apiPlatform = os.Getenv("LCOW_API_PLATFORM_IF_OMITTED") 83 } 84 p := system.ParsePlatform(apiPlatform) 85 if err := system.ValidatePlatform(p); err != nil { 86 return nil, validationError{fmt.Errorf("invalid platform: %s", err)} 87 } 88 options.Platform = p.OS 89 } 90 91 if r.Form.Get("shmsize") != "" { 92 shmSize, err := strconv.ParseInt(r.Form.Get("shmsize"), 10, 64) 93 if err != nil { 94 return nil, err 95 } 96 options.ShmSize = shmSize 97 } 98 99 if i := container.Isolation(r.FormValue("isolation")); i != "" { 100 if !container.Isolation.IsValid(i) { 101 return nil, invalidIsolationError(i) 102 } 103 options.Isolation = i 104 } 105 106 if runtime.GOOS != "windows" && options.SecurityOpt != nil { 107 return nil, validationError{fmt.Errorf("The daemon on this platform does not support setting security options on build")} 108 } 109 110 var buildUlimits = []*units.Ulimit{} 111 ulimitsJSON := r.FormValue("ulimits") 112 if ulimitsJSON != "" { 113 if err := json.Unmarshal([]byte(ulimitsJSON), &buildUlimits); err != nil { 114 return nil, errors.Wrap(validationError{err}, "error reading ulimit settings") 115 } 116 options.Ulimits = buildUlimits 117 } 118 119 // Note that there are two ways a --build-arg might appear in the 120 // json of the query param: 121 // "foo":"bar" 122 // and "foo":nil 123 // The first is the normal case, ie. --build-arg foo=bar 124 // or --build-arg foo 125 // where foo's value was picked up from an env var. 126 // The second ("foo":nil) is where they put --build-arg foo 127 // but "foo" isn't set as an env var. In that case we can't just drop 128 // the fact they mentioned it, we need to pass that along to the builder 129 // so that it can print a warning about "foo" being unused if there is 130 // no "ARG foo" in the Dockerfile. 131 buildArgsJSON := r.FormValue("buildargs") 132 if buildArgsJSON != "" { 133 var buildArgs = map[string]*string{} 134 if err := json.Unmarshal([]byte(buildArgsJSON), &buildArgs); err != nil { 135 return nil, errors.Wrap(validationError{err}, "error reading build args") 136 } 137 options.BuildArgs = buildArgs 138 } 139 140 labelsJSON := r.FormValue("labels") 141 if labelsJSON != "" { 142 var labels = map[string]string{} 143 if err := json.Unmarshal([]byte(labelsJSON), &labels); err != nil { 144 return nil, errors.Wrap(validationError{err}, "error reading labels") 145 } 146 options.Labels = labels 147 } 148 149 cacheFromJSON := r.FormValue("cachefrom") 150 if cacheFromJSON != "" { 151 var cacheFrom = []string{} 152 if err := json.Unmarshal([]byte(cacheFromJSON), &cacheFrom); err != nil { 153 return nil, err 154 } 155 options.CacheFrom = cacheFrom 156 } 157 options.SessionID = r.FormValue("session") 158 159 return options, nil 160 } 161 162 func (br *buildRouter) postPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 163 report, err := br.backend.PruneCache(ctx) 164 if err != nil { 165 return err 166 } 167 return httputils.WriteJSON(w, http.StatusOK, report) 168 } 169 170 type validationError struct { 171 cause error 172 } 173 174 func (e validationError) Error() string { 175 return e.cause.Error() 176 } 177 178 func (e validationError) InvalidParameter() {} 179 180 func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 181 var ( 182 notVerboseBuffer = bytes.NewBuffer(nil) 183 version = httputils.VersionFromContext(ctx) 184 ) 185 186 w.Header().Set("Content-Type", "application/json") 187 188 output := ioutils.NewWriteFlusher(w) 189 defer output.Close() 190 errf := func(err error) error { 191 if httputils.BoolValue(r, "q") && notVerboseBuffer.Len() > 0 { 192 output.Write(notVerboseBuffer.Bytes()) 193 } 194 // Do not write the error in the http output if it's still empty. 195 // This prevents from writing a 200(OK) when there is an internal error. 196 if !output.Flushed() { 197 return err 198 } 199 _, err = w.Write(streamformatter.FormatError(err)) 200 if err != nil { 201 logrus.Warnf("could not write error response: %v", err) 202 } 203 return nil 204 } 205 206 buildOptions, err := newImageBuildOptions(ctx, r) 207 if err != nil { 208 return errf(err) 209 } 210 buildOptions.AuthConfigs = getAuthConfigs(r.Header) 211 212 if buildOptions.Squash && !br.daemon.HasExperimental() { 213 return validationError{errors.New("squash is only supported with experimental mode")} 214 } 215 216 out := io.Writer(output) 217 if buildOptions.SuppressOutput { 218 out = notVerboseBuffer 219 } 220 221 // Currently, only used if context is from a remote url. 222 // Look at code in DetectContextFromRemoteURL for more information. 223 createProgressReader := func(in io.ReadCloser) io.ReadCloser { 224 progressOutput := streamformatter.NewJSONProgressOutput(out, true) 225 return progress.NewProgressReader(in, progressOutput, r.ContentLength, "Downloading context", buildOptions.RemoteContext) 226 } 227 228 wantAux := versions.GreaterThanOrEqualTo(version, "1.30") 229 230 imgID, err := br.backend.Build(ctx, backend.BuildConfig{ 231 Source: r.Body, 232 Options: buildOptions, 233 ProgressWriter: buildProgressWriter(out, wantAux, createProgressReader), 234 }) 235 if err != nil { 236 return errf(err) 237 } 238 239 // Everything worked so if -q was provided the output from the daemon 240 // should be just the image ID and we'll print that to stdout. 241 if buildOptions.SuppressOutput { 242 fmt.Fprintln(streamformatter.NewStdoutWriter(output), imgID) 243 } 244 return nil 245 } 246 247 func getAuthConfigs(header http.Header) map[string]types.AuthConfig { 248 authConfigs := map[string]types.AuthConfig{} 249 authConfigsEncoded := header.Get("X-Registry-Config") 250 251 if authConfigsEncoded == "" { 252 return authConfigs 253 } 254 255 authConfigsJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authConfigsEncoded)) 256 // Pulling an image does not error when no auth is provided so to remain 257 // consistent with the existing api decode errors are ignored 258 json.NewDecoder(authConfigsJSON).Decode(&authConfigs) 259 return authConfigs 260 } 261 262 type syncWriter struct { 263 w io.Writer 264 mu sync.Mutex 265 } 266 267 func (s *syncWriter) Write(b []byte) (count int, err error) { 268 s.mu.Lock() 269 count, err = s.w.Write(b) 270 s.mu.Unlock() 271 return 272 } 273 274 func buildProgressWriter(out io.Writer, wantAux bool, createProgressReader func(io.ReadCloser) io.ReadCloser) backend.ProgressWriter { 275 out = &syncWriter{w: out} 276 277 var aux *streamformatter.AuxFormatter 278 if wantAux { 279 aux = &streamformatter.AuxFormatter{Writer: out} 280 } 281 282 return backend.ProgressWriter{ 283 Output: out, 284 StdoutFormatter: streamformatter.NewStdoutWriter(out), 285 StderrFormatter: streamformatter.NewStderrWriter(out), 286 AuxFormatter: aux, 287 ProgressReaderFunc: createProgressReader, 288 } 289 }