Browse code

Add ability to add multiple tags with docker build

Signed-off-by: Shijiang Wei <mountkin@gmail.com>

Shijiang Wei authored on 2015/08/30 22:48:03
Showing 10 changed files
... ...
@@ -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
+}
... ...
@@ -1122,7 +1122,6 @@ func buildImageCmd(name, dockerfile string, useCache bool, buildFlags ...string)
1122 1122
 	buildCmd := exec.Command(dockerBinary, args...)
1123 1123
 	buildCmd.Stdin = strings.NewReader(dockerfile)
1124 1124
 	return buildCmd
1125
-
1126 1125
 }
1127 1126
 
1128 1127
 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
 }