From a3c8b3474ef94175d072e30590eebe9d4b057237 Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Mon, 4 May 2015 22:09:40 -0400 Subject: [PATCH] Adding --filter flag to ls command Initially supporting `swarm=`, `state=`, and `driver=` filters. Signed-off-by: Dave Henderson --- commands/commands.go | 5 + commands/ls.go | 115 ++++++++++++++++++- commands/ls_test.go | 231 +++++++++++++++++++++++++++++++++++++++ docs/index.md | 36 +++++- test/integration/ls.bats | 27 +++++ 5 files changed, 408 insertions(+), 6 deletions(-) create mode 100644 test/integration/ls.bats diff --git a/commands/commands.go b/commands/commands.go index 5aedc13f22..9bb76615ce 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -293,6 +293,11 @@ var Commands = []cli.Command{ Name: "quiet, q", Usage: "Enable quiet mode", }, + cli.StringSliceFlag{ + Name: "filter", + Usage: "Filter output based on conditions provided", + Value: &cli.StringSlice{}, + }, }, Name: "ls", Usage: "List machines", diff --git a/commands/ls.go b/commands/ls.go index 31f546802d..8dec449ab5 100644 --- a/commands/ls.go +++ b/commands/ls.go @@ -2,16 +2,28 @@ package commands import ( "fmt" - "log" "os" + "strings" "text/tabwriter" "github.com/codegangsta/cli" "github.com/docker/machine/libmachine" + "github.com/docker/machine/log" ) +// FilterOptions - +type FilterOptions struct { + SwarmName []string + DriverName []string + State []string +} + func cmdLs(c *cli.Context) { quiet := c.Bool("quiet") + filters, err := parseFilters(c.StringSlice("filter")) + if err != nil { + log.Fatal(err) + } mcn := getDefaultMcn(c) hostList, err := mcn.List() @@ -19,6 +31,8 @@ func cmdLs(c *cli.Context) { log.Fatal(err) } + hostList = filterHosts(hostList, filters) + // Just print out the names if we're being quiet if quiet { for _, host := range hostList { @@ -68,3 +82,102 @@ func cmdLs(c *cli.Context) { w.Flush() } + +func parseFilters(filters []string) (FilterOptions, error) { + options := FilterOptions{} + for _, f := range filters { + kv := strings.SplitN(f, "=", 2) + key, value := kv[0], kv[1] + + switch key { + case "swarm": + options.SwarmName = append(options.SwarmName, value) + case "driver": + options.DriverName = append(options.DriverName, value) + case "state": + options.State = append(options.State, value) + default: + return options, fmt.Errorf("Unsupported filter key '%s'", key) + } + } + return options, nil +} + +func filterHosts(hosts []*libmachine.Host, filters FilterOptions) []*libmachine.Host { + if len(filters.SwarmName) == 0 && + len(filters.DriverName) == 0 && + len(filters.State) == 0 { + return hosts + } + + filteredHosts := []*libmachine.Host{} + swarmMasters := getSwarmMasters(hosts) + + for _, h := range hosts { + if filterHost(h, filters, swarmMasters) { + filteredHosts = append(filteredHosts, h) + } + } + return filteredHosts +} + +func getSwarmMasters(hosts []*libmachine.Host) map[string]string { + swarmMasters := make(map[string]string) + for _, h := range hosts { + swarmOptions := h.HostOptions.SwarmOptions + if swarmOptions != nil && swarmOptions.Master { + swarmMasters[swarmOptions.Discovery] = h.Name + } + } + return swarmMasters +} + +func filterHost(host *libmachine.Host, filters FilterOptions, swarmMasters map[string]string) bool { + swarmMatches := matchesSwarmName(host, filters.SwarmName, swarmMasters) + driverMatches := matchesDriverName(host, filters.DriverName) + stateMatches := matchesState(host, filters.State) + + return swarmMatches && driverMatches && stateMatches +} + +func matchesSwarmName(host *libmachine.Host, swarmNames []string, swarmMasters map[string]string) bool { + if len(swarmNames) == 0 { + return true + } + for _, n := range swarmNames { + if host.HostOptions.SwarmOptions != nil { + if n == swarmMasters[host.HostOptions.SwarmOptions.Discovery] { + return true + } + } + } + return false +} + +func matchesDriverName(host *libmachine.Host, driverNames []string) bool { + if len(driverNames) == 0 { + return true + } + for _, n := range driverNames { + if host.DriverName == n { + return true + } + } + return false +} + +func matchesState(host *libmachine.Host, states []string) bool { + if len(states) == 0 { + return true + } + for _, n := range states { + s, err := host.Driver.GetState() + if err != nil { + log.Warn(err) + } + if n == s.String() { + return true + } + } + return false +} diff --git a/commands/ls_test.go b/commands/ls_test.go index cdff10da75..471fa15cd8 100644 --- a/commands/ls_test.go +++ b/commands/ls_test.go @@ -1 +1,232 @@ package commands + +import ( + "testing" + + "github.com/docker/machine/drivers/fakedriver" + "github.com/docker/machine/libmachine" + "github.com/docker/machine/libmachine/swarm" + "github.com/docker/machine/state" + "github.com/stretchr/testify/assert" +) + +func TestParseFiltersErrorsGivenInvalidFilter(t *testing.T) { + _, err := parseFilters([]string{"foo=bar"}) + assert.EqualError(t, err, "Unsupported filter key 'foo'") +} + +func TestParseFiltersSwarm(t *testing.T) { + actual, _ := parseFilters([]string{"swarm=foo"}) + assert.Equal(t, actual, FilterOptions{SwarmName: []string{"foo"}}) +} + +func TestParseFiltersDriver(t *testing.T) { + actual, _ := parseFilters([]string{"driver=bar"}) + assert.Equal(t, actual, FilterOptions{DriverName: []string{"bar"}}) +} + +func TestParseFiltersState(t *testing.T) { + actual, _ := parseFilters([]string{"state=Running"}) + assert.Equal(t, actual, FilterOptions{State: []string{"Running"}}) +} + +func TestParseFiltersAll(t *testing.T) { + actual, _ := parseFilters([]string{"swarm=foo", "driver=bar", "state=Stopped"}) + assert.Equal(t, actual, FilterOptions{SwarmName: []string{"foo"}, DriverName: []string{"bar"}, State: []string{"Stopped"}}) +} + +func TestParseFiltersDuplicates(t *testing.T) { + actual, _ := parseFilters([]string{"swarm=foo", "driver=bar", "swarm=baz", "driver=qux", "state=Running", "state=Starting"}) + assert.Equal(t, actual, FilterOptions{SwarmName: []string{"foo", "baz"}, DriverName: []string{"bar", "qux"}, State: []string{"Running", "Starting"}}) +} + +func TestParseFiltersValueWithEqual(t *testing.T) { + actual, _ := parseFilters([]string{"driver=bar=baz"}) + assert.Equal(t, actual, FilterOptions{DriverName: []string{"bar=baz"}}) +} + +func TestFilterHostsReturnsSameGivenNoFilters(t *testing.T) { + opts := FilterOptions{} + hosts := []*libmachine.Host{ + { + Name: "testhost", + DriverName: "fakedriver", + HostOptions: &libmachine.HostOptions{}, + }, + } + actual := filterHosts(hosts, opts) + assert.EqualValues(t, actual, hosts) +} + +func TestFilterHostsReturnsEmptyGivenEmptyHosts(t *testing.T) { + opts := FilterOptions{ + SwarmName: []string{"foo"}, + } + hosts := []*libmachine.Host{} + assert.Empty(t, filterHosts(hosts, opts)) +} + +func TestFilterHostsReturnsEmptyGivenNonMatchingFilters(t *testing.T) { + opts := FilterOptions{ + SwarmName: []string{"foo"}, + } + hosts := []*libmachine.Host{ + { + Name: "testhost", + DriverName: "fakedriver", + HostOptions: &libmachine.HostOptions{}, + }, + } + assert.Empty(t, filterHosts(hosts, opts)) +} + +func TestFilterHostsBySwarmName(t *testing.T) { + opts := FilterOptions{ + SwarmName: []string{"master"}, + } + master := + &libmachine.Host{ + Name: "master", + HostOptions: &libmachine.HostOptions{ + SwarmOptions: &swarm.SwarmOptions{Master: true, Discovery: "foo"}, + }, + } + node1 := + &libmachine.Host{ + Name: "node1", + HostOptions: &libmachine.HostOptions{ + SwarmOptions: &swarm.SwarmOptions{Master: false, Discovery: "foo"}, + }, + } + othermaster := + &libmachine.Host{ + Name: "othermaster", + HostOptions: &libmachine.HostOptions{ + SwarmOptions: &swarm.SwarmOptions{Master: true, Discovery: "bar"}, + }, + } + hosts := []*libmachine.Host{master, node1, othermaster} + expected := []*libmachine.Host{master, node1} + + assert.EqualValues(t, filterHosts(hosts, opts), expected) +} + +func TestFilterHostsByDriverName(t *testing.T) { + opts := FilterOptions{ + DriverName: []string{"fakedriver"}, + } + node1 := + &libmachine.Host{ + Name: "node1", + DriverName: "fakedriver", + HostOptions: &libmachine.HostOptions{}, + } + node2 := + &libmachine.Host{ + Name: "node2", + DriverName: "virtualbox", + HostOptions: &libmachine.HostOptions{}, + } + node3 := + &libmachine.Host{ + Name: "node3", + DriverName: "fakedriver", + HostOptions: &libmachine.HostOptions{}, + } + hosts := []*libmachine.Host{node1, node2, node3} + expected := []*libmachine.Host{node1, node3} + + assert.EqualValues(t, filterHosts(hosts, opts), expected) +} + +func TestFilterHostsByState(t *testing.T) { + opts := FilterOptions{ + State: []string{"Paused", "Saved", "Stopped"}, + } + node1 := + &libmachine.Host{ + Name: "node1", + DriverName: "fakedriver", + HostOptions: &libmachine.HostOptions{}, + Driver: &fakedriver.FakeDriver{MockState: state.Paused}, + } + node2 := + &libmachine.Host{ + Name: "node2", + DriverName: "virtualbox", + HostOptions: &libmachine.HostOptions{}, + Driver: &fakedriver.FakeDriver{MockState: state.Stopped}, + } + node3 := + &libmachine.Host{ + Name: "node3", + DriverName: "fakedriver", + HostOptions: &libmachine.HostOptions{}, + Driver: &fakedriver.FakeDriver{MockState: state.Running}, + } + hosts := []*libmachine.Host{node1, node2, node3} + expected := []*libmachine.Host{node1, node2} + + assert.EqualValues(t, filterHosts(hosts, opts), expected) +} + +func TestFilterHostsMultiFlags(t *testing.T) { + opts := FilterOptions{ + SwarmName: []string{}, + DriverName: []string{"fakedriver", "virtualbox"}, + } + node1 := + &libmachine.Host{ + Name: "node1", + DriverName: "fakedriver", + HostOptions: &libmachine.HostOptions{}, + } + node2 := + &libmachine.Host{ + Name: "node2", + DriverName: "virtualbox", + HostOptions: &libmachine.HostOptions{}, + } + node3 := + &libmachine.Host{ + Name: "node3", + DriverName: "softlayer", + HostOptions: &libmachine.HostOptions{}, + } + hosts := []*libmachine.Host{node1, node2, node3} + expected := []*libmachine.Host{node1, node2} + + assert.EqualValues(t, filterHosts(hosts, opts), expected) +} + +func TestFilterHostsDifferentFlagsProduceAND(t *testing.T) { + opts := FilterOptions{ + DriverName: []string{"virtualbox"}, + State: []string{"Running"}, + } + node1 := + &libmachine.Host{ + Name: "node1", + DriverName: "fakedriver", + HostOptions: &libmachine.HostOptions{}, + Driver: &fakedriver.FakeDriver{MockState: state.Paused}, + } + node2 := + &libmachine.Host{ + Name: "node2", + DriverName: "virtualbox", + HostOptions: &libmachine.HostOptions{}, + Driver: &fakedriver.FakeDriver{MockState: state.Stopped}, + } + node3 := + &libmachine.Host{ + Name: "node3", + DriverName: "fakedriver", + HostOptions: &libmachine.HostOptions{}, + Driver: &fakedriver.FakeDriver{MockState: state.Running}, + } + hosts := []*libmachine.Host{node1, node2, node3} + expected := []*libmachine.Host{} + + assert.EqualValues(t, filterHosts(hosts, opts), expected) +} diff --git a/docs/index.md b/docs/index.md index b29a288b91..74755a13f2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -784,7 +784,29 @@ dev * virtualbox Stopped #### ls -List machines. +``` +Usage: docker-machine ls [OPTIONS] [arg...] + +List machines + +Options: + + --quiet, -q Enable quiet mode + --filter [--filter option --filter option] Filter output based on conditions provided +``` + +##### Filtering + +The filtering flag (`-f` or `--filter)` format is a `key=value` pair. If there is more +than one filter, then pass multiple flags (e.g. `--filter "foo=bar" --filter "bif=baz"`) + +The currently supported filters are: + +* driver (driver name) +* swarm (swarm master's name) +* state (`Running|Paused|Saved|Stopped|Stopping|Starting|Error`) + +##### Examples ``` $ docker-machine ls @@ -792,9 +814,13 @@ NAME ACTIVE DRIVER STATE URL dev virtualbox Stopped foo0 virtualbox Running tcp://192.168.99.105:2376 foo1 virtualbox Running tcp://192.168.99.106:2376 -foo2 virtualbox Running tcp://192.168.99.107:2376 -foo3 virtualbox Running tcp://192.168.99.108:2376 -foo4 * virtualbox Running tcp://192.168.99.109:2376 +foo2 * virtualbox Running tcp://192.168.99.107:2376 +``` + +``` +$ docker-machine ls --filter driver=virtualbox --filter state=Stopped +NAME ACTIVE DRIVER STATE URL SWARM +dev virtualbox Stopped ``` #### regenerate-certs @@ -1030,7 +1056,7 @@ Options: - `--google-scopes`: The scopes for OAuth 2.0 to Access Google APIs. See [Google Compute Engine Doc](https://cloud.google.com/storage/docs/authentication). - `--google-disk-size`: The disk size of instance. Default: `10` - `--google-disk-type`: The disk type of instance. Default: `pd-standard` - + The GCE driver will use the `ubuntu-1404-trusty-v20150316` instance type unless otherwise specified. #### IBM Softlayer diff --git a/test/integration/ls.bats b/test/integration/ls.bats new file mode 100644 index 0000000000..cf16832873 --- /dev/null +++ b/test/integration/ls.bats @@ -0,0 +1,27 @@ +#!/usr/bin/env bats + +load helpers + +teardown() { + echo "$BATS_TEST_NAME +---------- +$output +---------- + +" >> ${BATS_LOG} + machine rm -f testmachine +} + +@test "ls: filter on driver" { + run machine create -d none --url tcp://127.0.0.1:2375 testmachine + run machine ls --filter driver=none + [ "$status" -eq 0 ] + [[ ${lines[1]} =~ "testmachine" ]] +} + +@test "ls: filter on swarm" { + run machine create -d none --url tcp://127.0.0.1:2375 --swarm --swarm-master --swarm-discovery token://deadbeef testmachine + run machine ls --filter swarm=testmachine + [ "$status" -eq 0 ] + [[ ${lines[1]} =~ "testmachine" ]] +}