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