diff --git a/api/server/router/build/backend.go b/api/server/router/build/backend.go new file mode 100644 index 0000000000..fd9e314a03 --- /dev/null +++ b/api/server/router/build/backend.go @@ -0,0 +1,12 @@ +package build + +// Backend abstracts an image builder whose only purpose is to build an image referenced by an imageID. +type Backend interface { + // Build builds a Docker image referenced by an imageID string. + // + // Note: Tagging an image should not be done by a Builder, it should instead be done + // by the caller. + // + // TODO: make this return a reference instead of string + Build() (imageID string) +} diff --git a/api/server/router/build/build.go b/api/server/router/build/build.go new file mode 100644 index 0000000000..e21b634609 --- /dev/null +++ b/api/server/router/build/build.go @@ -0,0 +1,33 @@ +package build + +import ( + "github.com/docker/docker/api/server/router" + "github.com/docker/docker/api/server/router/local" + "github.com/docker/docker/daemon" +) + +// buildRouter is a router to talk with the build controller +type buildRouter struct { + backend *daemon.Daemon + routes []router.Route +} + +// NewRouter initializes a new build router +func NewRouter(b *daemon.Daemon) router.Router { + r := &buildRouter{ + backend: b, + } + r.initRoutes() + return r +} + +// Routes returns the available routers to the build controller +func (r *buildRouter) Routes() []router.Route { + return r.routes +} + +func (r *buildRouter) initRoutes() { + r.routes = []router.Route{ + local.NewPostRoute("/build", r.postBuild), + } +} diff --git a/api/server/router/build/build_routes.go b/api/server/router/build/build_routes.go new file mode 100644 index 0000000000..c834b077a0 --- /dev/null +++ b/api/server/router/build/build_routes.go @@ -0,0 +1,239 @@ +package build + +import ( + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strconv" + "strings" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/api/server/httputils" + "github.com/docker/docker/api/types" + "github.com/docker/docker/builder" + "github.com/docker/docker/builder/dockerfile" + "github.com/docker/docker/daemon/daemonbuilder" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/chrootarchive" + "github.com/docker/docker/pkg/ioutils" + "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/pkg/streamformatter" + "github.com/docker/docker/pkg/ulimit" + "github.com/docker/docker/reference" + "github.com/docker/docker/runconfig" + "github.com/docker/docker/utils" + "golang.org/x/net/context" +) + +// sanitizeRepoAndTags parses the raw "t" parameter received from the client +// to a slice of repoAndTag. +// It also validates each repoName and tag. +func sanitizeRepoAndTags(names []string) ([]reference.Named, error) { + var ( + repoAndTags []reference.Named + // This map is used for deduplicating the "-t" parameter. + uniqNames = make(map[string]struct{}) + ) + for _, repo := range names { + if repo == "" { + continue + } + + ref, err := reference.ParseNamed(repo) + if err != nil { + return nil, err + } + + ref = reference.WithDefaultTag(ref) + + if _, isCanonical := ref.(reference.Canonical); isCanonical { + return nil, errors.New("build tag cannot contain a digest") + } + + if _, isTagged := ref.(reference.NamedTagged); !isTagged { + ref, err = reference.WithTag(ref, reference.DefaultTag) + } + + nameWithTag := ref.String() + + if _, exists := uniqNames[nameWithTag]; !exists { + uniqNames[nameWithTag] = struct{}{} + repoAndTags = append(repoAndTags, ref) + } + } + return repoAndTags, nil +} + +func (br *buildRouter) postBuild(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + var ( + authConfigs = map[string]types.AuthConfig{} + authConfigsEncoded = r.Header.Get("X-Registry-Config") + buildConfig = &dockerfile.Config{} + ) + + if authConfigsEncoded != "" { + authConfigsJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authConfigsEncoded)) + if err := json.NewDecoder(authConfigsJSON).Decode(&authConfigs); err != nil { + // for a pull it is not an error if no auth was given + // to increase compatibility with the existing api it is defaulting + // to be empty. + } + } + + w.Header().Set("Content-Type", "application/json") + + version := httputils.VersionFromContext(ctx) + output := ioutils.NewWriteFlusher(w) + defer output.Close() + sf := streamformatter.NewJSONStreamFormatter() + errf := func(err error) error { + // Do not write the error in the http output if it's still empty. + // This prevents from writing a 200(OK) when there is an internal error. + if !output.Flushed() { + return err + } + _, err = w.Write(sf.FormatError(errors.New(utils.GetErrorMessage(err)))) + if err != nil { + logrus.Warnf("could not write error response: %v", err) + } + return nil + } + + if httputils.BoolValue(r, "forcerm") && version.GreaterThanOrEqualTo("1.12") { + buildConfig.Remove = true + } else if r.FormValue("rm") == "" && version.GreaterThanOrEqualTo("1.12") { + buildConfig.Remove = true + } else { + buildConfig.Remove = httputils.BoolValue(r, "rm") + } + if httputils.BoolValue(r, "pull") && version.GreaterThanOrEqualTo("1.16") { + buildConfig.Pull = true + } + + repoAndTags, err := sanitizeRepoAndTags(r.Form["t"]) + if err != nil { + return errf(err) + } + + buildConfig.DockerfileName = r.FormValue("dockerfile") + buildConfig.Verbose = !httputils.BoolValue(r, "q") + buildConfig.UseCache = !httputils.BoolValue(r, "nocache") + buildConfig.ForceRemove = httputils.BoolValue(r, "forcerm") + buildConfig.MemorySwap = httputils.Int64ValueOrZero(r, "memswap") + buildConfig.Memory = httputils.Int64ValueOrZero(r, "memory") + buildConfig.CPUShares = httputils.Int64ValueOrZero(r, "cpushares") + buildConfig.CPUPeriod = httputils.Int64ValueOrZero(r, "cpuperiod") + buildConfig.CPUQuota = httputils.Int64ValueOrZero(r, "cpuquota") + buildConfig.CPUSetCpus = r.FormValue("cpusetcpus") + buildConfig.CPUSetMems = r.FormValue("cpusetmems") + buildConfig.CgroupParent = r.FormValue("cgroupparent") + + if r.Form.Get("shmsize") != "" { + shmSize, err := strconv.ParseInt(r.Form.Get("shmsize"), 10, 64) + if err != nil { + return errf(err) + } + buildConfig.ShmSize = &shmSize + } + + if i := runconfig.IsolationLevel(r.FormValue("isolation")); i != "" { + if !runconfig.IsolationLevel.IsValid(i) { + return errf(fmt.Errorf("Unsupported isolation: %q", i)) + } + buildConfig.Isolation = i + } + + var buildUlimits = []*ulimit.Ulimit{} + ulimitsJSON := r.FormValue("ulimits") + if ulimitsJSON != "" { + if err := json.NewDecoder(strings.NewReader(ulimitsJSON)).Decode(&buildUlimits); err != nil { + return errf(err) + } + buildConfig.Ulimits = buildUlimits + } + + var buildArgs = map[string]string{} + buildArgsJSON := r.FormValue("buildargs") + if buildArgsJSON != "" { + if err := json.NewDecoder(strings.NewReader(buildArgsJSON)).Decode(&buildArgs); err != nil { + return errf(err) + } + buildConfig.BuildArgs = buildArgs + } + + remoteURL := r.FormValue("remote") + + // Currently, only used if context is from a remote url. + // Look at code in DetectContextFromRemoteURL for more information. + createProgressReader := func(in io.ReadCloser) io.ReadCloser { + progressOutput := sf.NewProgressOutput(output, true) + return progress.NewProgressReader(in, progressOutput, r.ContentLength, "Downloading context", remoteURL) + } + + var ( + context builder.ModifiableContext + dockerfileName string + ) + context, dockerfileName, err = daemonbuilder.DetectContextFromRemoteURL(r.Body, remoteURL, createProgressReader) + if err != nil { + return errf(err) + } + defer func() { + if err := context.Close(); err != nil { + logrus.Debugf("[BUILDER] failed to remove temporary context: %v", err) + } + }() + + uidMaps, gidMaps := br.backend.GetUIDGIDMaps() + defaultArchiver := &archive.Archiver{ + Untar: chrootarchive.Untar, + UIDMaps: uidMaps, + GIDMaps: gidMaps, + } + docker := &daemonbuilder.Docker{ + Daemon: br.backend, + OutOld: output, + AuthConfigs: authConfigs, + Archiver: defaultArchiver, + } + + b, err := dockerfile.NewBuilder(buildConfig, docker, builder.DockerIgnoreContext{ModifiableContext: context}, nil) + if err != nil { + return errf(err) + } + b.Stdout = &streamformatter.StdoutFormatter{Writer: output, StreamFormatter: sf} + b.Stderr = &streamformatter.StderrFormatter{Writer: output, StreamFormatter: sf} + + if closeNotifier, ok := w.(http.CloseNotifier); ok { + finished := make(chan struct{}) + defer close(finished) + go func() { + select { + case <-finished: + case <-closeNotifier.CloseNotify(): + logrus.Infof("Client disconnected, cancelling job: build") + b.Cancel() + } + }() + } + + if len(dockerfileName) > 0 { + b.DockerfileName = dockerfileName + } + + imgID, err := b.Build() + if err != nil { + return errf(err) + } + + for _, rt := range repoAndTags { + if err := br.backend.TagImage(rt, imgID); err != nil { + return errf(err) + } + } + + return nil +} diff --git a/api/server/router/local/image.go b/api/server/router/local/image.go index c66a8d8f8b..ffd7bbb8f6 100644 --- a/api/server/router/local/image.go +++ b/api/server/router/local/image.go @@ -7,26 +7,17 @@ import ( "fmt" "io" "net/http" - "strconv" "strings" - "github.com/Sirupsen/logrus" "github.com/docker/distribution/digest" "github.com/docker/docker/api/server/httputils" "github.com/docker/docker/api/types" - "github.com/docker/docker/builder" "github.com/docker/docker/builder/dockerfile" - "github.com/docker/docker/daemon/daemonbuilder" derr "github.com/docker/docker/errors" - "github.com/docker/docker/pkg/archive" - "github.com/docker/docker/pkg/chrootarchive" "github.com/docker/docker/pkg/ioutils" - "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" - "github.com/docker/docker/pkg/ulimit" "github.com/docker/docker/reference" "github.com/docker/docker/runconfig" - "github.com/docker/docker/utils" "golang.org/x/net/context" ) @@ -306,211 +297,6 @@ func (s *router) getImagesByName(ctx context.Context, w http.ResponseWriter, r * return httputils.WriteJSON(w, http.StatusOK, imageInspect) } -func (s *router) postBuild(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { - var ( - authConfigs = map[string]types.AuthConfig{} - authConfigsEncoded = r.Header.Get("X-Registry-Config") - buildConfig = &dockerfile.Config{} - ) - - if authConfigsEncoded != "" { - authConfigsJSON := base64.NewDecoder(base64.URLEncoding, strings.NewReader(authConfigsEncoded)) - if err := json.NewDecoder(authConfigsJSON).Decode(&authConfigs); err != nil { - // for a pull it is not an error if no auth was given - // to increase compatibility with the existing api it is defaulting - // to be empty. - } - } - - w.Header().Set("Content-Type", "application/json") - - version := httputils.VersionFromContext(ctx) - output := ioutils.NewWriteFlusher(w) - defer output.Close() - sf := streamformatter.NewJSONStreamFormatter() - errf := func(err error) error { - // Do not write the error in the http output if it's still empty. - // This prevents from writing a 200(OK) when there is an internal error. - if !output.Flushed() { - return err - } - _, err = w.Write(sf.FormatError(errors.New(utils.GetErrorMessage(err)))) - if err != nil { - logrus.Warnf("could not write error response: %v", err) - } - return nil - } - - if httputils.BoolValue(r, "forcerm") && version.GreaterThanOrEqualTo("1.12") { - buildConfig.Remove = true - } else if r.FormValue("rm") == "" && version.GreaterThanOrEqualTo("1.12") { - buildConfig.Remove = true - } else { - buildConfig.Remove = httputils.BoolValue(r, "rm") - } - if httputils.BoolValue(r, "pull") && version.GreaterThanOrEqualTo("1.16") { - buildConfig.Pull = true - } - - repoAndTags, err := sanitizeRepoAndTags(r.Form["t"]) - if err != nil { - return errf(err) - } - - buildConfig.DockerfileName = r.FormValue("dockerfile") - buildConfig.Verbose = !httputils.BoolValue(r, "q") - buildConfig.UseCache = !httputils.BoolValue(r, "nocache") - buildConfig.ForceRemove = httputils.BoolValue(r, "forcerm") - buildConfig.MemorySwap = httputils.Int64ValueOrZero(r, "memswap") - buildConfig.Memory = httputils.Int64ValueOrZero(r, "memory") - buildConfig.CPUShares = httputils.Int64ValueOrZero(r, "cpushares") - buildConfig.CPUPeriod = httputils.Int64ValueOrZero(r, "cpuperiod") - buildConfig.CPUQuota = httputils.Int64ValueOrZero(r, "cpuquota") - buildConfig.CPUSetCpus = r.FormValue("cpusetcpus") - buildConfig.CPUSetMems = r.FormValue("cpusetmems") - buildConfig.CgroupParent = r.FormValue("cgroupparent") - - if r.Form.Get("shmsize") != "" { - shmSize, err := strconv.ParseInt(r.Form.Get("shmsize"), 10, 64) - if err != nil { - return errf(err) - } - buildConfig.ShmSize = &shmSize - } - - if i := runconfig.IsolationLevel(r.FormValue("isolation")); i != "" { - if !runconfig.IsolationLevel.IsValid(i) { - return errf(fmt.Errorf("Unsupported isolation: %q", i)) - } - buildConfig.Isolation = i - } - - var buildUlimits = []*ulimit.Ulimit{} - ulimitsJSON := r.FormValue("ulimits") - if ulimitsJSON != "" { - if err := json.NewDecoder(strings.NewReader(ulimitsJSON)).Decode(&buildUlimits); err != nil { - return errf(err) - } - buildConfig.Ulimits = buildUlimits - } - - var buildArgs = map[string]string{} - buildArgsJSON := r.FormValue("buildargs") - if buildArgsJSON != "" { - if err := json.NewDecoder(strings.NewReader(buildArgsJSON)).Decode(&buildArgs); err != nil { - return errf(err) - } - buildConfig.BuildArgs = buildArgs - } - - remoteURL := r.FormValue("remote") - - // Currently, only used if context is from a remote url. - // Look at code in DetectContextFromRemoteURL for more information. - createProgressReader := func(in io.ReadCloser) io.ReadCloser { - progressOutput := sf.NewProgressOutput(output, true) - return progress.NewProgressReader(in, progressOutput, r.ContentLength, "Downloading context", remoteURL) - } - - var ( - context builder.ModifiableContext - dockerfileName string - ) - context, dockerfileName, err = daemonbuilder.DetectContextFromRemoteURL(r.Body, remoteURL, createProgressReader) - if err != nil { - return errf(err) - } - defer func() { - if err := context.Close(); err != nil { - logrus.Debugf("[BUILDER] failed to remove temporary context: %v", err) - } - }() - - uidMaps, gidMaps := s.daemon.GetUIDGIDMaps() - defaultArchiver := &archive.Archiver{ - Untar: chrootarchive.Untar, - UIDMaps: uidMaps, - GIDMaps: gidMaps, - } - docker := &daemonbuilder.Docker{ - Daemon: s.daemon, - OutOld: output, - AuthConfigs: authConfigs, - Archiver: defaultArchiver, - } - - b, err := dockerfile.NewBuilder(buildConfig, docker, builder.DockerIgnoreContext{ModifiableContext: context}, nil) - if err != nil { - return errf(err) - } - b.Stdout = &streamformatter.StdoutFormatter{Writer: output, StreamFormatter: sf} - b.Stderr = &streamformatter.StderrFormatter{Writer: output, StreamFormatter: sf} - - if closeNotifier, ok := w.(http.CloseNotifier); ok { - finished := make(chan struct{}) - defer close(finished) - go func() { - select { - case <-finished: - case <-closeNotifier.CloseNotify(): - logrus.Infof("Client disconnected, cancelling job: build") - b.Cancel() - } - }() - } - - if len(dockerfileName) > 0 { - b.DockerfileName = dockerfileName - } - - imgID, err := b.Build() - if err != nil { - return errf(err) - } - - for _, rt := range repoAndTags { - if err := s.daemon.TagImage(rt, imgID); err != nil { - return errf(err) - } - } - - return nil -} - -// sanitizeRepoAndTags parses the raw "t" parameter received from the client -// to a slice of repoAndTag. -// It also validates each repoName and tag. -func sanitizeRepoAndTags(names []string) ([]reference.Named, error) { - var ( - repoAndTags []reference.Named - // This map is used for deduplicating the "-t" parameter. - uniqNames = make(map[string]struct{}) - ) - for _, repo := range names { - if repo == "" { - continue - } - - ref, err := reference.ParseNamed(repo) - if err != nil { - return nil, err - } - ref = reference.WithDefaultTag(ref) - - if _, isCanonical := ref.(reference.Canonical); isCanonical { - return nil, errors.New("build tag cannot contain a digest") - } - - nameWithTag := ref.String() - - if _, exists := uniqNames[nameWithTag]; !exists { - uniqNames[nameWithTag] = struct{}{} - repoAndTags = append(repoAndTags, ref) - } - } - return repoAndTags, nil -} - func (s *router) getImagesJSON(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { if err := httputils.ParseForm(r); err != nil { return err diff --git a/api/server/router/local/local.go b/api/server/router/local/local.go index 2a3106aeec..ed07f97786 100644 --- a/api/server/router/local/local.go +++ b/api/server/router/local/local.go @@ -97,7 +97,6 @@ func (r *router) initRoutes() { NewGetRoute("/images/{name:.*}/json", r.getImagesByName), // POST NewPostRoute("/commit", r.postCommit), - NewPostRoute("/build", r.postBuild), NewPostRoute("/images/create", r.postImagesCreate), NewPostRoute("/images/load", r.postImagesLoad), NewPostRoute("/images/{name:.*}/push", r.postImagesPush), diff --git a/api/server/server.go b/api/server/server.go index 218456e515..e092ad3b7a 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -10,6 +10,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/docker/api/server/httputils" "github.com/docker/docker/api/server/router" + "github.com/docker/docker/api/server/router/build" "github.com/docker/docker/api/server/router/container" "github.com/docker/docker/api/server/router/local" "github.com/docker/docker/api/server/router/network" @@ -177,6 +178,7 @@ func (s *Server) InitRouters(d *daemon.Daemon) { s.addRouter(network.NewRouter(d)) s.addRouter(system.NewRouter(d)) s.addRouter(volume.NewRouter(d)) + s.addRouter(build.NewRouter(d)) } // addRouter adds a new router to the server. diff --git a/builder/builder.go b/builder/builder.go index 07293f4f8e..94ca43d950 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -15,17 +15,6 @@ import ( "github.com/docker/docker/runconfig" ) -// Builder abstracts a Docker builder whose only purpose is to build a Docker image referenced by an imageID. -type Builder interface { - // Build builds a Docker image referenced by an imageID string. - // - // Note: Tagging an image should not be done by a Builder, it should instead be done - // by the caller. - // - // TODO: make this return a reference instead of string - Build() (imageID string) -} - // Context represents a file system tree. type Context interface { // Close allows to signal that the filesystem tree won't be used anymore. diff --git a/builder/dockerfile/builder.go b/builder/dockerfile/builder.go index 5b254440eb..b4f7a6a779 100644 --- a/builder/dockerfile/builder.go +++ b/builder/dockerfile/builder.go @@ -70,7 +70,7 @@ type Config struct { } // Builder is a Dockerfile builder -// It implements the builder.Builder interface. +// It implements the builder.Backend interface. type Builder struct { *Config