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/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_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..d6d8d2ab5c 100644 --- a/integration-cli/docker_cli_ps_test.go +++ b/integration-cli/docker_cli_ps_test.go @@ -412,6 +412,54 @@ 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", "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 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/parse.go b/runconfig/parse.go index bf87c42f94..cc638efc1b 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, for example com.example.key=value") + 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{ @@ -330,6 +335,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{}