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 eadef4a1e0..389fcc25e8 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/docs/sources/reference/api/docker_remote_api.md b/docs/sources/reference/api/docker_remote_api.md
index 051b90ce97..0919d511b7 100644
--- a/docs/sources/reference/api/docker_remote_api.md
+++ b/docs/sources/reference/api/docker_remote_api.md
@@ -71,6 +71,13 @@ This endpoint now returns `SystemTime`, `HttpProxy`,`HttpsProxy` and `NoProxy`.
### 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`
**New!**
diff --git a/docs/sources/reference/api/docker_remote_api_v1.17.md b/docs/sources/reference/api/docker_remote_api_v1.17.md
index 96887559c2..f4e16b29dc 100644
--- a/docs/sources/reference/api/docker_remote_api_v1.17.md
+++ b/docs/sources/reference/api/docker_remote_api_v1.17.md
@@ -129,6 +129,11 @@ Create a container
],
"Entrypoint": "",
"Image": "ubuntu",
+ "Labels": {
+ "Vendor": "Acme",
+ "License": "GPL",
+ "Version": "1.0"
+ },
"Volumes": {
"/tmp": {}
},
@@ -1169,6 +1174,7 @@ Return low-level information on the image `name`
"Cmd": ["/bin/bash"],
"Dns": null,
"Image": "ubuntu",
+ "Labels": null,
"Volumes": null,
"VolumesFrom": "",
"WorkingDir": ""
diff --git a/docs/sources/reference/builder.md b/docs/sources/reference/builder.md
index 3d34db38cc..00c70e6fa1 100644
--- a/docs/sources/reference/builder.md
+++ b/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 intended command for the image.
+## LABEL
+ LABEL = = = ...
+
+ --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 [...]
@@ -907,6 +918,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/integration-cli/docker_cli_build_test.go b/integration-cli/docker_cli_build_test.go
index ecef76f9da..c7749be233 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/runconfig/config.go b/runconfig/config.go
index ca5c3240b6..bcd3c46ec5 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