From c2eb37f9aeb6215293483e02613514e49011cf2c Mon Sep 17 00:00:00 2001 From: Shijiang Wei Date: Sun, 30 Aug 2015 21:48:03 +0800 Subject: [PATCH] Add ability to add multiple tags with docker build Signed-off-by: Shijiang Wei --- api/client/build.go | 40 +++++++------ api/server/router/local/image.go | 60 +++++++++++++++---- docs/reference/api/docker_remote_api_v1.21.md | 5 +- docs/reference/api/docker_remote_api_v1.22.md | 5 +- docs/reference/builder.md | 5 ++ docs/reference/commandline/build.md | 10 +++- integration-cli/docker_cli_build_test.go | 19 ++++++ integration-cli/docker_utils.go | 1 - man/docker-build.1.md | 12 +++- opts/opts.go | 2 - 10 files changed, 119 insertions(+), 40 deletions(-) diff --git a/api/client/build.go b/api/client/build.go index 2257868414..b61cdf37cc 100644 --- a/api/client/build.go +++ b/api/client/build.go @@ -49,7 +49,8 @@ const ( // Usage: docker build [OPTIONS] PATH | URL | - func (cli *DockerCli) CmdBuild(args ...string) error { cmd := Cli.Subcmd("build", []string{"PATH | URL | -"}, Cli.DockerCommands["build"].Description, true) - tag := cmd.String([]string{"t", "-tag"}, "", "Repository name (and optionally a tag) for the image") + flTags := opts.NewListOpts(validateTag) + cmd.Var(&flTags, []string{"t", "-tag"}, "Name and optionally a tag in the 'name:tag' format") suppressOutput := cmd.Bool([]string{"q", "-quiet"}, false, "Suppress the verbose output generated by the containers") noCache := cmd.Bool([]string{"#no-cache", "-no-cache"}, false, "Do not use cache when building the image") rm := cmd.Bool([]string{"#rm", "-rm"}, true, "Remove intermediate containers after a successful build") @@ -207,24 +208,11 @@ func (cli *DockerCli) CmdBuild(args ...string) error { memorySwap = parsedMemorySwap } } + // Send the build context - v := &url.Values{} - - //Check if the given image name can be resolved - if *tag != "" { - repository, tag := parsers.ParseRepositoryTag(*tag) - if err := registry.ValidateRepositoryName(repository); err != nil { - return err - } - if len(tag) > 0 { - if err := tags.ValidateTagName(tag); err != nil { - return err - } - } + v := url.Values{ + "t": flTags.GetAll(), } - - v.Set("t", *tag) - if *suppressOutput { v.Set("q", "1") } @@ -324,6 +312,24 @@ func (cli *DockerCli) CmdBuild(args ...string) error { return nil } +// validateTag checks if the given image name can be resolved. +func validateTag(rawRepo string) (string, error) { + repository, tag := parsers.ParseRepositoryTag(rawRepo) + if err := registry.ValidateRepositoryName(repository); err != nil { + return "", err + } + + if len(tag) == 0 { + return rawRepo, nil + } + + if err := tags.ValidateTagName(tag); err != nil { + return "", err + } + + return rawRepo, nil +} + // isUNC returns true if the path is UNC (one starting \\). It always returns // false on Linux. func isUNC(path string) bool { diff --git a/api/server/router/local/image.go b/api/server/router/local/image.go index aa34f21752..d06940d63f 100644 --- a/api/server/router/local/image.go +++ b/api/server/router/local/image.go @@ -308,16 +308,9 @@ func (s *router) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R buildConfig.Pull = true } - repoName, tag := parsers.ParseRepositoryTag(r.FormValue("t")) - if repoName != "" { - if err := registry.ValidateRepositoryName(repoName); err != nil { - return errf(err) - } - if len(tag) > 0 { - if err := tags.ValidateTagName(tag); err != nil { - return errf(err) - } - } + repoAndTags, err := sanitizeRepoAndTags(r.Form["t"]) + if err != nil { + return errf(err) } buildConfig.DockerfileName = r.FormValue("dockerfile") @@ -369,7 +362,6 @@ func (s *router) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R var ( context builder.ModifiableContext dockerfileName string - err error ) context, dockerfileName, err = daemonbuilder.DetectContextFromRemoteURL(r.Body, remoteURL, pReader) if err != nil { @@ -418,8 +410,8 @@ func (s *router) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R return errf(err) } - if repoName != "" { - if err := s.daemon.TagImage(repoName, tag, string(imgID), true); err != nil { + for _, rt := range repoAndTags { + if err := s.daemon.TagImage(rt.repo, rt.tag, string(imgID), true); err != nil { return errf(err) } } @@ -427,6 +419,48 @@ func (s *router) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R return nil } +// repoAndTag is a helper struct for holding the parsed repositories and tags of +// the input "t" argument. +type repoAndTag struct { + repo, tag string +} + +// 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) ([]repoAndTag, error) { + var ( + repoAndTags []repoAndTag + // This map is used for deduplicating the "-t" paramter. + uniqNames = make(map[string]struct{}) + ) + for _, repo := range names { + name, tag := parsers.ParseRepositoryTag(repo) + if name == "" { + continue + } + + if err := registry.ValidateRepositoryName(name); err != nil { + return nil, err + } + + nameWithTag := name + if len(tag) > 0 { + if err := tags.ValidateTagName(tag); err != nil { + return nil, err + } + nameWithTag += ":" + tag + } else { + nameWithTag += ":" + tags.DefaultTag + } + if _, exists := uniqNames[nameWithTag]; !exists { + uniqNames[nameWithTag] = struct{}{} + repoAndTags = append(repoAndTags, repoAndTag{repo: name, tag: tag}) + } + } + 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/docs/reference/api/docker_remote_api_v1.21.md b/docs/reference/api/docker_remote_api_v1.21.md index 4b6b840f26..3b0ae89c93 100644 --- a/docs/reference/api/docker_remote_api_v1.21.md +++ b/docs/reference/api/docker_remote_api_v1.21.md @@ -1386,8 +1386,9 @@ Query Parameters: - **dockerfile** - Path within the build context to the Dockerfile. This is ignored if `remote` is specified and points to an individual filename. -- **t** – A repository name (and optionally a tag) to apply to - the resulting image in case of success. +- **t** – A name and optional tag to apply to the image in the `name:tag` format. + If you omit the `tag` the default `latest` value is assumed. + You can provide one or more `t` parameters. - **remote** – A Git repository URI or HTTP/HTTPS URI build source. If the URI specifies a filename, the file's contents are placed into a file called `Dockerfile`. diff --git a/docs/reference/api/docker_remote_api_v1.22.md b/docs/reference/api/docker_remote_api_v1.22.md index 768133666d..1db1359c82 100644 --- a/docs/reference/api/docker_remote_api_v1.22.md +++ b/docs/reference/api/docker_remote_api_v1.22.md @@ -1386,8 +1386,9 @@ Query Parameters: - **dockerfile** - Path within the build context to the Dockerfile. This is ignored if `remote` is specified and points to an individual filename. -- **t** – A repository name (and optionally a tag) to apply to - the resulting image in case of success. +- **t** – A name and optional tag to apply to the image in the `name:tag` format. + If you omit the `tag` the default `latest` value is assumed. + You can provide one or more `t` parameters. - **remote** – A Git repository URI or HTTP/HTTPS URI build source. If the URI specifies a filename, the file's contents are placed into a file called `Dockerfile`. diff --git a/docs/reference/builder.md b/docs/reference/builder.md index c65f34c308..b1ed35ded2 100644 --- a/docs/reference/builder.md +++ b/docs/reference/builder.md @@ -62,6 +62,11 @@ the build succeeds: $ docker build -t shykes/myapp . +To tag the image into multiple repositories after the build, +add multiple `-t` parameters when you run the `build` command: + + $ docker build -t shykes/myapp:1.0.2 -t shykes/myapp:latest . + The Docker daemon runs the instructions in the `Dockerfile` one-by-one, committing the result of each instruction to a new image if necessary, before finally outputting the ID of your diff --git a/docs/reference/commandline/build.md b/docs/reference/commandline/build.md index 23a34581f0..32a51ceb8b 100644 --- a/docs/reference/commandline/build.md +++ b/docs/reference/commandline/build.md @@ -31,7 +31,7 @@ parent = "smn_cli" --pull=false Always attempt to pull a newer version of the image -q, --quiet=false Suppress the verbose output generated by the containers --rm=true Remove intermediate containers after a successful build - -t, --tag="" Repository name (and optionally a tag) for the image + -t, --tag=[] Name and optionally a tag in the 'name:tag' format --ulimit=[] Ulimit options Builds Docker images from a Dockerfile and a "context". A build's context is @@ -227,6 +227,14 @@ uploaded context. The builder reference contains detailed information on This will build like the previous example, but it will then tag the resulting image. The repository name will be `vieux/apache` and the tag will be `2.0` +You can apply multiple tags to an image. For example, you can apply the `latest` +tag to a newly built image and add another tag that references a specific +version. +For example, to tag an image both as `whenry/fedora-jboss:latest` and +`whenry/fedora-jboss:v2.1`, use the following: + + $ docker build -t whenry/fedora-jboss:latest -t whenry/fedora-jboss:v2.1 . + ### Specify Dockerfile (-f) $ docker build -f Dockerfile.debug . diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index 00498541ff..ff1fbbd015 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/integration-cli/docker_cli_build_test.go @@ -6258,3 +6258,22 @@ func (s *DockerSuite) TestBuildTagEvent(c *check.C) { c.Fatal("The 'tag' event not heard from the server") } } + +// #15780 +func (s *DockerSuite) TestBuildMultipleTags(c *check.C) { + dockerfile := ` + FROM busybox + MAINTAINER test-15780 + ` + cmd := exec.Command(dockerBinary, "build", "-t", "tag1", "-t", "tag2:v2", + "-t", "tag1:latest", "-t", "tag1", "--no-cache", "-") + cmd.Stdin = strings.NewReader(dockerfile) + _, err := runCommand(cmd) + c.Assert(err, check.IsNil) + + id1, err := getIDByName("tag1") + c.Assert(err, check.IsNil) + id2, err := getIDByName("tag2:v2") + c.Assert(err, check.IsNil) + c.Assert(id1, check.Equals, id2) +} diff --git a/integration-cli/docker_utils.go b/integration-cli/docker_utils.go index b9b48a53b0..06bab2a31c 100644 --- a/integration-cli/docker_utils.go +++ b/integration-cli/docker_utils.go @@ -1122,7 +1122,6 @@ func buildImageCmd(name, dockerfile string, useCache bool, buildFlags ...string) buildCmd := exec.Command(dockerBinary, args...) buildCmd.Stdin = strings.NewReader(dockerfile) return buildCmd - } func buildImageWithOut(name, dockerfile string, useCache bool, buildFlags ...string) (string, string, error) { diff --git a/man/docker-build.1.md b/man/docker-build.1.md index 876d6fa9bb..4bf4deea59 100644 --- a/man/docker-build.1.md +++ b/man/docker-build.1.md @@ -16,7 +16,7 @@ docker-build - Build a new image from the source code at PATH [**--pull**[=*false*]] [**-q**|**--quiet**[=*false*]] [**--rm**[=*true*]] -[**-t**|**--tag**[=*TAG*]] +[**-t**|**--tag**[=*[]*]] [**-m**|**--memory**[=*MEMORY*]] [**--memory-swap**[=*MEMORY-SWAP*]] [**--cpu-period**[=*0*]] @@ -82,7 +82,7 @@ set as the **URL**, the repository is cloned locally and then sent as the contex Remove intermediate containers after a successful build. The default is *true*. **-t**, **--tag**="" - Repository name (and optionally a tag) to be applied to the resulting image in case of success + Repository names (and optionally with tags) to be applied to the resulting image in case of success. **-m**, **--memory**=*MEMORY* Memory limit @@ -235,6 +235,14 @@ If you do not provide a version tag then Docker will assign `latest`: When you list the images, the image above will have the tag `latest`. +You can apply multiple tags to an image. For example, you can apply the `latest` +tag to a newly built image and add another tag that references a specific +version. +For example, to tag an image both as `whenry/fedora-jboss:latest` and +`whenry/fedora-jboss:v2.1`, use the following: + + docker build -t whenry/fedora-jboss:latest -t whenry/fedora-jboss:v2.1 . + So renaming an image is arbitrary but consideration should be given to a useful convention that makes sense for consumers and should also take into account Docker community conventions. diff --git a/opts/opts.go b/opts/opts.go index 78059739a8..35dc193f19 100644 --- a/opts/opts.go +++ b/opts/opts.go @@ -86,7 +86,6 @@ func (opts *ListOpts) Delete(key string) { // GetMap returns the content of values in a map in order to avoid // duplicates. -// FIXME: can we remove this? func (opts *ListOpts) GetMap() map[string]struct{} { ret := make(map[string]struct{}) for _, k := range *opts.values { @@ -96,7 +95,6 @@ func (opts *ListOpts) GetMap() map[string]struct{} { } // GetAll returns the values of slice. -// FIXME: Can we remove this? func (opts *ListOpts) GetAll() []string { return (*opts.values) }