diff --git a/builder/command/command.go b/builder/command/command.go index f99fa2d906..dd24ee44c5 100644 --- a/builder/command/command.go +++ b/builder/command/command.go @@ -3,6 +3,7 @@ package command const ( Env = "env" + Label = "label" Maintainer = "maintainer" Add = "add" Copy = "copy" diff --git a/builder/dispatchers.go b/builder/dispatchers.go index 52757281f3..965fd68c03 100644 --- a/builder/dispatchers.go +++ b/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 diff --git a/builder/evaluator.go b/builder/evaluator.go index 1dc2a8873f..aee60426c1 100644 --- a/builder/evaluator.go +++ b/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 diff --git a/builder/parser/line_parsers.go b/builder/parser/line_parsers.go index c7fed13dbe..06115e831a 100644 --- a/builder/parser/line_parsers.go +++ b/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) { diff --git a/builder/parser/parser.go b/builder/parser/parser.go index 69bbfd0dc1..1ab151b30d 100644 --- a/builder/parser/parser.go +++ b/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, diff --git a/contrib/syntax/kate/Dockerfile.xml b/contrib/syntax/kate/Dockerfile.xml index e5602397ba..4fdef2393b 100644 --- a/contrib/syntax/kate/Dockerfile.xml +++ b/contrib/syntax/kate/Dockerfile.xml @@ -22,6 +22,7 @@ CMD WORKDIR USER + LABEL diff --git a/contrib/syntax/textmate/Docker.tmbundle/Syntaxes/Dockerfile.tmLanguage b/contrib/syntax/textmate/Docker.tmbundle/Syntaxes/Dockerfile.tmLanguage index 1d19a3ba2e..75efc2e811 100644 --- a/contrib/syntax/textmate/Docker.tmbundle/Syntaxes/Dockerfile.tmLanguage +++ b/contrib/syntax/textmate/Docker.tmbundle/Syntaxes/Dockerfile.tmLanguage @@ -12,7 +12,7 @@ match - ^\s*(ONBUILD\s+)?(FROM|MAINTAINER|RUN|EXPOSE|ENV|ADD|VOLUME|USER|WORKDIR|COPY)\s + ^\s*(ONBUILD\s+)?(FROM|MAINTAINER|RUN|EXPOSE|ENV|ADD|VOLUME|USER|LABEL|WORKDIR|COPY)\s captures 0 diff --git a/contrib/syntax/vim/syntax/dockerfile.vim b/contrib/syntax/vim/syntax/dockerfile.vim index 2984bec5f8..36691e2504 100644 --- a/contrib/syntax/vim/syntax/dockerfile.vim +++ b/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"/ diff --git a/daemon/list.go b/daemon/list.go index 5885ebd499..174ec7ec75 100644 --- a/daemon/list.go +++ b/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 } diff --git a/docs/man/Dockerfile.5.md b/docs/man/Dockerfile.5.md index 51e1390ceb..d460c30c21 100644 --- a/docs/man/Dockerfile.5.md +++ b/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 [=] [[=] ...]` + 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 [...]` The **EXPOSE** instruction informs Docker that the container listens on the diff --git a/docs/man/docker-create.1.md b/docs/man/docker-create.1.md index 9faeabe302..91dcb8df30 100644 --- a/docs/man/docker-create.1.md +++ b/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:': 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 :alias diff --git a/docs/man/docker-images.1.md b/docs/man/docker-images.1.md index 16fad991ce..c82d2883f7 100644 --- a/docs/man/docker-images.1.md +++ b/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 diff --git a/docs/man/docker-inspect.1.md b/docs/man/docker-inspect.1.md index 23ec6bedef..85f6730004 100644 --- a/docs/man/docker-inspect.1.md +++ b/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, diff --git a/docs/man/docker-ps.1.md b/docs/man/docker-ps.1.md index bd1e04d813..783f4621f6 100644 --- a/docs/man/docker-ps.1.md +++ b/docs/man/docker-ps.1.md @@ -36,6 +36,7 @@ the running containers. **-f**, **--filter**=[] Provide filter values. Valid filters: exited= - containers with exit code of + label= or label== status=(restarting|running|paused|exited) name= - container's name id= - container's ID diff --git a/docs/man/docker-run.1.md b/docs/man/docker-run.1.md index 74450fd143..3cb9c8717b 100644 --- a/docs/man/docker-run.1.md +++ b/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:': 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 :alias diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 95cbeedd87..88447ad923 100644 --- a/docs/mkdocs.yml +++ b/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**' ] diff --git a/docs/sources/reference/api/docker_remote_api.md b/docs/sources/reference/api/docker_remote_api.md index 051b90ce97..10c8c62aaa 100644 --- a/docs/sources/reference/api/docker_remote_api.md +++ b/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 diff --git a/docs/sources/reference/api/docker_remote_api_v1.18.md b/docs/sources/reference/api/docker_remote_api_v1.18.md index 3543c7e7a0..dc5d666248 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.18.md +++ b/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": "" diff --git a/docs/sources/reference/builder.md b/docs/sources/reference/builder.md index 3d34db38cc..9a03c516ee 100644 --- a/docs/sources/reference/builder.md +++ b/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 = = = ... + +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 [...] @@ -907,6 +928,7 @@ For example you might add something like this: FROM ubuntu MAINTAINER Victor Vieux + 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 diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index 6b2d7c1bdd..4ce50a7bae 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/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=` or `label==`) ##### 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 diff --git a/docs/sources/userguide/labels-custom-metadata.md b/docs/sources/userguide/labels-custom-metadata.md new file mode 100644 index 0000000000..dc5c2407aa --- /dev/null +++ b/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 `` / `` pair. Docker stores the values as *strings*. +You can specify multiple labels but each `` / `` 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 [.][=] ... + +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 diff --git a/graph/list.go b/graph/list.go index 49d4072be5..9551edaae2 100644 --- a/graph/list.go +++ b/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{":"}) @@ -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) } } diff --git a/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go index 45eae13bba..1c3c070cd4 100644 --- a/integration-cli/docker_cli_build_test.go +++ b/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 diff --git a/integration-cli/docker_cli_create_test.go b/integration-cli/docker_cli_create_test.go index fe402caa4a..e32400e603 100644 --- a/integration-cli/docker_cli_create_test.go +++ b/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") +} diff --git a/integration-cli/docker_cli_images_test.go b/integration-cli/docker_cli_images_test.go index bb24e9a347..2b22aa25e3 100644 --- a/integration-cli/docker_cli_images_test.go +++ b/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() diff --git a/integration-cli/docker_cli_ps_test.go b/integration-cli/docker_cli_ps_test.go index d5f9d00dcb..c2e108576d 100644 --- a/integration-cli/docker_cli_ps_test.go +++ b/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() diff --git a/integration-cli/docker_utils.go b/integration-cli/docker_utils.go index 9466d13256..1bc9fb5af5 100644 --- a/integration-cli/docker_utils.go +++ b/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) diff --git a/pkg/parsers/filters/parse.go b/pkg/parsers/filters/parse.go index 8b045a3098..9c056bb3cf 100644 --- a/pkg/parsers/filters/parse.go +++ b/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] diff --git a/runconfig/config.go b/runconfig/config.go index ccc9ee60e2..3e32a1e346 100644 --- a/runconfig/config.go +++ b/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 } diff --git a/runconfig/merge.go b/runconfig/merge.go index 9bc4748446..9bbdc6ad25 100644 --- a/runconfig/merge.go +++ b/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 diff --git a/runconfig/parse.go b/runconfig/parse.go index bb3f013035..5f259f78d1 100644 --- a/runconfig/parse.go +++ b/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{}