Browse code

Allow specification of Label Name/Value pairs in image json content

Save "LABEL" field in Dockerfile into image content.

This will allow a user to save user data into an image, which
can later be retrieved using:

docker inspect IMAGEID

I have copied this from the "Comment" handling in docker images.

We want to be able to add Name/Value data to an image to describe the image,
and then be able to use other tools to look at this data, to be able to do
security checks based on this data.

We are thinking about adding version names,
Perhaps listing the content of the dockerfile.
Descriptions of where the code came from etc.

This LABEL field should also be allowed to be specified in the
docker import --change LABEL:Name=Value
docker commit --change LABEL:Name=Value

Docker-DCO-1.1-Signed-off-by: Dan Walsh <dwalsh@redhat.com> (github: rhatdan)

Dan Walsh authored on 2015/02/18 00:20:06
Showing 14 changed files
... ...
@@ -3,6 +3,7 @@ package command
3 3
 
4 4
 const (
5 5
 	Env        = "env"
6
+	Label      = "label"
6 7
 	Maintainer = "maintainer"
7 8
 	Add        = "add"
8 9
 	Copy       = "copy"
... ...
@@ -85,6 +85,37 @@ func maintainer(b *Builder, args []string, attributes map[string]bool, original
85 85
 	return b.commit("", b.Config.Cmd, fmt.Sprintf("MAINTAINER %s", b.maintainer))
86 86
 }
87 87
 
88
+// LABEL some json data describing the image
89
+//
90
+// Sets the Label variable foo to bar,
91
+//
92
+func label(b *Builder, args []string, attributes map[string]bool, original string) error {
93
+	if len(args) == 0 {
94
+		return fmt.Errorf("LABEL is missing arguments")
95
+	}
96
+	if len(args)%2 != 0 {
97
+		// should never get here, but just in case
98
+		return fmt.Errorf("Bad input to LABEL, too many args")
99
+	}
100
+
101
+	commitStr := "LABEL"
102
+
103
+	if b.Config.Labels == nil {
104
+		b.Config.Labels = map[string]string{}
105
+	}
106
+
107
+	for j := 0; j < len(args); j++ {
108
+		// name  ==> args[j]
109
+		// value ==> args[j+1]
110
+		newVar := args[j] + "=" + args[j+1] + ""
111
+		commitStr += " " + newVar
112
+
113
+		b.Config.Labels[args[j]] = args[j+1]
114
+		j++
115
+	}
116
+	return b.commit("", b.Config.Cmd, commitStr)
117
+}
118
+
88 119
 // ADD foo /path
89 120
 //
90 121
 // Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling
... ...
@@ -62,6 +62,7 @@ var evaluateTable map[string]func(*Builder, []string, map[string]bool, string) e
62 62
 func init() {
63 63
 	evaluateTable = map[string]func(*Builder, []string, map[string]bool, string) error{
64 64
 		command.Env:        env,
65
+		command.Label:      label,
65 66
 		command.Maintainer: maintainer,
66 67
 		command.Add:        add,
67 68
 		command.Copy:       dispatchCopy, // copy() is a go builtin
... ...
@@ -44,10 +44,10 @@ func parseSubCommand(rest string) (*Node, map[string]bool, error) {
44 44
 
45 45
 // parse environment like statements. Note that this does *not* handle
46 46
 // variable interpolation, which will be handled in the evaluator.
47
-func parseEnv(rest string) (*Node, map[string]bool, error) {
47
+func parseNameVal(rest string, key string) (*Node, map[string]bool, error) {
48 48
 	// This is kind of tricky because we need to support the old
49
-	// variant:   ENV name value
50
-	// as well as the new one:    ENV name=value ...
49
+	// variant:   KEY name value
50
+	// as well as the new one:    KEY name=value ...
51 51
 	// The trigger to know which one is being used will be whether we hit
52 52
 	// a space or = first.  space ==> old, "=" ==> new
53 53
 
... ...
@@ -137,10 +137,10 @@ func parseEnv(rest string) (*Node, map[string]bool, error) {
137 137
 	}
138 138
 
139 139
 	if len(words) == 0 {
140
-		return nil, nil, fmt.Errorf("ENV requires at least one argument")
140
+		return nil, nil, fmt.Errorf(key + " requires at least one argument")
141 141
 	}
142 142
 
143
-	// Old format (ENV name value)
143
+	// Old format (KEY name value)
144 144
 	var rootnode *Node
145 145
 
146 146
 	if !strings.Contains(words[0], "=") {
... ...
@@ -149,7 +149,7 @@ func parseEnv(rest string) (*Node, map[string]bool, error) {
149 149
 		strs := TOKEN_WHITESPACE.Split(rest, 2)
150 150
 
151 151
 		if len(strs) < 2 {
152
-			return nil, nil, fmt.Errorf("ENV must have two arguments")
152
+			return nil, nil, fmt.Errorf(key + " must have two arguments")
153 153
 		}
154 154
 
155 155
 		node.Value = strs[0]
... ...
@@ -182,6 +182,14 @@ func parseEnv(rest string) (*Node, map[string]bool, error) {
182 182
 	return rootnode, nil, nil
183 183
 }
184 184
 
185
+func parseEnv(rest string) (*Node, map[string]bool, error) {
186
+	return parseNameVal(rest, "ENV")
187
+}
188
+
189
+func parseLabel(rest string) (*Node, map[string]bool, error) {
190
+	return parseNameVal(rest, "LABEL")
191
+}
192
+
185 193
 // parses a whitespace-delimited set of arguments. The result is effectively a
186 194
 // linked list of string arguments.
187 195
 func parseStringsWhitespaceDelimited(rest string) (*Node, map[string]bool, error) {
... ...
@@ -50,6 +50,7 @@ func init() {
50 50
 		command.Onbuild:    parseSubCommand,
51 51
 		command.Workdir:    parseString,
52 52
 		command.Env:        parseEnv,
53
+		command.Label:      parseLabel,
53 54
 		command.Maintainer: parseString,
54 55
 		command.From:       parseString,
55 56
 		command.Add:        parseMaybeJSONToList,
... ...
@@ -22,6 +22,7 @@
22 22
       <item> CMD </item>
23 23
       <item> WORKDIR </item>
24 24
       <item> USER </item>
25
+      <item> LABEL </item>
25 26
     </list>
26 27
 
27 28
     <contexts>
... ...
@@ -12,7 +12,7 @@
12 12
 	<array>
13 13
 		<dict>
14 14
 			<key>match</key>
15
-			<string>^\s*(ONBUILD\s+)?(FROM|MAINTAINER|RUN|EXPOSE|ENV|ADD|VOLUME|USER|WORKDIR|COPY)\s</string>
15
+			<string>^\s*(ONBUILD\s+)?(FROM|MAINTAINER|RUN|EXPOSE|ENV|ADD|VOLUME|USER|LABEL|WORKDIR|COPY)\s</string>
16 16
 			<key>captures</key>
17 17
 			<dict>
18 18
 				<key>0</key>
... ...
@@ -11,7 +11,7 @@ let b:current_syntax = "dockerfile"
11 11
 
12 12
 syntax case ignore
13 13
 
14
-syntax match dockerfileKeyword /\v^\s*(ONBUILD\s+)?(ADD|CMD|ENTRYPOINT|ENV|EXPOSE|FROM|MAINTAINER|RUN|USER|VOLUME|WORKDIR|COPY)\s/
14
+syntax match dockerfileKeyword /\v^\s*(ONBUILD\s+)?(ADD|CMD|ENTRYPOINT|ENV|EXPOSE|FROM|MAINTAINER|RUN|USER|LABEL|VOLUME|WORKDIR|COPY)\s/
15 15
 highlight link dockerfileKeyword Keyword
16 16
 
17 17
 syntax region dockerfileString start=/\v"/ skip=/\v\\./ end=/\v"/
... ...
@@ -71,6 +71,13 @@ This endpoint now returns `SystemTime`, `HttpProxy`,`HttpsProxy` and `NoProxy`.
71 71
 
72 72
 ### What's new
73 73
 
74
+**New!**
75
+Build now has support for `LABEL` command which can be used to add user data
76
+to an image.  For example you could add data describing the content of an image.
77
+
78
+`LABEL "Vendor"="ACME Incorporated"`
79
+
80
+**New!**
74 81
 `POST /containers/(id)/attach` and `POST /exec/(id)/start`
75 82
 
76 83
 **New!**
... ...
@@ -129,6 +129,11 @@ Create a container
129 129
              ],
130 130
              "Entrypoint": "",
131 131
              "Image": "ubuntu",
132
+             "Labels": {
133
+                     "Vendor": "Acme",
134
+                     "License": "GPL",
135
+                     "Version": "1.0"
136
+             },
132 137
              "Volumes": {
133 138
                      "/tmp": {}
134 139
              },
... ...
@@ -1169,6 +1174,7 @@ Return low-level information on the image `name`
1169 1169
                              "Cmd": ["/bin/bash"],
1170 1170
                              "Dns": null,
1171 1171
                              "Image": "ubuntu",
1172
+                             "Labels": null,
1172 1173
                              "Volumes": null,
1173 1174
                              "VolumesFrom": "",
1174 1175
                              "WorkingDir": ""
... ...
@@ -328,6 +328,17 @@ default specified in `CMD`.
328 328
 > the result; `CMD` does not execute anything at build time, but specifies
329 329
 > the intended command for the image.
330 330
 
331
+## LABEL
332
+   LABEL <key>=<value> <key>=<value> <key>=<value> ...
333
+
334
+ --The `LABEL` instruction allows you to describe the image your `Dockerfile`
335
+is building. `LABEL` is specified as name value pairs. This data can
336
+be retrieved using the `docker inspect` command
337
+
338
+
339
+    LABEL Description="This image is used to start the foobar executable" Vendor="ACME Products"
340
+    LABEL Version="1.0"
341
+
331 342
 ## EXPOSE
332 343
 
333 344
     EXPOSE <port> [<port>...]
... ...
@@ -907,6 +918,7 @@ For example you might add something like this:
907 907
     FROM      ubuntu
908 908
     MAINTAINER Victor Vieux <victor@docker.com>
909 909
 
910
+    LABEL Description="This image is used to start the foobar executable" Vendor="ACME Products" Version="1.0"
910 911
     RUN apt-get update && apt-get install -y inotify-tools nginx apache2 openssh-server
911 912
 
912 913
     # Firefox over VNC
... ...
@@ -4541,6 +4541,28 @@ func TestBuildWithTabs(t *testing.T) {
4541 4541
 	logDone("build - with tabs")
4542 4542
 }
4543 4543
 
4544
+func TestBuildLabels(t *testing.T) {
4545
+	name := "testbuildlabel"
4546
+	expected := `{"License":"GPL","Vendor":"Acme"}`
4547
+	defer deleteImages(name)
4548
+	_, err := buildImage(name,
4549
+		`FROM busybox
4550
+		LABEL Vendor=Acme
4551
+                LABEL License GPL`,
4552
+		true)
4553
+	if err != nil {
4554
+		t.Fatal(err)
4555
+	}
4556
+	res, err := inspectFieldJSON(name, "Config.Labels")
4557
+	if err != nil {
4558
+		t.Fatal(err)
4559
+	}
4560
+	if res != expected {
4561
+		t.Fatalf("Labels %s, expected %s", res, expected)
4562
+	}
4563
+	logDone("build - label")
4564
+}
4565
+
4544 4566
 func TestBuildStderr(t *testing.T) {
4545 4567
 	// This test just makes sure that no non-error output goes
4546 4568
 	// to stderr
... ...
@@ -33,6 +33,8 @@ type Config struct {
33 33
 	NetworkDisabled bool
34 34
 	MacAddress      string
35 35
 	OnBuild         []string
36
+	SecurityOpt     []string
37
+	Labels          map[string]string
36 38
 }
37 39
 
38 40
 func ContainerConfigFromJob(job *engine.Job) *Config {
... ...
@@ -66,6 +68,9 @@ func ContainerConfigFromJob(job *engine.Job) *Config {
66 66
 	if Cmd := job.GetenvList("Cmd"); Cmd != nil {
67 67
 		config.Cmd = Cmd
68 68
 	}
69
+
70
+	job.GetenvJson("Labels", &config.Labels)
71
+
69 72
 	if Entrypoint := job.GetenvList("Entrypoint"); Entrypoint != nil {
70 73
 		config.Entrypoint = Entrypoint
71 74
 	}
... ...
@@ -84,6 +84,16 @@ func Merge(userConf, imageConf *Config) error {
84 84
 		}
85 85
 	}
86 86
 
87
+	if userConf.Labels == nil {
88
+		userConf.Labels = map[string]string{}
89
+	}
90
+	if imageConf.Labels != nil {
91
+		for l := range userConf.Labels {
92
+			imageConf.Labels[l] = userConf.Labels[l]
93
+		}
94
+		userConf.Labels = imageConf.Labels
95
+	}
96
+
87 97
 	if len(userConf.Entrypoint) == 0 {
88 98
 		if len(userConf.Cmd) == 0 {
89 99
 			userConf.Cmd = imageConf.Cmd