Browse Source

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 10 years ago
parent
commit
cdfdfbfb62

+ 1 - 0
builder/command/command.go

@@ -3,6 +3,7 @@ package command
 
 
 const (
 const (
 	Env        = "env"
 	Env        = "env"
+	Label      = "label"
 	Maintainer = "maintainer"
 	Maintainer = "maintainer"
 	Add        = "add"
 	Add        = "add"
 	Copy       = "copy"
 	Copy       = "copy"

+ 31 - 0
builder/dispatchers.go

@@ -85,6 +85,37 @@ func maintainer(b *Builder, args []string, attributes map[string]bool, original
 	return b.commit("", b.Config.Cmd, fmt.Sprintf("MAINTAINER %s", b.maintainer))
 	return b.commit("", b.Config.Cmd, fmt.Sprintf("MAINTAINER %s", b.maintainer))
 }
 }
 
 
+// LABEL some json data describing the image
+//
+// Sets the Label variable foo to bar,
+//
+func label(b *Builder, args []string, attributes map[string]bool, original string) error {
+	if len(args) == 0 {
+		return fmt.Errorf("LABEL is missing arguments")
+	}
+	if len(args)%2 != 0 {
+		// should never get here, but just in case
+		return fmt.Errorf("Bad input to LABEL, too many args")
+	}
+
+	commitStr := "LABEL"
+
+	if b.Config.Labels == nil {
+		b.Config.Labels = map[string]string{}
+	}
+
+	for j := 0; j < len(args); j++ {
+		// name  ==> args[j]
+		// value ==> args[j+1]
+		newVar := args[j] + "=" + args[j+1] + ""
+		commitStr += " " + newVar
+
+		b.Config.Labels[args[j]] = args[j+1]
+		j++
+	}
+	return b.commit("", b.Config.Cmd, commitStr)
+}
+
 // ADD foo /path
 // ADD foo /path
 //
 //
 // Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling
 // Add the file 'foo' to '/path'. Tarball and Remote URL (git, http) handling

+ 1 - 0
builder/evaluator.go

@@ -62,6 +62,7 @@ var evaluateTable map[string]func(*Builder, []string, map[string]bool, string) e
 func init() {
 func init() {
 	evaluateTable = map[string]func(*Builder, []string, map[string]bool, string) error{
 	evaluateTable = map[string]func(*Builder, []string, map[string]bool, string) error{
 		command.Env:        env,
 		command.Env:        env,
+		command.Label:      label,
 		command.Maintainer: maintainer,
 		command.Maintainer: maintainer,
 		command.Add:        add,
 		command.Add:        add,
 		command.Copy:       dispatchCopy, // copy() is a go builtin
 		command.Copy:       dispatchCopy, // copy() is a go builtin

+ 14 - 6
builder/parser/line_parsers.go

@@ -44,10 +44,10 @@ func parseSubCommand(rest string) (*Node, map[string]bool, error) {
 
 
 // parse environment like statements. Note that this does *not* handle
 // parse environment like statements. Note that this does *not* handle
 // variable interpolation, which will be handled in the evaluator.
 // variable interpolation, which will be handled in the evaluator.
-func parseEnv(rest string) (*Node, map[string]bool, error) {
+func parseNameVal(rest string, key string) (*Node, map[string]bool, error) {
 	// This is kind of tricky because we need to support the old
 	// This is kind of tricky because we need to support the old
-	// variant:   ENV name value
-	// as well as the new one:    ENV name=value ...
+	// variant:   KEY name value
+	// as well as the new one:    KEY name=value ...
 	// The trigger to know which one is being used will be whether we hit
 	// The trigger to know which one is being used will be whether we hit
 	// a space or = first.  space ==> old, "=" ==> new
 	// a space or = first.  space ==> old, "=" ==> new
 
 
@@ -137,10 +137,10 @@ func parseEnv(rest string) (*Node, map[string]bool, error) {
 	}
 	}
 
 
 	if len(words) == 0 {
 	if len(words) == 0 {
-		return nil, nil, fmt.Errorf("ENV requires at least one argument")
+		return nil, nil, fmt.Errorf(key + " requires at least one argument")
 	}
 	}
 
 
-	// Old format (ENV name value)
+	// Old format (KEY name value)
 	var rootnode *Node
 	var rootnode *Node
 
 
 	if !strings.Contains(words[0], "=") {
 	if !strings.Contains(words[0], "=") {
@@ -149,7 +149,7 @@ func parseEnv(rest string) (*Node, map[string]bool, error) {
 		strs := TOKEN_WHITESPACE.Split(rest, 2)
 		strs := TOKEN_WHITESPACE.Split(rest, 2)
 
 
 		if len(strs) < 2 {
 		if len(strs) < 2 {
-			return nil, nil, fmt.Errorf("ENV must have two arguments")
+			return nil, nil, fmt.Errorf(key + " must have two arguments")
 		}
 		}
 
 
 		node.Value = strs[0]
 		node.Value = strs[0]
@@ -182,6 +182,14 @@ func parseEnv(rest string) (*Node, map[string]bool, error) {
 	return rootnode, nil, nil
 	return rootnode, nil, nil
 }
 }
 
 
+func parseEnv(rest string) (*Node, map[string]bool, error) {
+	return parseNameVal(rest, "ENV")
+}
+
+func parseLabel(rest string) (*Node, map[string]bool, error) {
+	return parseNameVal(rest, "LABEL")
+}
+
 // parses a whitespace-delimited set of arguments. The result is effectively a
 // parses a whitespace-delimited set of arguments. The result is effectively a
 // linked list of string arguments.
 // linked list of string arguments.
 func parseStringsWhitespaceDelimited(rest string) (*Node, map[string]bool, error) {
 func parseStringsWhitespaceDelimited(rest string) (*Node, map[string]bool, error) {

+ 1 - 0
builder/parser/parser.go

@@ -50,6 +50,7 @@ func init() {
 		command.Onbuild:    parseSubCommand,
 		command.Onbuild:    parseSubCommand,
 		command.Workdir:    parseString,
 		command.Workdir:    parseString,
 		command.Env:        parseEnv,
 		command.Env:        parseEnv,
+		command.Label:      parseLabel,
 		command.Maintainer: parseString,
 		command.Maintainer: parseString,
 		command.From:       parseString,
 		command.From:       parseString,
 		command.Add:        parseMaybeJSONToList,
 		command.Add:        parseMaybeJSONToList,

+ 1 - 0
contrib/syntax/kate/Dockerfile.xml

@@ -22,6 +22,7 @@
       <item> CMD </item>
       <item> CMD </item>
       <item> WORKDIR </item>
       <item> WORKDIR </item>
       <item> USER </item>
       <item> USER </item>
+      <item> LABEL </item>
     </list>
     </list>
 
 
     <contexts>
     <contexts>

+ 1 - 1
contrib/syntax/textmate/Docker.tmbundle/Syntaxes/Dockerfile.tmLanguage

@@ -12,7 +12,7 @@
 	<array>
 	<array>
 		<dict>
 		<dict>
 			<key>match</key>
 			<key>match</key>
-			<string>^\s*(ONBUILD\s+)?(FROM|MAINTAINER|RUN|EXPOSE|ENV|ADD|VOLUME|USER|WORKDIR|COPY)\s</string>
+			<string>^\s*(ONBUILD\s+)?(FROM|MAINTAINER|RUN|EXPOSE|ENV|ADD|VOLUME|USER|LABEL|WORKDIR|COPY)\s</string>
 			<key>captures</key>
 			<key>captures</key>
 			<dict>
 			<dict>
 				<key>0</key>
 				<key>0</key>

+ 1 - 1
contrib/syntax/vim/syntax/dockerfile.vim

@@ -11,7 +11,7 @@ let b:current_syntax = "dockerfile"
 
 
 syntax case ignore
 syntax case ignore
 
 
-syntax match dockerfileKeyword /\v^\s*(ONBUILD\s+)?(ADD|CMD|ENTRYPOINT|ENV|EXPOSE|FROM|MAINTAINER|RUN|USER|VOLUME|WORKDIR|COPY)\s/
+syntax match dockerfileKeyword /\v^\s*(ONBUILD\s+)?(ADD|CMD|ENTRYPOINT|ENV|EXPOSE|FROM|MAINTAINER|RUN|USER|LABEL|VOLUME|WORKDIR|COPY)\s/
 highlight link dockerfileKeyword Keyword
 highlight link dockerfileKeyword Keyword
 
 
 syntax region dockerfileString start=/\v"/ skip=/\v\\./ end=/\v"/
 syntax region dockerfileString start=/\v"/ skip=/\v\\./ end=/\v"/

+ 7 - 0
docs/sources/reference/api/docker_remote_api.md

@@ -71,6 +71,13 @@ This endpoint now returns `SystemTime`, `HttpProxy`,`HttpsProxy` and `NoProxy`.
 
 
 ### What's new
 ### What's new
 
 
+**New!**
+Build now has support for `LABEL` command which can be used to add user data
+to an image.  For example you could add data describing the content of an image.
+
+`LABEL "Vendor"="ACME Incorporated"`
+
+**New!**
 `POST /containers/(id)/attach` and `POST /exec/(id)/start`
 `POST /containers/(id)/attach` and `POST /exec/(id)/start`
 
 
 **New!**
 **New!**

+ 6 - 0
docs/sources/reference/api/docker_remote_api_v1.17.md

@@ -129,6 +129,11 @@ Create a container
              ],
              ],
              "Entrypoint": "",
              "Entrypoint": "",
              "Image": "ubuntu",
              "Image": "ubuntu",
+             "Labels": {
+                     "Vendor": "Acme",
+                     "License": "GPL",
+                     "Version": "1.0"
+             },
              "Volumes": {
              "Volumes": {
                      "/tmp": {}
                      "/tmp": {}
              },
              },
@@ -1169,6 +1174,7 @@ Return low-level information on the image `name`
                              "Cmd": ["/bin/bash"],
                              "Cmd": ["/bin/bash"],
                              "Dns": null,
                              "Dns": null,
                              "Image": "ubuntu",
                              "Image": "ubuntu",
+                             "Labels": null,
                              "Volumes": null,
                              "Volumes": null,
                              "VolumesFrom": "",
                              "VolumesFrom": "",
                              "WorkingDir": ""
                              "WorkingDir": ""

+ 12 - 0
docs/sources/reference/builder.md

@@ -328,6 +328,17 @@ default specified in `CMD`.
 > the result; `CMD` does not execute anything at build time, but specifies
 > the result; `CMD` does not execute anything at build time, but specifies
 > the intended command for the image.
 > the intended command for the image.
 
 
+## LABEL
+   LABEL <key>=<value> <key>=<value> <key>=<value> ...
+
+ --The `LABEL` instruction allows you to describe the image your `Dockerfile`
+is building. `LABEL` is specified as name value pairs. This data can
+be retrieved using the `docker inspect` command
+
+
+    LABEL Description="This image is used to start the foobar executable" Vendor="ACME Products"
+    LABEL Version="1.0"
+
 ## EXPOSE
 ## EXPOSE
 
 
     EXPOSE <port> [<port>...]
     EXPOSE <port> [<port>...]
@@ -907,6 +918,7 @@ For example you might add something like this:
     FROM      ubuntu
     FROM      ubuntu
     MAINTAINER Victor Vieux <victor@docker.com>
     MAINTAINER Victor Vieux <victor@docker.com>
 
 
+    LABEL Description="This image is used to start the foobar executable" Vendor="ACME Products" Version="1.0"
     RUN apt-get update && apt-get install -y inotify-tools nginx apache2 openssh-server
     RUN apt-get update && apt-get install -y inotify-tools nginx apache2 openssh-server
 
 
     # Firefox over VNC
     # Firefox over VNC

+ 22 - 0
integration-cli/docker_cli_build_test.go

@@ -4541,6 +4541,28 @@ func TestBuildWithTabs(t *testing.T) {
 	logDone("build - with tabs")
 	logDone("build - with tabs")
 }
 }
 
 
+func TestBuildLabels(t *testing.T) {
+	name := "testbuildlabel"
+	expected := `{"License":"GPL","Vendor":"Acme"}`
+	defer deleteImages(name)
+	_, err := buildImage(name,
+		`FROM busybox
+		LABEL Vendor=Acme
+                LABEL License GPL`,
+		true)
+	if err != nil {
+		t.Fatal(err)
+	}
+	res, err := inspectFieldJSON(name, "Config.Labels")
+	if err != nil {
+		t.Fatal(err)
+	}
+	if res != expected {
+		t.Fatalf("Labels %s, expected %s", res, expected)
+	}
+	logDone("build - label")
+}
+
 func TestBuildStderr(t *testing.T) {
 func TestBuildStderr(t *testing.T) {
 	// This test just makes sure that no non-error output goes
 	// This test just makes sure that no non-error output goes
 	// to stderr
 	// to stderr

+ 5 - 0
runconfig/config.go

@@ -33,6 +33,8 @@ type Config struct {
 	NetworkDisabled bool
 	NetworkDisabled bool
 	MacAddress      string
 	MacAddress      string
 	OnBuild         []string
 	OnBuild         []string
+	SecurityOpt     []string
+	Labels          map[string]string
 }
 }
 
 
 func ContainerConfigFromJob(job *engine.Job) *Config {
 func ContainerConfigFromJob(job *engine.Job) *Config {
@@ -66,6 +68,9 @@ func ContainerConfigFromJob(job *engine.Job) *Config {
 	if Cmd := job.GetenvList("Cmd"); Cmd != nil {
 	if Cmd := job.GetenvList("Cmd"); Cmd != nil {
 		config.Cmd = Cmd
 		config.Cmd = Cmd
 	}
 	}
+
+	job.GetenvJson("Labels", &config.Labels)
+
 	if Entrypoint := job.GetenvList("Entrypoint"); Entrypoint != nil {
 	if Entrypoint := job.GetenvList("Entrypoint"); Entrypoint != nil {
 		config.Entrypoint = Entrypoint
 		config.Entrypoint = Entrypoint
 	}
 	}

+ 10 - 0
runconfig/merge.go

@@ -84,6 +84,16 @@ func Merge(userConf, imageConf *Config) error {
 		}
 		}
 	}
 	}
 
 
+	if userConf.Labels == nil {
+		userConf.Labels = map[string]string{}
+	}
+	if imageConf.Labels != nil {
+		for l := range userConf.Labels {
+			imageConf.Labels[l] = userConf.Labels[l]
+		}
+		userConf.Labels = imageConf.Labels
+	}
+
 	if len(userConf.Entrypoint) == 0 {
 	if len(userConf.Entrypoint) == 0 {
 		if len(userConf.Cmd) == 0 {
 		if len(userConf.Cmd) == 0 {
 			userConf.Cmd = imageConf.Cmd
 			userConf.Cmd = imageConf.Cmd