Browse Source

Merge pull request #9882 from ibuildthecloud/labels

Proposal: One Meta Data to Rule Them All => Labels
Arnaud Porterie 10 years ago
parent
commit
b6ac111abf

+ 1 - 0
builder/command/command.go

@@ -3,6 +3,7 @@ package command
 
 const (
 	Env        = "env"
+	Label      = "label"
 	Maintainer = "maintainer"
 	Add        = "add"
 	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))
 }
 
+// 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 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() {
 	evaluateTable = map[string]func(*Builder, []string, map[string]bool, string) error{
 		command.Env:        env,
+		command.Label:      label,
 		command.Maintainer: maintainer,
 		command.Add:        add,
 		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
 // 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
-	// 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
 	// a space or = first.  space ==> old, "=" ==> new
 
@@ -137,10 +137,10 @@ func parseEnv(rest string) (*Node, map[string]bool, error) {
 	}
 
 	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
 
 	if !strings.Contains(words[0], "=") {
@@ -149,7 +149,7 @@ func parseEnv(rest string) (*Node, map[string]bool, error) {
 		strs := TOKEN_WHITESPACE.Split(rest, 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]
@@ -182,6 +182,14 @@ func parseEnv(rest string) (*Node, map[string]bool, error) {
 	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
 // linked list of string arguments.
 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.Workdir:    parseString,
 		command.Env:        parseEnv,
+		command.Label:      parseLabel,
 		command.Maintainer: parseString,
 		command.From:       parseString,
 		command.Add:        parseMaybeJSONToList,

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

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

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

@@ -12,7 +12,7 @@
 	<array>
 		<dict>
 			<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>
 			<dict>
 				<key>0</key>

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

@@ -11,7 +11,7 @@ let b:current_syntax = "dockerfile"
 
 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
 
 syntax region dockerfileString start=/\v"/ skip=/\v\\./ end=/\v"/

+ 5 - 0
daemon/list.go

@@ -90,6 +90,10 @@ func (daemon *Daemon) Containers(job *engine.Job) engine.Status {
 			return nil
 		}
 
+		if !psFilters.MatchKVList("label", container.Config.Labels) {
+			return nil
+		}
+
 		if before != "" && !foundBefore {
 			if container.ID == beforeCont.ID {
 				foundBefore = true
@@ -157,6 +161,7 @@ func (daemon *Daemon) Containers(job *engine.Job) engine.Status {
 			out.SetInt64("SizeRw", sizeRw)
 			out.SetInt64("SizeRootFs", sizeRootFs)
 		}
+		out.SetJson("Labels", container.Config.Labels)
 		outs.Add(out)
 		return nil
 	}

+ 15 - 0
docs/man/Dockerfile.5.md

@@ -149,6 +149,21 @@ A Dockerfile is similar to a Makefile.
   **CMD** executes nothing at build time, but specifies the intended command for
   the image.
 
+**LABEL**
+  -- `LABEL <key>[=<value>] [<key>[=<value>] ...]`
+  The **LABEL** instruction adds metadata to an image. A **LABEL** is a
+  key-value pair. To include spaces within a **LABEL** value, use quotes and
+  blackslashes as you would in command-line parsing.
+
+  ```
+  LABEL "com.example.vendor"="ACME Incorporated"
+  ```
+
+  An image can have more than one label. To specify multiple labels, separate each
+  key-value pair by a space.
+
+  To display an image's labels, use the `docker inspect` command.
+
 **EXPOSE**
   -- `EXPOSE <port> [<port>...]`
   The **EXPOSE** instruction informs Docker that the container listens on the

+ 8 - 0
docs/man/docker-create.1.md

@@ -24,6 +24,8 @@ docker-create - Create a new container
 [**--help**]
 [**-i**|**--interactive**[=*false*]]
 [**--ipc**[=*IPC*]]
+[**-l**|**--label**[=*[]*]]
+[**--label-file**[=*[]*]]
 [**--link**[=*[]*]]
 [**--lxc-conf**[=*[]*]]
 [**-m**|**--memory**[=*MEMORY*]]
@@ -102,6 +104,12 @@ IMAGE [COMMAND] [ARG...]
                                'container:<name|id>': reuses another container shared memory, semaphores and message queues
                                'host': use the host shared memory,semaphores and message queues inside the container.  Note: the host mode gives the container full access to local shared memory and is therefore considered insecure.
 
+**-l**, **--label**=[]
+   Set metadata on the container (e.g., --label=com.example.key=value)
+
+**--label-file**=[]
+   Read in a file of labels (EOL delimited)
+
 **--link**=[]
    Add link to another container in the form of <name or id>:alias
 

+ 1 - 1
docs/man/docker-images.1.md

@@ -34,7 +34,7 @@ versions.
    Show all images (by default filter out the intermediate image layers). The default is *false*.
 
 **-f**, **--filter**=[]
-   Provide filter values (i.e., 'dangling=true')
+   Filters the output. The dangling=true filter finds unused images. While label=com.foo=amd64 filters for images with a com.foo value of amd64. The label=com.foo filter finds images with the label com.foo of any value.
 
 **--help**
   Print usage statement

+ 5 - 0
docs/man/docker-inspect.1.md

@@ -83,6 +83,11 @@ To get information on a container use it's ID or instance name:
             "Ghost": false
         },
         "Image": "df53773a4390e25936f9fd3739e0c0e60a62d024ea7b669282b27e65ae8458e6",
+        "Labels": {
+            "com.example.vendor": "Acme",
+            "com.example.license": "GPL",
+            "com.example.version": "1.0"
+        },
         "NetworkSettings": {
             "IPAddress": "172.17.0.2",
             "IPPrefixLen": 16,

+ 1 - 0
docs/man/docker-ps.1.md

@@ -36,6 +36,7 @@ the running containers.
 **-f**, **--filter**=[]
    Provide filter values. Valid filters:
                           exited=<int> - containers with exit code of <int>
+                          label=<key> or label=<key>=<value>
                           status=(restarting|running|paused|exited)
                           name=<string> - container's name
                           id=<ID> - container's ID

+ 8 - 0
docs/man/docker-run.1.md

@@ -25,6 +25,8 @@ docker-run - Run a command in a new container
 [**--help**]
 [**-i**|**--interactive**[=*false*]]
 [**--ipc**[=*IPC*]]
+[**-l**|**--label**[=*[]*]]
+[**--label-file**[=*[]*]]
 [**--link**[=*[]*]]
 [**--lxc-conf**[=*[]*]]
 [**-m**|**--memory**[=*MEMORY*]]
@@ -197,6 +199,12 @@ ENTRYPOINT.
                                'container:<name|id>': reuses another container shared memory, semaphores and message queues
                                'host': use the host shared memory,semaphores and message queues inside the container.  Note: the host mode gives the container full access to local shared memory and is therefore considered insecure.
 
+**-l**, **--label**=[]
+   Set metadata on the container (e.g., --label com.example.key=value)
+
+**--label-file**=[]
+   Read in a line delimited file of labels
+
 **--link**=[]
    Add link to another container in the form of <name or id>:alias
 

+ 1 - 0
docs/mkdocs.yml

@@ -59,6 +59,7 @@ pages:
 - ['userguide/dockerimages.md', 'User Guide', 'Working with Docker Images' ]
 - ['userguide/dockerlinks.md', 'User Guide', 'Linking containers together' ]
 - ['userguide/dockervolumes.md', 'User Guide', 'Managing data in containers' ]
+- ['userguide/labels-custom-metadata.md', 'User Guide', 'Labels - custom metadata in Docker' ]
 - ['userguide/dockerrepos.md', 'User Guide', 'Working with Docker Hub' ]
 - ['userguide/level1.md', '**HIDDEN**' ]
 - ['userguide/level2.md', '**HIDDEN**' ]

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

@@ -71,15 +71,32 @@ This endpoint now returns `SystemTime`, `HttpProxy`,`HttpsProxy` and `NoProxy`.
 
 ### What's new
 
+The build supports `LABEL` command. Use this to add metadata
+to an image. For example you could add data describing the content of an image.
+
+`LABEL "com.example.vendor"="ACME Incorporated"`
+
+**New!**
 `POST /containers/(id)/attach` and `POST /exec/(id)/start`
 
 **New!**
 Docker client now hints potential proxies about connection hijacking using HTTP Upgrade headers.
 
+`POST /containers/create`
+
+**New!**
+You can set labels on container create describing the container.
+
+`GET /containers/json`
+
+**New!**
+The endpoint returns the labels associated with the containers (`Labels`).
+
 `GET /containers/(id)/json`
 
 **New!**
 This endpoint now returns the list current execs associated with the container (`ExecIDs`).
+This endpoint now returns the container labels (`Config.Labels`).
 
 `POST /containers/(id)/rename`
 
@@ -98,6 +115,12 @@ root filesystem as read only.
 **New!**
 This endpoint returns a live stream of a container's resource usage statistics.
 
+`GET /images/json`
+
+**New!**
+This endpoint now returns the labels associated with each image (`Labels`).
+
+
 ## v1.16
 
 ### Full Documentation

+ 16 - 0
docs/sources/reference/api/docker_remote_api_v1.18.md

@@ -125,6 +125,11 @@ Create a container
              ],
              "Entrypoint": "",
              "Image": "ubuntu",
+             "Labels": {
+                     "com.example.vendor": "Acme",
+                     "com.example.license": "GPL",
+                     "com.example.version": "1.0"
+             },
              "Volumes": {
                      "/tmp": {}
              },
@@ -191,6 +196,7 @@ Json Parameters:
 -   **OpenStdin** - Boolean value, opens stdin,
 -   **StdinOnce** - Boolean value, close stdin after the 1 attached client disconnects.
 -   **Env** - A list of environment variables in the form of `VAR=value`
+-   **Labels** - Adds a map of labels that to a container. To specify a map: `{"key":"value"[,"key2":"value2"]}`
 -   **Cmd** - Command to run specified as a string or an array of strings.
 -   **Entrypoint** - Set the entrypoint for the container a a string or an array
       of strings
@@ -301,6 +307,11 @@ Return low-level information on the container `id`
 			"ExposedPorts": null,
 			"Hostname": "ba033ac44011",
 			"Image": "ubuntu",
+			"Labels": {
+				"com.example.vendor": "Acme",
+				"com.example.license": "GPL",
+				"com.example.version": "1.0"
+			},
 			"MacAddress": "",
 			"NetworkDisabled": false,
 			"OnBuild": null,
@@ -1185,6 +1196,11 @@ Return low-level information on the image `name`
                              "Cmd": ["/bin/bash"],
                              "Dns": null,
                              "Image": "ubuntu",
+                             "Labels": {
+                                 "com.example.vendor": "Acme",
+                                 "com.example.license": "GPL",
+                                 "com.example.version": "1.0"
+                             },
                              "Volumes": null,
                              "VolumesFrom": "",
                              "WorkingDir": ""

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

@@ -328,6 +328,27 @@ default specified in `CMD`.
 > the result; `CMD` does not execute anything at build time, but specifies
 > the intended command for the image.
 
+## LABEL
+
+    LABEL <key>=<value> <key>=<value> <key>=<value> ...
+
+The `LABEL` instruction adds metadata to an image. A `LABEL` is a
+key-value pair. To include spaces within a `LABEL` value, use quotes and
+blackslashes as you would in command-line parsing.
+
+    LABEL "com.example.vendor"="ACME Incorporated"
+
+An image can have more than one label. To specify multiple labels, separate each
+key-value pair by an EOL.
+
+    LABEL com.example.label-without-value
+    LABEL com.example.label-with-value="foo"
+    LABEL version="1.0"
+    LABEL description="This text illustrates \
+    that label-values can span multiple lines."
+
+To view an image's labels, use the `docker inspect` command.
+
 ## EXPOSE
 
     EXPOSE <port> [<port>...]
@@ -907,6 +928,7 @@ For example you might add something like this:
     FROM      ubuntu
     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
 
     # Firefox over VNC

+ 38 - 2
docs/sources/reference/commandline/cli.md

@@ -814,6 +814,8 @@ Creates a new container.
       -h, --hostname=""          Container host name
       -i, --interactive=false    Keep STDIN open even if not attached
       --ipc=""                   IPC namespace to use
+      -l, --label=[]             Set metadata on the container (e.g., --label=com.example.key=value)
+      --label-file=[]            Read in a line delimited file of labels
       --link=[]                  Add link to another container
       --lxc-conf=[]              Add custom lxc options
       -m, --memory=""            Memory limit
@@ -1166,6 +1168,7 @@ than one filter, then pass multiple flags (e.g., `--filter "foo=bar" --filter "b
 
 Current filters:
  * dangling (boolean - true or false)
+ * label (`label=<key>` or `label=<key>=<value>`)
 
 ##### Untagged images
 
@@ -1685,6 +1688,8 @@ removed before the image is removed.
       --link=[]                  Add link to another container
       --lxc-conf=[]              Add custom lxc options
       -m, --memory=""            Memory limit
+      -l, --label=[]             Set metadata on the container (e.g., --label=com.example.key=value)
+      --label-file=[]            Read in a file of labels (EOL delimited)
       --mac-address=""           Container MAC address (e.g. 92:d0:c6:0a:29:33)
       --memory-swap=""           Total memory (memory + swap), '-1' to disable swap
       --name=""                  Assign a name to the container
@@ -1855,8 +1860,39 @@ An example of a file passed with `--env-file`
 
     $ sudo docker run --name console -t -i ubuntu bash
 
-This will create and run a new container with the container name being
-`console`.
+A label is a a `key=value` pair that applies metadata to a container. To label a container with two labels:
+
+    $ sudo docker run -l my-label --label com.example.foo=bar ubuntu bash
+
+The `my-label` key doesn't specify so the label defaults to an empty
+string(`""`). To add multiple labels, repeat the label flag (`-l` or
+`--label`).
+
+The `key=value` must be unique. If you specify the same key multiple times
+with different values, each subsequent value overwrites the previous. Docker
+applies the last `key=value` you supply.
+
+Use the `--label-file` flag to load multiple labels from a file. Delimit each
+label in the file with an EOL mark. The example below loads labels from a
+labels file in the current directory;
+
+    $ sudo docker run --label-file ./labels ubuntu bash
+
+The label-file format is similar to the format for loading environment variables
+(see `--env-file` above). The following example illustrates a label-file format;
+
+    com.example.label1="a label"
+
+    # this is a comment
+    com.example.label2=another\ label
+    com.example.label3
+
+You can load multiple label-files by supplying the `--label-file` flag multiple
+times.
+
+For additional information on working with labels, see
+[*Labels - custom metadata in Docker*](/userguide/labels-custom-metadata/) in
+the Docker User Guide.
 
     $ sudo docker run --link /redis:redis --name console ubuntu bash
 

+ 194 - 0
docs/sources/userguide/labels-custom-metadata.md

@@ -0,0 +1,194 @@
+page_title: Labels - custom metadata in Docker
+page_description: Learn how to work with custom metadata in Docker, using labels.
+page_keywords: Usage, user guide, labels, metadata, docker, documentation, examples, annotating
+
+## Labels - custom metadata in Docker
+
+You can add metadata to your images, containers, and daemons via
+labels. Metadata can serve a wide range of uses. Use them to add notes or
+licensing information to an image or to identify a host.
+
+A label is a `<key>` / `<value>` pair. Docker stores the values as *strings*.
+You can specify multiple labels but each `<key>` / `<value>` must be unique.  If
+you specify the same `key` multiple times with different values, each subsequent
+value overwrites the previous. Docker applies the last `key=value` you supply.
+
+>**note:** Support for daemon-labels was added in Docker 1.4.1. Labels on
+>containers and images are new in Docker 1.6.0
+
+### Naming your labels - namespaces
+
+Docker puts no hard restrictions on the label `key` you. However, labels can
+conflict. For example, you can categorize your images by using a chip "architecture"
+label:
+
+    LABEL architecture="amd64"
+
+    LABEL architecture="ARMv7"
+
+But a user can label images by building architectural style:
+
+    LABEL architecture="Art Nouveau"
+
+To prevent such conflicts, Docker namespaces label keys using a reverse domain
+notation. This notation has the following guidelines:
+
+
+- All (third-party) tools should prefix their keys with the
+  reverse DNS notation of a domain controlled by the author. For
+  example, `com.example.some-label`.
+
+- The `com.docker.*`, `io.docker.*` and `com.dockerproject.*` namespaces are
+  reserved for Docker's internal use.
+
+- Keys should only consist of lower-cased alphanumeric characters,
+  dots and dashes (for example, `[a-z0-9-.]`)
+
+- Keys should start *and* end with an alpha numeric character
+
+- Keys may not contain consecutive dots or dashes.
+
+- Keys *without* namespace (dots) are reserved for CLI use. This allows end-
+  users to add metadata to their containers and images, without having to type
+  cumbersome namespaces on the command-line.
+
+
+These are guidelines and are not enforced. Docker does not *enforce* them.
+Failing following these guidelines can result in conflicting labels. If you're
+building a tool that uses labels, you *should* use namespaces for your label keys.
+
+
+### Storing structured data in labels
+
+Label values can contain any data type as long as the value can be stored as a
+string. For example, consider this JSON:
+
+
+    {
+        "Description": "A containerized foobar",
+        "Usage": "docker run --rm example/foobar [args]",
+        "License": "GPL",
+        "Version": "0.0.1-beta",
+        "aBoolean": true,
+        "aNumber" : 0.01234,
+        "aNestedArray": ["a", "b", "c"]
+    }
+
+You can store this struct in a label by serializing it to a string first:
+
+    LABEL com.example.image-specs="{\"Description\":\"A containerized foobar\",\"Usage\":\"docker run --rm example\\/foobar [args]\",\"License\":\"GPL\",\"Version\":\"0.0.1-beta\",\"aBoolean\":true,\"aNumber\":0.01234,\"aNestedArray\":[\"a\",\"b\",\"c\"]}"
+
+While it is *possible* to store structured data in label values, Docker treats this
+data as a 'regular' string. This means that Docker doesn't offer ways to query
+(filter) based on nested properties.
+
+If your tool needs to filter on nested properties, the tool itself should
+implement this.
+
+
+### Adding labels to images; the `LABEL` instruction
+
+Adding labels to an image:
+
+
+    LABEL [<namespace>.]<key>[=<value>] ...
+
+The `LABEL` instruction adds a label to your image, optionally setting its value.
+Use surrounding quotes or backslashes for labels that contain
+white space character:
+
+    LABEL vendor=ACME\ Incorporated
+    LABEL com.example.version.is-beta
+    LABEL com.example.version="0.0.1-beta"
+    LABEL com.example.release-date="2015-02-12"
+
+The `LABEL` instruction supports setting multiple labels in a single instruction
+using this notation;
+
+    LABEL com.example.version="0.0.1-beta" com.example.release-date="2015-02-12"
+
+Wrapping is allowed by using a backslash (`\`) as continuation marker:
+
+    LABEL vendor=ACME\ Incorporated \
+          com.example.is-beta \
+          com.example.version="0.0.1-beta" \
+          com.example.release-date="2015-02-12"
+
+Docker recommends combining labels in a single `LABEL` instruction instead of
+using a `LABEL` instruction for each label. Each instruction in a Dockerfile
+produces a new layer that can result in an inefficient image if you use many
+labels.
+
+You can view the labels via the `docker inspect` command:
+
+    $ docker inspect 4fa6e0f0c678
+
+    ...
+    "Labels": {
+        "vendor": "ACME Incorporated",
+        "com.example.is-beta": "",
+        "com.example.version": "0.0.1-beta",
+        "com.example.release-date": "2015-02-12"
+    }
+    ...
+
+    $ docker inspect -f "{{json .Labels }}" 4fa6e0f0c678
+
+    {"Vendor":"ACME Incorporated","com.example.is-beta":"","com.example.version":"0.0.1-beta","com.example.release-date":"2015-02-12"}
+
+
+
+### Querying labels
+
+Besides storing metadata, you can filter images and labels by label. To list all
+running containers that have a `com.example.is-beta` label:
+
+    # List all running containers that have a `com.example.is-beta` label
+    $ docker ps --filter "label=com.example.is-beta"
+
+List all running containers with a `color` label of `blue`:
+
+    $ docker ps --filter "label=color=blue"
+
+List all images with `vendor` `ACME`:
+
+    $ docker images --filter "label=vendor=ACME"
+
+
+### Daemon labels
+
+
+    docker -d \
+      --dns 8.8.8.8 \
+      --dns 8.8.4.4 \
+      -H unix:///var/run/docker.sock \
+      --label com.example.environment="production" \
+      --label com.example.storage="ssd"
+
+These labels appear as part of the `docker info` output for the daemon:
+
+    docker -D info
+    Containers: 12
+    Images: 672
+    Storage Driver: aufs
+     Root Dir: /var/lib/docker/aufs
+     Backing Filesystem: extfs
+     Dirs: 697
+    Execution Driver: native-0.2
+    Kernel Version: 3.13.0-32-generic
+    Operating System: Ubuntu 14.04.1 LTS
+    CPUs: 1
+    Total Memory: 994.1 MiB
+    Name: docker.example.com
+    ID: RC3P:JTCT:32YS:XYSB:YUBG:VFED:AAJZ:W3YW:76XO:D7NN:TEVU:UCRW
+    Debug mode (server): false
+    Debug mode (client): true
+    Fds: 11
+    Goroutines: 14
+    EventsListeners: 0
+    Init Path: /usr/bin/docker
+    Docker Root Dir: /var/lib/docker
+    WARNING: No swap limit support
+    Labels:
+     com.example.environment=production
+     com.example.storage=ssd

+ 16 - 2
graph/list.go

@@ -11,13 +11,17 @@ import (
 	"github.com/docker/docker/pkg/parsers/filters"
 )
 
-var acceptedImageFilterTags = map[string]struct{}{"dangling": {}}
+var acceptedImageFilterTags = map[string]struct{}{
+	"dangling": {},
+	"label":    {},
+}
 
 func (s *TagStore) CmdImages(job *engine.Job) engine.Status {
 	var (
 		allImages   map[string]*image.Image
 		err         error
 		filt_tagged = true
+		filt_label  = false
 	)
 
 	imageFilters, err := filters.FromParam(job.Getenv("filters"))
@@ -38,6 +42,8 @@ func (s *TagStore) CmdImages(job *engine.Job) engine.Status {
 		}
 	}
 
+	_, filt_label = imageFilters["label"]
+
 	if job.GetenvBool("all") && filt_tagged {
 		allImages, err = s.graph.Map()
 	} else {
@@ -68,6 +74,9 @@ func (s *TagStore) CmdImages(job *engine.Job) engine.Status {
 			} else {
 				// get the boolean list for if only the untagged images are requested
 				delete(allImages, id)
+				if !imageFilters.MatchKVList("label", image.ContainerConfig.Labels) {
+					continue
+				}
 				if filt_tagged {
 					out := &engine.Env{}
 					out.SetJson("ParentId", image.Parent)
@@ -76,6 +85,7 @@ func (s *TagStore) CmdImages(job *engine.Job) engine.Status {
 					out.SetInt64("Created", image.Created.Unix())
 					out.SetInt64("Size", image.Size)
 					out.SetInt64("VirtualSize", image.GetParentsSize(0)+image.Size)
+					out.SetJson("Labels", image.ContainerConfig.Labels)
 					lookup[id] = out
 				}
 			}
@@ -90,8 +100,11 @@ func (s *TagStore) CmdImages(job *engine.Job) engine.Status {
 	}
 
 	// Display images which aren't part of a repository/tag
-	if job.Getenv("filter") == "" {
+	if job.Getenv("filter") == "" || filt_label {
 		for _, image := range allImages {
+			if !imageFilters.MatchKVList("label", image.ContainerConfig.Labels) {
+				continue
+			}
 			out := &engine.Env{}
 			out.SetJson("ParentId", image.Parent)
 			out.SetList("RepoTags", []string{"<none>:<none>"})
@@ -99,6 +112,7 @@ func (s *TagStore) CmdImages(job *engine.Job) engine.Status {
 			out.SetInt64("Created", image.Created.Unix())
 			out.SetInt64("Size", image.Size)
 			out.SetInt64("VirtualSize", image.GetParentsSize(0)+image.Size)
+			out.SetJson("Labels", image.ContainerConfig.Labels)
 			outs.Add(out)
 		}
 	}

+ 22 - 0
integration-cli/docker_cli_build_test.go

@@ -4541,6 +4541,28 @@ func TestBuildWithTabs(t *testing.T) {
 	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) {
 	// This test just makes sure that no non-error output goes
 	// to stderr

+ 55 - 0
integration-cli/docker_cli_create_test.go

@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"os"
 	"os/exec"
+	"reflect"
 	"testing"
 	"time"
 
@@ -249,3 +250,57 @@ func TestCreateVolumesCreated(t *testing.T) {
 
 	logDone("create - volumes are created")
 }
+
+func TestCreateLabels(t *testing.T) {
+	name := "test_create_labels"
+	expected := map[string]string{"k1": "v1", "k2": "v2"}
+	if out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "create", "--name", name, "-l", "k1=v1", "--label", "k2=v2", "busybox")); err != nil {
+		t.Fatal(out, err)
+	}
+
+	actual := make(map[string]string)
+	err := inspectFieldAndMarshall(name, "Config.Labels", &actual)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Fatalf("Expected %s got %s", expected, actual)
+	}
+
+	deleteAllContainers()
+
+	logDone("create - labels")
+}
+
+func TestCreateLabelFromImage(t *testing.T) {
+	imageName := "testcreatebuildlabel"
+	defer deleteImages(imageName)
+	_, err := buildImage(imageName,
+		`FROM busybox
+		LABEL k1=v1 k2=v2`,
+		true)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	name := "test_create_labels_from_image"
+	expected := map[string]string{"k2": "x", "k3": "v3", "k1": "v1"}
+	if out, _, err := runCommandWithOutput(exec.Command(dockerBinary, "create", "--name", name, "-l", "k2=x", "--label", "k3=v3", imageName)); err != nil {
+		t.Fatal(out, err)
+	}
+
+	actual := make(map[string]string)
+	err = inspectFieldAndMarshall(name, "Config.Labels", &actual)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if !reflect.DeepEqual(expected, actual) {
+		t.Fatalf("Expected %s got %s", expected, actual)
+	}
+
+	deleteAllContainers()
+
+	logDone("create - labels from image")
+}

+ 54 - 0
integration-cli/docker_cli_images_test.go

@@ -77,6 +77,60 @@ func TestImagesErrorWithInvalidFilterNameTest(t *testing.T) {
 	logDone("images - invalid filter name check working")
 }
 
+func TestImagesFilterLabel(t *testing.T) {
+	imageName1 := "images_filter_test1"
+	imageName2 := "images_filter_test2"
+	imageName3 := "images_filter_test3"
+	defer deleteAllContainers()
+	defer deleteImages(imageName1)
+	defer deleteImages(imageName2)
+	defer deleteImages(imageName3)
+	image1ID, err := buildImage(imageName1,
+		`FROM scratch
+		 LABEL match me`, true)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	image2ID, err := buildImage(imageName2,
+		`FROM scratch
+		 LABEL match="me too"`, true)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	image3ID, err := buildImage(imageName3,
+		`FROM scratch
+		 LABEL nomatch me`, true)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	cmd := exec.Command(dockerBinary, "images", "--no-trunc", "-q", "-f", "label=match")
+	out, _, err := runCommandWithOutput(cmd)
+	if err != nil {
+		t.Fatal(out, err)
+	}
+	out = strings.TrimSpace(out)
+
+	if (!strings.Contains(out, image1ID) && !strings.Contains(out, image2ID)) || strings.Contains(out, image3ID) {
+		t.Fatalf("Expected ids %s,%s got %s", image1ID, image2ID, out)
+	}
+
+	cmd = exec.Command(dockerBinary, "images", "--no-trunc", "-q", "-f", "label=match=me too")
+	out, _, err = runCommandWithOutput(cmd)
+	if err != nil {
+		t.Fatal(out, err)
+	}
+	out = strings.TrimSpace(out)
+
+	if out != image2ID {
+		t.Fatalf("Expected %s got %s", image2ID, out)
+	}
+
+	logDone("images - filter label")
+}
+
 func TestImagesFilterWhiteSpaceTrimmingAndLowerCasingWorking(t *testing.T) {
 	imageName := "images_filter_test"
 	defer deleteAllContainers()

+ 68 - 0
integration-cli/docker_cli_ps_test.go

@@ -412,6 +412,74 @@ func TestPsListContainersFilterName(t *testing.T) {
 	logDone("ps - test ps filter name")
 }
 
+func TestPsListContainersFilterLabel(t *testing.T) {
+	// start container
+	runCmd := exec.Command(dockerBinary, "run", "-d", "-l", "match=me", "-l", "second=tag", "busybox")
+	out, _, err := runCommandWithOutput(runCmd)
+	if err != nil {
+		t.Fatal(out, err)
+	}
+	firstID := stripTrailingCharacters(out)
+
+	// start another container
+	runCmd = exec.Command(dockerBinary, "run", "-d", "-l", "match=me too", "busybox")
+	if out, _, err = runCommandWithOutput(runCmd); err != nil {
+		t.Fatal(out, err)
+	}
+	secondID := stripTrailingCharacters(out)
+
+	// start third container
+	runCmd = exec.Command(dockerBinary, "run", "-d", "-l", "nomatch=me", "busybox")
+	if out, _, err = runCommandWithOutput(runCmd); err != nil {
+		t.Fatal(out, err)
+	}
+	thirdID := stripTrailingCharacters(out)
+
+	// filter containers by exact match
+	runCmd = exec.Command(dockerBinary, "ps", "-a", "-q", "--no-trunc", "--filter=label=match=me")
+	if out, _, err = runCommandWithOutput(runCmd); err != nil {
+		t.Fatal(out, err)
+	}
+	containerOut := strings.TrimSpace(out)
+	if containerOut != firstID {
+		t.Fatalf("Expected id %s, got %s for exited filter, output: %q", firstID, containerOut, out)
+	}
+
+	// filter containers by two labels
+	runCmd = exec.Command(dockerBinary, "ps", "-a", "-q", "--no-trunc", "--filter=label=match=me", "--filter=label=second=tag")
+	if out, _, err = runCommandWithOutput(runCmd); err != nil {
+		t.Fatal(out, err)
+	}
+	containerOut = strings.TrimSpace(out)
+	if containerOut != firstID {
+		t.Fatalf("Expected id %s, got %s for exited filter, output: %q", firstID, containerOut, out)
+	}
+
+	// filter containers by two labels, but expect not found because of AND behavior
+	runCmd = exec.Command(dockerBinary, "ps", "-a", "-q", "--no-trunc", "--filter=label=match=me", "--filter=label=second=tag-no")
+	if out, _, err = runCommandWithOutput(runCmd); err != nil {
+		t.Fatal(out, err)
+	}
+	containerOut = strings.TrimSpace(out)
+	if containerOut != "" {
+		t.Fatalf("Expected nothing, got %s for exited filter, output: %q", containerOut, out)
+	}
+
+	// filter containers by exact key
+	runCmd = exec.Command(dockerBinary, "ps", "-a", "-q", "--no-trunc", "--filter=label=match")
+	if out, _, err = runCommandWithOutput(runCmd); err != nil {
+		t.Fatal(out, err)
+	}
+	containerOut = strings.TrimSpace(out)
+	if (!strings.Contains(containerOut, firstID) || !strings.Contains(containerOut, secondID)) || strings.Contains(containerOut, thirdID) {
+		t.Fatalf("Expected ids %s,%s, got %s for exited filter, output: %q", firstID, secondID, containerOut, out)
+	}
+
+	deleteAllContainers()
+
+	logDone("ps - test ps filter label")
+}
+
 func TestPsListContainersFilterExited(t *testing.T) {
 	defer deleteAllContainers()
 

+ 9 - 0
integration-cli/docker_utils.go

@@ -724,6 +724,15 @@ COPY . /static`); err != nil {
 		ctx:       ctx}, nil
 }
 
+func inspectFieldAndMarshall(name, field string, output interface{}) error {
+	str, err := inspectFieldJSON(name, field)
+	if err != nil {
+		return err
+	}
+
+	return json.Unmarshal([]byte(str), output)
+}
+
 func inspectFilter(name, filter string) (string, error) {
 	format := fmt.Sprintf("{{%s}}", filter)
 	inspectCmd := exec.Command(dockerBinary, "inspect", "-f", format, name)

+ 32 - 0
pkg/parsers/filters/parse.go

@@ -65,6 +65,38 @@ func FromParam(p string) (Args, error) {
 	return args, nil
 }
 
+func (filters Args) MatchKVList(field string, sources map[string]string) bool {
+	fieldValues := filters[field]
+
+	//do not filter if there is no filter set or cannot determine filter
+	if len(fieldValues) == 0 {
+		return true
+	}
+
+	if sources == nil || len(sources) == 0 {
+		return false
+	}
+
+outer:
+	for _, name2match := range fieldValues {
+		testKV := strings.SplitN(name2match, "=", 2)
+
+		for k, v := range sources {
+			if len(testKV) == 1 {
+				if k == testKV[0] {
+					continue outer
+				}
+			} else if k == testKV[0] && v == testKV[1] {
+				continue outer
+			}
+		}
+
+		return false
+	}
+
+	return true
+}
+
 func (filters Args) Match(field, source string) bool {
 	fieldValues := filters[field]
 

+ 5 - 0
runconfig/config.go

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

+ 45 - 9
runconfig/parse.go

@@ -31,6 +31,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe
 		flVolumes = opts.NewListOpts(opts.ValidatePath)
 		flLinks   = opts.NewListOpts(opts.ValidateLink)
 		flEnv     = opts.NewListOpts(opts.ValidateEnv)
+		flLabels  = opts.NewListOpts(opts.ValidateEnv)
 		flDevices = opts.NewListOpts(opts.ValidatePath)
 
 		ulimits   = make(map[string]*ulimit.Ulimit)
@@ -47,6 +48,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe
 		flCapAdd      = opts.NewListOpts(nil)
 		flCapDrop     = opts.NewListOpts(nil)
 		flSecurityOpt = opts.NewListOpts(nil)
+		flLabelsFile  = opts.NewListOpts(nil)
 
 		flNetwork         = cmd.Bool([]string{"#n", "#-networking"}, true, "Enable networking for this container")
 		flPrivileged      = cmd.Bool([]string{"#privileged", "-privileged"}, false, "Give extended privileges to this container")
@@ -74,6 +76,8 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe
 	cmd.Var(&flVolumes, []string{"v", "-volume"}, "Bind mount a volume")
 	cmd.Var(&flLinks, []string{"#link", "-link"}, "Add link to another container")
 	cmd.Var(&flDevices, []string{"-device"}, "Add a host device to the container")
+	cmd.Var(&flLabels, []string{"l", "-label"}, "Set meta data on a container")
+	cmd.Var(&flLabelsFile, []string{"-label-file"}, "Read in a line delimited file of labels")
 	cmd.Var(&flEnv, []string{"e", "-env"}, "Set environment variables")
 	cmd.Var(&flEnvFile, []string{"-env-file"}, "Read in a file of environment variables")
 	cmd.Var(&flPublish, []string{"p", "-publish"}, "Publish a container's port(s) to the host")
@@ -243,16 +247,16 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe
 	}
 
 	// collect all the environment variables for the container
-	envVariables := []string{}
-	for _, ef := range flEnvFile.GetAll() {
-		parsedVars, err := opts.ParseEnvFile(ef)
-		if err != nil {
-			return nil, nil, cmd, err
-		}
-		envVariables = append(envVariables, parsedVars...)
+	envVariables, err := readKVStrings(flEnvFile.GetAll(), flEnv.GetAll())
+	if err != nil {
+		return nil, nil, cmd, err
+	}
+
+	// collect all the labels for the container
+	labels, err := readKVStrings(flLabelsFile.GetAll(), flLabels.GetAll())
+	if err != nil {
+		return nil, nil, cmd, err
 	}
-	// parse the '-e' and '--env' after, to allow override
-	envVariables = append(envVariables, flEnv.GetAll()...)
 
 	ipcMode := IpcMode(*flIpcMode)
 	if !ipcMode.Valid() {
@@ -297,6 +301,7 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe
 		MacAddress:      *flMacAddress,
 		Entrypoint:      entrypoint,
 		WorkingDir:      *flWorkingDir,
+		Labels:          convertKVStringsToMap(labels),
 	}
 
 	hostConfig := &HostConfig{
@@ -334,6 +339,37 @@ func Parse(cmd *flag.FlagSet, args []string) (*Config, *HostConfig, *flag.FlagSe
 	return config, hostConfig, cmd, nil
 }
 
+// reads a file of line terminated key=value pairs and override that with override parameter
+func readKVStrings(files []string, override []string) ([]string, error) {
+	envVariables := []string{}
+	for _, ef := range files {
+		parsedVars, err := opts.ParseEnvFile(ef)
+		if err != nil {
+			return nil, err
+		}
+		envVariables = append(envVariables, parsedVars...)
+	}
+	// parse the '-e' and '--env' after, to allow override
+	envVariables = append(envVariables, override...)
+
+	return envVariables, nil
+}
+
+// converts ["key=value"] to {"key":"value"}
+func convertKVStringsToMap(values []string) map[string]string {
+	result := make(map[string]string, len(values))
+	for _, value := range values {
+		kv := strings.SplitN(value, "=", 2)
+		if len(kv) == 1 {
+			result[kv[0]] = ""
+		} else {
+			result[kv[0]] = kv[1]
+		}
+	}
+
+	return result
+}
+
 // parseRestartPolicy returns the parsed policy or an error indicating what is incorrect
 func parseRestartPolicy(policy string) (RestartPolicy, error) {
 	p := RestartPolicy{}