diff --git a/api/server/server.go b/api/server/server.go index 50dc84ff7f..65e1391670 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -23,6 +23,7 @@ import ( "github.com/docker/docker/api" "github.com/docker/docker/api/types" "github.com/docker/docker/autogen/dockerversion" + "github.com/docker/docker/builder" "github.com/docker/docker/daemon" "github.com/docker/docker/daemon/networkdriver/bridge" "github.com/docker/docker/engine" @@ -236,12 +237,12 @@ func writeJSON(w http.ResponseWriter, code int, v interface{}) error { return json.NewEncoder(w).Encode(v) } -func streamJSON(job *engine.Job, w http.ResponseWriter, flush bool) { +func streamJSON(out *engine.Output, w http.ResponseWriter, flush bool) { w.Header().Set("Content-Type", "application/json") if flush { - job.Stdout.Add(utils.NewWriteFlusher(w)) + out.Add(utils.NewWriteFlusher(w)) } else { - job.Stdout.Add(w) + out.Add(w) } } @@ -857,7 +858,7 @@ func (s *Server) postImagesPush(eng *engine.Engine, version version.Version, w h job.Setenv("tag", r.Form.Get("tag")) if version.GreaterThan("1.0") { job.SetenvBool("json", true) - streamJSON(job, w, true) + streamJSON(job.Stdout, w, true) } else { job.Stdout.Add(utils.NewWriteFlusher(w)) } @@ -1207,7 +1208,7 @@ func (s *Server) getContainersByName(eng *engine.Engine, version version.Version if version.LessThan("1.12") { job.SetenvBool("raw", true) } - streamJSON(job, w, false) + streamJSON(job.Stdout, w, false) return job.Run() } @@ -1232,7 +1233,7 @@ func (s *Server) getImagesByName(eng *engine.Engine, version version.Version, w if version.LessThan("1.12") { job.SetenvBool("raw", true) } - streamJSON(job, w, false) + streamJSON(job.Stdout, w, false) return job.Run() } @@ -1245,9 +1246,11 @@ func (s *Server) postBuild(eng *engine.Engine, version version.Version, w http.R authConfig = ®istry.AuthConfig{} configFileEncoded = r.Header.Get("X-Registry-Config") configFile = ®istry.ConfigFile{} - job = eng.Job("build") + job = builder.NewBuildConfig(eng.Logging, eng.Stderr) ) + b := &builder.BuilderJob{eng, getDaemon(eng)} + // This block can be removed when API versions prior to 1.9 are deprecated. // Both headers will be parsed and sent along to the daemon, but if a non-empty // ConfigFile is present, any value provided as an AuthConfig directly will @@ -1271,36 +1274,38 @@ func (s *Server) postBuild(eng *engine.Engine, version version.Version, w http.R } if version.GreaterThanOrEqualTo("1.8") { - job.SetenvBool("json", true) - streamJSON(job, w, true) + job.JSONFormat = true + streamJSON(job.Stdout, w, true) } else { job.Stdout.Add(utils.NewWriteFlusher(w)) } if toBool(r.FormValue("forcerm")) && version.GreaterThanOrEqualTo("1.12") { - job.Setenv("rm", "1") + job.Remove = true } else if r.FormValue("rm") == "" && version.GreaterThanOrEqualTo("1.12") { - job.Setenv("rm", "1") + job.Remove = true } else { - job.Setenv("rm", r.FormValue("rm")) + job.Remove = toBool(r.FormValue("rm")) } if toBool(r.FormValue("pull")) && version.GreaterThanOrEqualTo("1.16") { - job.Setenv("pull", "1") + job.Pull = true } job.Stdin.Add(r.Body) - job.Setenv("remote", r.FormValue("remote")) - job.Setenv("dockerfile", r.FormValue("dockerfile")) - job.Setenv("t", r.FormValue("t")) - job.Setenv("q", r.FormValue("q")) - job.Setenv("nocache", r.FormValue("nocache")) - job.Setenv("forcerm", r.FormValue("forcerm")) - job.SetenvJson("authConfig", authConfig) - job.SetenvJson("configFile", configFile) - job.Setenv("memswap", r.FormValue("memswap")) - job.Setenv("memory", r.FormValue("memory")) - job.Setenv("cpusetcpus", r.FormValue("cpusetcpus")) - job.Setenv("cpusetmems", r.FormValue("cpusetmems")) - job.Setenv("cpushares", r.FormValue("cpushares")) + + // FIXME(calavera): !!!!! Remote might not be used. Solve the mistery before merging + //job.Setenv("remote", r.FormValue("remote")) + job.DockerfileName = r.FormValue("dockerfile") + job.RepoName = r.FormValue("t") + job.SuppressOutput = toBool(r.FormValue("q")) + job.NoCache = toBool(r.FormValue("nocache")) + job.ForceRemove = toBool(r.FormValue("forcerm")) + job.AuthConfig = authConfig + job.ConfigFile = configFile + job.MemorySwap = toInt64(r.FormValue("memswap")) + job.Memory = toInt64(r.FormValue("memory")) + job.CpuShares = toInt64(r.FormValue("cpushares")) + job.CpuSetCpus = r.FormValue("cpusetcpus") + job.CpuSetMems = r.FormValue("cpusetmems") // Job cancellation. Note: not all job types support this. if closeNotifier, ok := w.(http.CloseNotifier); ok { @@ -1310,13 +1315,13 @@ func (s *Server) postBuild(eng *engine.Engine, version version.Version, w http.R select { case <-finished: case <-closeNotifier.CloseNotify(): - logrus.Infof("Client disconnected, cancelling job: %s", job.Name) + logrus.Infof("Client disconnected, cancelling job: build") job.Cancel() } }() } - if err := job.Run(); err != nil { + if err := b.CmdBuild(job); err != nil { if !job.Stdout.Used() { return err } @@ -1676,3 +1681,12 @@ func toBool(s string) bool { s = strings.ToLower(strings.TrimSpace(s)) return !(s == "" || s == "0" || s == "no" || s == "false" || s == "none") } + +// FIXME(calavera): This is a copy of the Env.GetInt64 +func toInt64(s string) int64 { + val, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return 0 + } + return val +} diff --git a/builder/job.go b/builder/job.go index 930d7b7f16..6ad64d716b 100644 --- a/builder/job.go +++ b/builder/job.go @@ -9,6 +9,7 @@ import ( "os" "os/exec" "strings" + "sync" "github.com/docker/docker/api" "github.com/docker/docker/builder/parser" @@ -17,6 +18,7 @@ import ( "github.com/docker/docker/graph" "github.com/docker/docker/pkg/archive" "github.com/docker/docker/pkg/httputils" + "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/parsers" "github.com/docker/docker/pkg/streamformatter" "github.com/docker/docker/pkg/urlutil" @@ -41,41 +43,73 @@ type BuilderJob struct { Daemon *daemon.Daemon } +type Config struct { + DockerfileName string + RemoteURL string + RepoName string + SuppressOutput bool + NoCache bool + Remove bool + ForceRemove bool + Pull bool + JSONFormat bool + Memory int64 + MemorySwap int64 + CpuShares int64 + CpuSetCpus string + CpuSetMems string + AuthConfig *registry.AuthConfig + ConfigFile *registry.ConfigFile + + Stdout *engine.Output + Stderr *engine.Output + Stdin *engine.Input + // When closed, the job has been cancelled. + // Note: not all jobs implement cancellation. + // See Job.Cancel() and Job.WaitCancelled() + cancelled chan struct{} + cancelOnce sync.Once +} + +// When called, causes the Job.WaitCancelled channel to unblock. +func (b *Config) Cancel() { + b.cancelOnce.Do(func() { + close(b.cancelled) + }) +} + +// Returns a channel which is closed ("never blocks") when the job is cancelled. +func (b *Config) WaitCancelled() <-chan struct{} { + return b.cancelled +} + +func NewBuildConfig(logging bool, err io.Writer) *Config { + c := &Config{ + Stdout: engine.NewOutput(), + Stderr: engine.NewOutput(), + Stdin: engine.NewInput(), + cancelled: make(chan struct{}), + } + if logging { + c.Stderr.Add(ioutils.NopWriteCloser(err)) + } + return c +} + func (b *BuilderJob) Install() { - b.Engine.Register("build", b.CmdBuild) b.Engine.Register("build_config", b.CmdBuildConfig) } -func (b *BuilderJob) CmdBuild(job *engine.Job) error { - if len(job.Args) != 0 { - return fmt.Errorf("Usage: %s\n", job.Name) - } +func (b *BuilderJob) CmdBuild(buildConfig *Config) error { var ( - dockerfileName = job.Getenv("dockerfile") - remoteURL = job.Getenv("remote") - repoName = job.Getenv("t") - suppressOutput = job.GetenvBool("q") - noCache = job.GetenvBool("nocache") - rm = job.GetenvBool("rm") - forceRm = job.GetenvBool("forcerm") - pull = job.GetenvBool("pull") - memory = job.GetenvInt64("memory") - memorySwap = job.GetenvInt64("memswap") - cpuShares = job.GetenvInt64("cpushares") - cpuSetCpus = job.Getenv("cpusetcpus") - cpuSetMems = job.Getenv("cpusetmems") - authConfig = ®istry.AuthConfig{} - configFile = ®istry.ConfigFile{} - tag string - context io.ReadCloser + repoName string + tag string + context io.ReadCloser ) - job.GetenvJson("authConfig", authConfig) - job.GetenvJson("configFile", configFile) - - repoName, tag = parsers.ParseRepositoryTag(repoName) + repoName, tag = parsers.ParseRepositoryTag(buildConfig.RepoName) if repoName != "" { - if err := registry.ValidateRepositoryName(repoName); err != nil { + if err := registry.ValidateRepositoryName(buildConfig.RepoName); err != nil { return err } if len(tag) > 0 { @@ -85,11 +119,11 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) error { } } - if remoteURL == "" { - context = ioutil.NopCloser(job.Stdin) - } else if urlutil.IsGitURL(remoteURL) { - if !urlutil.IsGitTransport(remoteURL) { - remoteURL = "https://" + remoteURL + if buildConfig.RemoteURL == "" { + context = ioutil.NopCloser(buildConfig.Stdin) + } else if urlutil.IsGitURL(buildConfig.RemoteURL) { + if !urlutil.IsGitTransport(buildConfig.RemoteURL) { + buildConfig.RemoteURL = "https://" + buildConfig.RemoteURL } root, err := ioutil.TempDir("", "docker-build-git") if err != nil { @@ -97,7 +131,7 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) error { } defer os.RemoveAll(root) - if output, err := exec.Command("git", "clone", "--recursive", remoteURL, root).CombinedOutput(); err != nil { + if output, err := exec.Command("git", "clone", "--recursive", buildConfig.RemoteURL, root).CombinedOutput(); err != nil { return fmt.Errorf("Error trying to use git: %s (%s)", err, output) } @@ -106,8 +140,8 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) error { return err } context = c - } else if urlutil.IsURL(remoteURL) { - f, err := httputils.Download(remoteURL) + } else if urlutil.IsURL(buildConfig.RemoteURL) { + f, err := httputils.Download(buildConfig.RemoteURL) if err != nil { return err } @@ -119,9 +153,9 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) error { // When we're downloading just a Dockerfile put it in // the default name - don't allow the client to move/specify it - dockerfileName = api.DefaultDockerfileName + buildConfig.DockerfileName = api.DefaultDockerfileName - c, err := archive.Generate(dockerfileName, string(dockerFile)) + c, err := archive.Generate(buildConfig.DockerfileName, string(dockerFile)) if err != nil { return err } @@ -129,35 +163,35 @@ func (b *BuilderJob) CmdBuild(job *engine.Job) error { } defer context.Close() - sf := streamformatter.NewStreamFormatter(job.GetenvBool("json")) + sf := streamformatter.NewStreamFormatter(buildConfig.JSONFormat) builder := &Builder{ Daemon: b.Daemon, Engine: b.Engine, OutStream: &streamformatter.StdoutFormater{ - Writer: job.Stdout, + Writer: buildConfig.Stdout, StreamFormatter: sf, }, ErrStream: &streamformatter.StderrFormater{ - Writer: job.Stdout, + Writer: buildConfig.Stdout, StreamFormatter: sf, }, - Verbose: !suppressOutput, - UtilizeCache: !noCache, - Remove: rm, - ForceRemove: forceRm, - Pull: pull, - OutOld: job.Stdout, + Verbose: !buildConfig.SuppressOutput, + UtilizeCache: !buildConfig.NoCache, + Remove: buildConfig.Remove, + ForceRemove: buildConfig.ForceRemove, + Pull: buildConfig.Pull, + OutOld: buildConfig.Stdout, StreamFormatter: sf, - AuthConfig: authConfig, - ConfigFile: configFile, - dockerfileName: dockerfileName, - cpuShares: cpuShares, - cpuSetCpus: cpuSetCpus, - cpuSetMems: cpuSetMems, - memory: memory, - memorySwap: memorySwap, - cancelled: job.WaitCancelled(), + AuthConfig: buildConfig.AuthConfig, + ConfigFile: buildConfig.ConfigFile, + dockerfileName: buildConfig.DockerfileName, + cpuShares: buildConfig.CpuShares, + cpuSetCpus: buildConfig.CpuSetCpus, + cpuSetMems: buildConfig.CpuSetMems, + memory: buildConfig.Memory, + memorySwap: buildConfig.MemorySwap, + cancelled: buildConfig.WaitCancelled(), } id, err := builder.Run(context)