diff --git a/api/client/commands.go b/api/client/commands.go index 6080595849..4f0915ba79 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -26,12 +26,14 @@ import ( "github.com/dotcloud/docker/dockerversion" "github.com/dotcloud/docker/engine" "github.com/dotcloud/docker/nat" + "github.com/dotcloud/docker/opts" "github.com/dotcloud/docker/pkg/signal" "github.com/dotcloud/docker/pkg/term" "github.com/dotcloud/docker/pkg/units" "github.com/dotcloud/docker/registry" "github.com/dotcloud/docker/runconfig" "github.com/dotcloud/docker/utils" + "github.com/dotcloud/docker/utils/filters" ) func (cli *DockerCli) CmdHelp(args ...string) error { @@ -1145,6 +1147,9 @@ func (cli *DockerCli) CmdImages(args ...string) error { flViz := cmd.Bool([]string{"#v", "#viz", "#-viz"}, false, "Output graph in graphviz format") flTree := cmd.Bool([]string{"#t", "#tree", "#-tree"}, false, "Output graph in tree format") + var flFilter opts.ListOpts + cmd.Var(&flFilter, []string{"f", "-filter"}, "Provide filter values (i.e. 'dangling=true')") + if err := cmd.Parse(args); err != nil { return nil } @@ -1153,11 +1158,32 @@ func (cli *DockerCli) CmdImages(args ...string) error { return nil } - filter := cmd.Arg(0) + // Consolidate all filter flags, and sanity check them early. + // They'll get process in the daemon/server. + imageFilterArgs := filters.Args{} + for _, f := range flFilter.GetAll() { + var err error + imageFilterArgs, err = filters.ParseFlag(f, imageFilterArgs) + if err != nil { + return err + } + } + matchName := cmd.Arg(0) // FIXME: --viz and --tree are deprecated. Remove them in a future version. if *flViz || *flTree { - body, _, err := readBody(cli.call("GET", "/images/json?all=1", nil, false)) + v := url.Values{ + "all": []string{"1"}, + } + if len(imageFilterArgs) > 0 { + filterJson, err := filters.ToParam(imageFilterArgs) + if err != nil { + return err + } + v.Set("filters", filterJson) + } + + body, _, err := readBody(cli.call("GET", "/images/json?"+v.Encode(), nil, false)) if err != nil { return err } @@ -1187,13 +1213,13 @@ func (cli *DockerCli) CmdImages(args ...string) error { } } - if filter != "" { - if filter == image.Get("Id") || filter == utils.TruncateID(image.Get("Id")) { + if matchName != "" { + if matchName == image.Get("Id") || matchName == utils.TruncateID(image.Get("Id")) { startImage = image } for _, repotag := range image.GetList("RepoTags") { - if repotag == filter { + if repotag == matchName { startImage = image } } @@ -1211,7 +1237,7 @@ func (cli *DockerCli) CmdImages(args ...string) error { root := engine.NewTable("Created", 1) root.Add(startImage) cli.WalkTree(*noTrunc, root, byParent, "", printNode) - } else if filter == "" { + } else if matchName == "" { cli.WalkTree(*noTrunc, roots, byParent, "", printNode) } if *flViz { @@ -1219,8 +1245,17 @@ func (cli *DockerCli) CmdImages(args ...string) error { } } else { v := url.Values{} + if len(imageFilterArgs) > 0 { + filterJson, err := filters.ToParam(imageFilterArgs) + if err != nil { + return err + } + v.Set("filters", filterJson) + } + if cmd.NArg() == 1 { - v.Set("filter", filter) + // FIXME rename this parameter, to not be confused with the filters flag + v.Set("filter", matchName) } if *all { v.Set("all", "1") diff --git a/api/server/server.go b/api/server/server.go index 25b377ffdb..43b2c62323 100644 --- a/api/server/server.go +++ b/api/server/server.go @@ -188,6 +188,8 @@ func getImagesJSON(eng *engine.Engine, version version.Version, w http.ResponseW job = eng.Job("images") ) + job.Setenv("filters", r.Form.Get("filters")) + // FIXME this parameter could just be a match filter job.Setenv("filter", r.Form.Get("filter")) job.Setenv("all", r.Form.Get("all")) diff --git a/docs/sources/reference/api/docker_remote_api_v1.12.md b/docs/sources/reference/api/docker_remote_api_v1.12.md index 9cc84f4ffc..23afa36bcf 100644 --- a/docs/sources/reference/api/docker_remote_api_v1.12.md +++ b/docs/sources/reference/api/docker_remote_api_v1.12.md @@ -712,6 +712,16 @@ Copy files or folders of container `id` } ] + + Query Parameters: + +   + + - **all** – 1/True/true or 0/False/false, default false + - **filters** – a json encoded value of the filters (a map[string][]string) to process on the images list. + + + ### Create an image `POST /images/create` diff --git a/docs/sources/reference/commandline/cli.md b/docs/sources/reference/commandline/cli.md index f2e370ace5..829f13b9a6 100644 --- a/docs/sources/reference/commandline/cli.md +++ b/docs/sources/reference/commandline/cli.md @@ -440,6 +440,7 @@ To see how the `docker:latest` image was built: List images -a, --all=false Show all images (by default filter out the intermediate image layers) + -f, --filter=[]: Provide filter values (i.e. 'dangling=true') --no-trunc=false Don't truncate output -q, --quiet=false Only show numeric IDs @@ -479,6 +480,46 @@ by default. tryout latest 2629d1fa0b81b222fca63371ca16cbf6a0772d07759ff80e8d1369b926940074 23 hours ago 131.5 MB 5ed6274db6ceb2397844896966ea239290555e74ef307030ebb01ff91b1914df 24 hours ago 1.089 GB +### Filtering + +The filtering flag (-f or --filter) format is of "key=value". If there are more +than one filter, then pass multiple flags (e.g. `--filter "foo=bar" --filter "bif=baz"`) + +Current filters: + * dangling (boolean - true or false) + +#### untagged images + + $ sudo docker images --filter "dangling=true" + + REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE + 8abc22fbb042 4 weeks ago 0 B + 48e5f45168b9 4 weeks ago 2.489 MB + bf747efa0e2f 4 weeks ago 0 B + 980fe10e5736 12 weeks ago 101.4 MB + dea752e4e117 12 weeks ago 101.4 MB + 511136ea3c5a 8 months ago 0 B + +This will display untagged images, that are the leaves of the images tree (not +intermediary layers). These images occur when a new build of an image takes the +repo:tag away from the IMAGE ID, leaving it untagged. A warning will be issued +if trying to remove an image when a container is presently using it. +By having this flag it allows for batch cleanup. + +Ready for use by `docker rmi ...`, like: + + $ sudo docker rmi $(sudo docker images -f "dangling=true" -q) + + 8abc22fbb042 + 48e5f45168b9 + bf747efa0e2f + 980fe10e5736 + dea752e4e117 + 511136ea3c5a + +NOTE: Docker will warn you if any containers exist that are using these untagged images. + + ## import Usage: docker import URL|- [REPOSITORY[:TAG]] diff --git a/server/server.go b/server/server.go index 3239893e0e..507f32688c 100644 --- a/server/server.go +++ b/server/server.go @@ -55,6 +55,7 @@ import ( "github.com/dotcloud/docker/registry" "github.com/dotcloud/docker/runconfig" "github.com/dotcloud/docker/utils" + "github.com/dotcloud/docker/utils/filters" ) func (srv *Server) handlerWrap(h engine.Handler) engine.Handler { @@ -694,10 +695,24 @@ func (srv *Server) ImagesViz(job *engine.Job) engine.Status { func (srv *Server) Images(job *engine.Job) engine.Status { var ( - allImages map[string]*image.Image - err error + allImages map[string]*image.Image + err error + filt_tagged = true ) - if job.GetenvBool("all") { + + imageFilters, err := filters.FromParam(job.Getenv("filters")) + if err != nil { + return job.Error(err) + } + if i, ok := imageFilters["dangling"]; ok { + for _, value := range i { + if strings.ToLower(value) == "true" { + filt_tagged = false + } + } + } + + if job.GetenvBool("all") && filt_tagged { allImages, err = srv.daemon.Graph().Map() } else { allImages, err = srv.daemon.Graph().Heads() @@ -721,17 +736,22 @@ func (srv *Server) Images(job *engine.Job) engine.Status { } if out, exists := lookup[id]; exists { - out.SetList("RepoTags", append(out.GetList("RepoTags"), fmt.Sprintf("%s:%s", name, tag))) + if filt_tagged { + out.SetList("RepoTags", append(out.GetList("RepoTags"), fmt.Sprintf("%s:%s", name, tag))) + } } else { - out := &engine.Env{} + // get the boolean list for if only the untagged images are requested delete(allImages, id) - out.Set("ParentId", image.Parent) - out.SetList("RepoTags", []string{fmt.Sprintf("%s:%s", name, tag)}) - out.Set("Id", image.ID) - out.SetInt64("Created", image.Created.Unix()) - out.SetInt64("Size", image.Size) - out.SetInt64("VirtualSize", image.GetParentsSize(0)+image.Size) - lookup[id] = out + if filt_tagged { + out := &engine.Env{} + out.Set("ParentId", image.Parent) + out.SetList("RepoTags", []string{fmt.Sprintf("%s:%s", name, tag)}) + out.Set("Id", image.ID) + out.SetInt64("Created", image.Created.Unix()) + out.SetInt64("Size", image.Size) + out.SetInt64("VirtualSize", image.GetParentsSize(0)+image.Size) + lookup[id] = out + } } } diff --git a/utils/filters/parse.go b/utils/filters/parse.go new file mode 100644 index 0000000000..27c7132e8e --- /dev/null +++ b/utils/filters/parse.go @@ -0,0 +1,63 @@ +package filters + +import ( + "encoding/json" + "errors" + "strings" +) + +type Args map[string][]string + +// Parse the argument to the filter flag. Like +// +// `docker ps -f 'created=today' -f 'image.name=ubuntu*'` +// +// If prev map is provided, then it is appended to, and returned. By default a new +// map is created. +func ParseFlag(arg string, prev Args) (Args, error) { + var filters Args = prev + if prev == nil { + filters = Args{} + } + if len(arg) == 0 { + return filters, nil + } + + if !strings.Contains(arg, "=") { + return filters, ErrorBadFormat + } + + f := strings.SplitN(arg, "=", 2) + filters[f[0]] = append(filters[f[0]], f[1]) + + return filters, nil +} + +var ErrorBadFormat = errors.New("bad format of filter (expected name=value)") + +// packs the Args into an string for easy transport from client to server +func ToParam(a Args) (string, error) { + // this way we don't URL encode {}, just empty space + if len(a) == 0 { + return "", nil + } + + buf, err := json.Marshal(a) + if err != nil { + return "", err + } + return string(buf), nil +} + +// unpacks the filter Args +func FromParam(p string) (Args, error) { + args := Args{} + if len(p) == 0 { + return args, nil + } + err := json.Unmarshal([]byte(p), &args) + if err != nil { + return nil, err + } + return args, nil +} diff --git a/utils/filters/parse_test.go b/utils/filters/parse_test.go new file mode 100644 index 0000000000..a248350223 --- /dev/null +++ b/utils/filters/parse_test.go @@ -0,0 +1,78 @@ +package filters + +import ( + "sort" + "testing" +) + +func TestParseArgs(t *testing.T) { + // equivalent of `docker ps -f 'created=today' -f 'image.name=ubuntu*' -f 'image.name=*untu'` + flagArgs := []string{ + "created=today", + "image.name=ubuntu*", + "image.name=*untu", + } + var ( + args = Args{} + err error + ) + for i := range flagArgs { + args, err = ParseFlag(flagArgs[i], args) + if err != nil { + t.Errorf("failed to parse %s: %s", flagArgs[i], err) + } + } + if len(args["created"]) != 1 { + t.Errorf("failed to set this arg") + } + if len(args["image.name"]) != 2 { + t.Errorf("the args should have collapsed") + } +} + +func TestParam(t *testing.T) { + a := Args{ + "created": []string{"today"}, + "image.name": []string{"ubuntu*", "*untu"}, + } + + v, err := ToParam(a) + if err != nil { + t.Errorf("failed to marshal the filters: %s", err) + } + v1, err := FromParam(v) + if err != nil { + t.Errorf("%s", err) + } + for key, vals := range v1 { + if _, ok := a[key]; !ok { + t.Errorf("could not find key %s in original set", key) + } + sort.Strings(vals) + sort.Strings(a[key]) + if len(vals) != len(a[key]) { + t.Errorf("value lengths ought to match") + continue + } + for i := range vals { + if vals[i] != a[key][i] { + t.Errorf("expected %s, but got %s", a[key][i], vals[i]) + } + } + } +} + +func TestEmpty(t *testing.T) { + a := Args{} + v, err := ToParam(a) + if err != nil { + t.Errorf("failed to marshal the filters: %s", err) + } + v1, err := FromParam(v) + if err != nil { + t.Errorf("%s", err) + } + if len(a) != len(v1) { + t.Errorf("these should both be empty sets") + } +}