Add ability to add multiple tags with docker build
| ... | ... |
@@ -49,7 +49,8 @@ const ( |
| 49 | 49 |
// Usage: docker build [OPTIONS] PATH | URL | - |
| 50 | 50 |
func (cli *DockerCli) CmdBuild(args ...string) error {
|
| 51 | 51 |
cmd := Cli.Subcmd("build", []string{"PATH | URL | -"}, Cli.DockerCommands["build"].Description, true)
|
| 52 |
- tag := cmd.String([]string{"t", "-tag"}, "", "Repository name (and optionally a tag) for the image")
|
|
| 52 |
+ flTags := opts.NewListOpts(validateTag) |
|
| 53 |
+ cmd.Var(&flTags, []string{"t", "-tag"}, "Name and optionally a tag in the 'name:tag' format")
|
|
| 53 | 54 |
suppressOutput := cmd.Bool([]string{"q", "-quiet"}, false, "Suppress the verbose output generated by the containers")
|
| 54 | 55 |
noCache := cmd.Bool([]string{"#no-cache", "-no-cache"}, false, "Do not use cache when building the image")
|
| 55 | 56 |
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 {
|
| 207 | 207 |
memorySwap = parsedMemorySwap |
| 208 | 208 |
} |
| 209 | 209 |
} |
| 210 |
- // Send the build context |
|
| 211 |
- v := &url.Values{}
|
|
| 212 | 210 |
|
| 213 |
- //Check if the given image name can be resolved |
|
| 214 |
- if *tag != "" {
|
|
| 215 |
- repository, tag := parsers.ParseRepositoryTag(*tag) |
|
| 216 |
- if err := registry.ValidateRepositoryName(repository); err != nil {
|
|
| 217 |
- return err |
|
| 218 |
- } |
|
| 219 |
- if len(tag) > 0 {
|
|
| 220 |
- if err := tags.ValidateTagName(tag); err != nil {
|
|
| 221 |
- return err |
|
| 222 |
- } |
|
| 223 |
- } |
|
| 211 |
+ // Send the build context |
|
| 212 |
+ v := url.Values{
|
|
| 213 |
+ "t": flTags.GetAll(), |
|
| 224 | 214 |
} |
| 225 |
- |
|
| 226 |
- v.Set("t", *tag)
|
|
| 227 |
- |
|
| 228 | 215 |
if *suppressOutput {
|
| 229 | 216 |
v.Set("q", "1")
|
| 230 | 217 |
} |
| ... | ... |
@@ -324,6 +312,24 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
|
| 324 | 324 |
return nil |
| 325 | 325 |
} |
| 326 | 326 |
|
| 327 |
+// validateTag checks if the given image name can be resolved. |
|
| 328 |
+func validateTag(rawRepo string) (string, error) {
|
|
| 329 |
+ repository, tag := parsers.ParseRepositoryTag(rawRepo) |
|
| 330 |
+ if err := registry.ValidateRepositoryName(repository); err != nil {
|
|
| 331 |
+ return "", err |
|
| 332 |
+ } |
|
| 333 |
+ |
|
| 334 |
+ if len(tag) == 0 {
|
|
| 335 |
+ return rawRepo, nil |
|
| 336 |
+ } |
|
| 337 |
+ |
|
| 338 |
+ if err := tags.ValidateTagName(tag); err != nil {
|
|
| 339 |
+ return "", err |
|
| 340 |
+ } |
|
| 341 |
+ |
|
| 342 |
+ return rawRepo, nil |
|
| 343 |
+} |
|
| 344 |
+ |
|
| 327 | 345 |
// isUNC returns true if the path is UNC (one starting \\). It always returns |
| 328 | 346 |
// false on Linux. |
| 329 | 347 |
func isUNC(path string) bool {
|
| ... | ... |
@@ -308,16 +308,9 @@ func (s *router) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R |
| 308 | 308 |
buildConfig.Pull = true |
| 309 | 309 |
} |
| 310 | 310 |
|
| 311 |
- repoName, tag := parsers.ParseRepositoryTag(r.FormValue("t"))
|
|
| 312 |
- if repoName != "" {
|
|
| 313 |
- if err := registry.ValidateRepositoryName(repoName); err != nil {
|
|
| 314 |
- return errf(err) |
|
| 315 |
- } |
|
| 316 |
- if len(tag) > 0 {
|
|
| 317 |
- if err := tags.ValidateTagName(tag); err != nil {
|
|
| 318 |
- return errf(err) |
|
| 319 |
- } |
|
| 320 |
- } |
|
| 311 |
+ repoAndTags, err := sanitizeRepoAndTags(r.Form["t"]) |
|
| 312 |
+ if err != nil {
|
|
| 313 |
+ return errf(err) |
|
| 321 | 314 |
} |
| 322 | 315 |
|
| 323 | 316 |
buildConfig.DockerfileName = r.FormValue("dockerfile")
|
| ... | ... |
@@ -369,7 +362,6 @@ func (s *router) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R |
| 369 | 369 |
var ( |
| 370 | 370 |
context builder.ModifiableContext |
| 371 | 371 |
dockerfileName string |
| 372 |
- err error |
|
| 373 | 372 |
) |
| 374 | 373 |
context, dockerfileName, err = daemonbuilder.DetectContextFromRemoteURL(r.Body, remoteURL, pReader) |
| 375 | 374 |
if err != nil {
|
| ... | ... |
@@ -418,8 +410,8 @@ func (s *router) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R |
| 418 | 418 |
return errf(err) |
| 419 | 419 |
} |
| 420 | 420 |
|
| 421 |
- if repoName != "" {
|
|
| 422 |
- if err := s.daemon.TagImage(repoName, tag, string(imgID), true); err != nil {
|
|
| 421 |
+ for _, rt := range repoAndTags {
|
|
| 422 |
+ if err := s.daemon.TagImage(rt.repo, rt.tag, string(imgID), true); err != nil {
|
|
| 423 | 423 |
return errf(err) |
| 424 | 424 |
} |
| 425 | 425 |
} |
| ... | ... |
@@ -427,6 +419,48 @@ func (s *router) postBuild(ctx context.Context, w http.ResponseWriter, r *http.R |
| 427 | 427 |
return nil |
| 428 | 428 |
} |
| 429 | 429 |
|
| 430 |
+// repoAndTag is a helper struct for holding the parsed repositories and tags of |
|
| 431 |
+// the input "t" argument. |
|
| 432 |
+type repoAndTag struct {
|
|
| 433 |
+ repo, tag string |
|
| 434 |
+} |
|
| 435 |
+ |
|
| 436 |
+// sanitizeRepoAndTags parses the raw "t" parameter received from the client |
|
| 437 |
+// to a slice of repoAndTag. |
|
| 438 |
+// It also validates each repoName and tag. |
|
| 439 |
+func sanitizeRepoAndTags(names []string) ([]repoAndTag, error) {
|
|
| 440 |
+ var ( |
|
| 441 |
+ repoAndTags []repoAndTag |
|
| 442 |
+ // This map is used for deduplicating the "-t" paramter. |
|
| 443 |
+ uniqNames = make(map[string]struct{})
|
|
| 444 |
+ ) |
|
| 445 |
+ for _, repo := range names {
|
|
| 446 |
+ name, tag := parsers.ParseRepositoryTag(repo) |
|
| 447 |
+ if name == "" {
|
|
| 448 |
+ continue |
|
| 449 |
+ } |
|
| 450 |
+ |
|
| 451 |
+ if err := registry.ValidateRepositoryName(name); err != nil {
|
|
| 452 |
+ return nil, err |
|
| 453 |
+ } |
|
| 454 |
+ |
|
| 455 |
+ nameWithTag := name |
|
| 456 |
+ if len(tag) > 0 {
|
|
| 457 |
+ if err := tags.ValidateTagName(tag); err != nil {
|
|
| 458 |
+ return nil, err |
|
| 459 |
+ } |
|
| 460 |
+ nameWithTag += ":" + tag |
|
| 461 |
+ } else {
|
|
| 462 |
+ nameWithTag += ":" + tags.DefaultTag |
|
| 463 |
+ } |
|
| 464 |
+ if _, exists := uniqNames[nameWithTag]; !exists {
|
|
| 465 |
+ uniqNames[nameWithTag] = struct{}{}
|
|
| 466 |
+ repoAndTags = append(repoAndTags, repoAndTag{repo: name, tag: tag})
|
|
| 467 |
+ } |
|
| 468 |
+ } |
|
| 469 |
+ return repoAndTags, nil |
|
| 470 |
+} |
|
| 471 |
+ |
|
| 430 | 472 |
func (s *router) getImagesJSON(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
| 431 | 473 |
if err := httputils.ParseForm(r); err != nil {
|
| 432 | 474 |
return err |
| ... | ... |
@@ -1386,8 +1386,9 @@ Query Parameters: |
| 1386 | 1386 |
|
| 1387 | 1387 |
- **dockerfile** - Path within the build context to the Dockerfile. This is |
| 1388 | 1388 |
ignored if `remote` is specified and points to an individual filename. |
| 1389 |
-- **t** – A repository name (and optionally a tag) to apply to |
|
| 1390 |
- the resulting image in case of success. |
|
| 1389 |
+- **t** – A name and optional tag to apply to the image in the `name:tag` format. |
|
| 1390 |
+ If you omit the `tag` the default `latest` value is assumed. |
|
| 1391 |
+ You can provide one or more `t` parameters. |
|
| 1391 | 1392 |
- **remote** – A Git repository URI or HTTP/HTTPS URI build source. If the |
| 1392 | 1393 |
URI specifies a filename, the file's contents are placed into a file |
| 1393 | 1394 |
called `Dockerfile`. |
| ... | ... |
@@ -1386,8 +1386,9 @@ Query Parameters: |
| 1386 | 1386 |
|
| 1387 | 1387 |
- **dockerfile** - Path within the build context to the Dockerfile. This is |
| 1388 | 1388 |
ignored if `remote` is specified and points to an individual filename. |
| 1389 |
-- **t** – A repository name (and optionally a tag) to apply to |
|
| 1390 |
- the resulting image in case of success. |
|
| 1389 |
+- **t** – A name and optional tag to apply to the image in the `name:tag` format. |
|
| 1390 |
+ If you omit the `tag` the default `latest` value is assumed. |
|
| 1391 |
+ You can provide one or more `t` parameters. |
|
| 1391 | 1392 |
- **remote** – A Git repository URI or HTTP/HTTPS URI build source. If the |
| 1392 | 1393 |
URI specifies a filename, the file's contents are placed into a file |
| 1393 | 1394 |
called `Dockerfile`. |
| ... | ... |
@@ -62,6 +62,11 @@ the build succeeds: |
| 62 | 62 |
|
| 63 | 63 |
$ docker build -t shykes/myapp . |
| 64 | 64 |
|
| 65 |
+To tag the image into multiple repositories after the build, |
|
| 66 |
+add multiple `-t` parameters when you run the `build` command: |
|
| 67 |
+ |
|
| 68 |
+ $ docker build -t shykes/myapp:1.0.2 -t shykes/myapp:latest . |
|
| 69 |
+ |
|
| 65 | 70 |
The Docker daemon runs the instructions in the `Dockerfile` one-by-one, |
| 66 | 71 |
committing the result of each instruction |
| 67 | 72 |
to a new image if necessary, before finally outputting the ID of your |
| ... | ... |
@@ -31,7 +31,7 @@ parent = "smn_cli" |
| 31 | 31 |
--pull=false Always attempt to pull a newer version of the image |
| 32 | 32 |
-q, --quiet=false Suppress the verbose output generated by the containers |
| 33 | 33 |
--rm=true Remove intermediate containers after a successful build |
| 34 |
- -t, --tag="" Repository name (and optionally a tag) for the image |
|
| 34 |
+ -t, --tag=[] Name and optionally a tag in the 'name:tag' format |
|
| 35 | 35 |
--ulimit=[] Ulimit options |
| 36 | 36 |
|
| 37 | 37 |
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 |
| 227 | 227 |
This will build like the previous example, but it will then tag the resulting |
| 228 | 228 |
image. The repository name will be `vieux/apache` and the tag will be `2.0` |
| 229 | 229 |
|
| 230 |
+You can apply multiple tags to an image. For example, you can apply the `latest` |
|
| 231 |
+tag to a newly built image and add another tag that references a specific |
|
| 232 |
+version. |
|
| 233 |
+For example, to tag an image both as `whenry/fedora-jboss:latest` and |
|
| 234 |
+`whenry/fedora-jboss:v2.1`, use the following: |
|
| 235 |
+ |
|
| 236 |
+ $ docker build -t whenry/fedora-jboss:latest -t whenry/fedora-jboss:v2.1 . |
|
| 237 |
+ |
|
| 230 | 238 |
### Specify Dockerfile (-f) |
| 231 | 239 |
|
| 232 | 240 |
$ docker build -f Dockerfile.debug . |
| ... | ... |
@@ -6258,3 +6258,22 @@ func (s *DockerSuite) TestBuildTagEvent(c *check.C) {
|
| 6258 | 6258 |
c.Fatal("The 'tag' event not heard from the server")
|
| 6259 | 6259 |
} |
| 6260 | 6260 |
} |
| 6261 |
+ |
|
| 6262 |
+// #15780 |
|
| 6263 |
+func (s *DockerSuite) TestBuildMultipleTags(c *check.C) {
|
|
| 6264 |
+ dockerfile := ` |
|
| 6265 |
+ FROM busybox |
|
| 6266 |
+ MAINTAINER test-15780 |
|
| 6267 |
+ ` |
|
| 6268 |
+ cmd := exec.Command(dockerBinary, "build", "-t", "tag1", "-t", "tag2:v2", |
|
| 6269 |
+ "-t", "tag1:latest", "-t", "tag1", "--no-cache", "-") |
|
| 6270 |
+ cmd.Stdin = strings.NewReader(dockerfile) |
|
| 6271 |
+ _, err := runCommand(cmd) |
|
| 6272 |
+ c.Assert(err, check.IsNil) |
|
| 6273 |
+ |
|
| 6274 |
+ id1, err := getIDByName("tag1")
|
|
| 6275 |
+ c.Assert(err, check.IsNil) |
|
| 6276 |
+ id2, err := getIDByName("tag2:v2")
|
|
| 6277 |
+ c.Assert(err, check.IsNil) |
|
| 6278 |
+ c.Assert(id1, check.Equals, id2) |
|
| 6279 |
+} |
| ... | ... |
@@ -1163,7 +1163,6 @@ func buildImageCmd(name, dockerfile string, useCache bool, buildFlags ...string) |
| 1163 | 1163 |
buildCmd := exec.Command(dockerBinary, args...) |
| 1164 | 1164 |
buildCmd.Stdin = strings.NewReader(dockerfile) |
| 1165 | 1165 |
return buildCmd |
| 1166 |
- |
|
| 1167 | 1166 |
} |
| 1168 | 1167 |
|
| 1169 | 1168 |
func buildImageWithOut(name, dockerfile string, useCache bool, buildFlags ...string) (string, string, error) {
|
| ... | ... |
@@ -16,7 +16,7 @@ docker-build - Build a new image from the source code at PATH |
| 16 | 16 |
[**--pull**[=*false*]] |
| 17 | 17 |
[**-q**|**--quiet**[=*false*]] |
| 18 | 18 |
[**--rm**[=*true*]] |
| 19 |
-[**-t**|**--tag**[=*TAG*]] |
|
| 19 |
+[**-t**|**--tag**[=*[]*]] |
|
| 20 | 20 |
[**-m**|**--memory**[=*MEMORY*]] |
| 21 | 21 |
[**--memory-swap**[=*MEMORY-SWAP*]] |
| 22 | 22 |
[**--cpu-period**[=*0*]] |
| ... | ... |
@@ -82,7 +82,7 @@ set as the **URL**, the repository is cloned locally and then sent as the contex |
| 82 | 82 |
Remove intermediate containers after a successful build. The default is *true*. |
| 83 | 83 |
|
| 84 | 84 |
**-t**, **--tag**="" |
| 85 |
- Repository name (and optionally a tag) to be applied to the resulting image in case of success |
|
| 85 |
+ Repository names (and optionally with tags) to be applied to the resulting image in case of success. |
|
| 86 | 86 |
|
| 87 | 87 |
**-m**, **--memory**=*MEMORY* |
| 88 | 88 |
Memory limit |
| ... | ... |
@@ -235,6 +235,14 @@ If you do not provide a version tag then Docker will assign `latest`: |
| 235 | 235 |
|
| 236 | 236 |
When you list the images, the image above will have the tag `latest`. |
| 237 | 237 |
|
| 238 |
+You can apply multiple tags to an image. For example, you can apply the `latest` |
|
| 239 |
+tag to a newly built image and add another tag that references a specific |
|
| 240 |
+version. |
|
| 241 |
+For example, to tag an image both as `whenry/fedora-jboss:latest` and |
|
| 242 |
+`whenry/fedora-jboss:v2.1`, use the following: |
|
| 243 |
+ |
|
| 244 |
+ docker build -t whenry/fedora-jboss:latest -t whenry/fedora-jboss:v2.1 . |
|
| 245 |
+ |
|
| 238 | 246 |
So renaming an image is arbitrary but consideration should be given to |
| 239 | 247 |
a useful convention that makes sense for consumers and should also take |
| 240 | 248 |
into account Docker community conventions. |
| ... | ... |
@@ -86,7 +86,6 @@ func (opts *ListOpts) Delete(key string) {
|
| 86 | 86 |
|
| 87 | 87 |
// GetMap returns the content of values in a map in order to avoid |
| 88 | 88 |
// duplicates. |
| 89 |
-// FIXME: can we remove this? |
|
| 90 | 89 |
func (opts *ListOpts) GetMap() map[string]struct{} {
|
| 91 | 90 |
ret := make(map[string]struct{})
|
| 92 | 91 |
for _, k := range *opts.values {
|
| ... | ... |
@@ -96,7 +95,6 @@ func (opts *ListOpts) GetMap() map[string]struct{} {
|
| 96 | 96 |
} |
| 97 | 97 |
|
| 98 | 98 |
// GetAll returns the values of slice. |
| 99 |
-// FIXME: Can we remove this? |
|
| 100 | 99 |
func (opts *ListOpts) GetAll() []string {
|
| 101 | 100 |
return (*opts.values) |
| 102 | 101 |
} |