Adding --filter flag to ls command

Initially supporting `swarm=`, `state=`, and `driver=` filters.

Signed-off-by: Dave Henderson <Dave.Henderson@ca.ibm.com>
This commit is contained in:
Dave Henderson 2015-05-04 22:09:40 -04:00
parent 8ed87c37c2
commit a3c8b3474e
5 changed files with 408 additions and 6 deletions

View File

@ -293,6 +293,11 @@ var Commands = []cli.Command{
Name: "quiet, q", Name: "quiet, q",
Usage: "Enable quiet mode", Usage: "Enable quiet mode",
}, },
cli.StringSliceFlag{
Name: "filter",
Usage: "Filter output based on conditions provided",
Value: &cli.StringSlice{},
},
}, },
Name: "ls", Name: "ls",
Usage: "List machines", Usage: "List machines",

View File

@ -2,16 +2,28 @@ package commands
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"strings"
"text/tabwriter" "text/tabwriter"
"github.com/codegangsta/cli" "github.com/codegangsta/cli"
"github.com/docker/machine/libmachine" "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) { func cmdLs(c *cli.Context) {
quiet := c.Bool("quiet") quiet := c.Bool("quiet")
filters, err := parseFilters(c.StringSlice("filter"))
if err != nil {
log.Fatal(err)
}
mcn := getDefaultMcn(c) mcn := getDefaultMcn(c)
hostList, err := mcn.List() hostList, err := mcn.List()
@ -19,6 +31,8 @@ func cmdLs(c *cli.Context) {
log.Fatal(err) log.Fatal(err)
} }
hostList = filterHosts(hostList, filters)
// Just print out the names if we're being quiet // Just print out the names if we're being quiet
if quiet { if quiet {
for _, host := range hostList { for _, host := range hostList {
@ -68,3 +82,102 @@ func cmdLs(c *cli.Context) {
w.Flush() 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
}

View File

@ -1 +1,232 @@
package commands 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)
}

View File

@ -784,7 +784,29 @@ dev * virtualbox Stopped
#### ls #### 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 $ docker-machine ls
@ -792,9 +814,13 @@ NAME ACTIVE DRIVER STATE URL
dev virtualbox Stopped dev virtualbox Stopped
foo0 virtualbox Running tcp://192.168.99.105:2376 foo0 virtualbox Running tcp://192.168.99.105:2376
foo1 virtualbox Running tcp://192.168.99.106:2376 foo1 virtualbox Running tcp://192.168.99.106:2376
foo2 virtualbox Running tcp://192.168.99.107: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
```
$ docker-machine ls --filter driver=virtualbox --filter state=Stopped
NAME ACTIVE DRIVER STATE URL SWARM
dev virtualbox Stopped
``` ```
#### regenerate-certs #### regenerate-certs

27
test/integration/ls.bats Normal file
View File

@ -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" ]]
}