-
Notifications
You must be signed in to change notification settings - Fork 123
[WIP] Display information about the cluster or all running footloose containers #103
Changes from 15 commits
a096521
d054122
84ca325
07ea620
869d6f8
8c2a35d
39f90a8
9440515
e45d5bb
3fbfb60
08a3e2d
acfdc08
295ff7b
25c7654
531ae18
a24fdcf
39e3245
9190cfa
7fb56a1
3b486fd
36f161b
3fde86f
26da54e
09b40d6
d183e61
9ca9c4b
e0be0ad
dd971fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,21 @@ | ||
module github.com/weaveworks/footloose | ||
|
||
require ( | ||
github.com/apcera/termtables v0.0.0-20170405184538-bcbc5dc54055 | ||
github.com/docker/distribution v2.7.1+incompatible // indirect | ||
github.com/docker/docker v1.13.1 | ||
github.com/docker/go-connections v0.4.0 // indirect | ||
github.com/docker/go-units v0.3.3 // indirect | ||
github.com/ghodss/yaml v1.0.0 | ||
github.com/inconshreveable/mousetrap v1.0.0 // indirect | ||
github.com/mattn/go-runewidth v0.0.4 // indirect | ||
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect | ||
github.com/pkg/errors v0.8.1 | ||
github.com/sirupsen/logrus v1.3.0 | ||
github.com/spf13/cobra v0.0.3 | ||
github.com/spf13/pflag v1.0.3 // indirect | ||
github.com/stretchr/testify v1.2.2 | ||
golang.org/x/net v0.0.0-20190322120337-addf6b3196f6 // indirect | ||
gopkg.in/yaml.v2 v2.2.2 // indirect | ||
sigs.k8s.io/kind v0.0.0-20190204012257-d1773a79317d | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package main | ||
|
||
import ( | ||
"github.com/spf13/cobra" | ||
"github.com/weaveworks/footloose/pkg/cluster" | ||
) | ||
|
||
var listCmd = &cobra.Command{ | ||
Use: "list", | ||
Short: "List all running machines", | ||
RunE: list, | ||
} | ||
|
||
var listOptions struct { | ||
format string | ||
config string | ||
all bool | ||
} | ||
|
||
func init() { | ||
listCmd.Flags().StringVarP(&listOptions.config, "config", "c", Footloose, "Cluster configuration file") | ||
listCmd.Flags().StringVarP(&listOptions.format, "format", "f", "default", "Formatting options") | ||
listCmd.Flags().BoolVar(&listOptions.all, "all", false, "List all footloose created machines in every cluster.") | ||
footloose.AddCommand(listCmd) | ||
} | ||
|
||
// list will list all machines in a given cluster. | ||
// if --all option is provided it will list every machine created by | ||
// footloose no matter what cluster they are in. | ||
func list(cmd *cobra.Command, args []string) error { | ||
cluster, err := cluster.NewFromFile(listOptions.config) | ||
if err != nil { | ||
return err | ||
} | ||
return cluster.List(listOptions.all, listOptions.format) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,17 @@ | ||
package cluster | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
"os" | ||
"regexp" | ||
"time" | ||
|
||
"github.com/docker/docker/api/types" | ||
"github.com/docker/docker/api/types/filters" | ||
"github.com/docker/docker/client" | ||
"github.com/ghodss/yaml" | ||
log "github.com/sirupsen/logrus" | ||
"github.com/weaveworks/footloose/pkg/config" | ||
|
@@ -162,6 +166,8 @@ func (c *Cluster) createMachine(machine *Machine, i int) error { | |
func (c *Cluster) createMachineRunArgs(machine *Machine, name string, i int) []string { | ||
runArgs := []string{ | ||
"-it", "-d", "--rm", | ||
"--label", "org.weaveworks.owner=footloose", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
"--label", "org.weaveworks.cluster=" + c.spec.Cluster.Name, | ||
"--name", name, | ||
"--hostname", machine.Hostname(), | ||
"--tmpfs", "/run", | ||
|
@@ -232,6 +238,94 @@ func (c *Cluster) Delete() error { | |
return c.forEachMachine(c.deleteMachine) | ||
} | ||
|
||
// List will generate an output for each machine. | ||
func (c *Cluster) List(all bool, format string) error { | ||
machines, err := c.gatherMachinesWithFallback(all) | ||
if err != nil { | ||
return err | ||
} | ||
formatter, err := getFormatter(format) | ||
if err != nil { | ||
return err | ||
} | ||
return formatter.Format(machines) | ||
} | ||
|
||
func (c *Cluster) gatherMachinesWithFallback(all bool) (machines []*Machine, err error) { | ||
machines, err = c.gatherMachinesByAPI(all) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was wondering if we should have the |
||
if err != nil { | ||
return []*Machine{}, err | ||
} | ||
// Footloose has no machines running. Falling back to display | ||
// cluster related data. | ||
if len(machines) < 1 { | ||
machines = c.gatherMachinesByCluster() | ||
} | ||
return | ||
} | ||
|
||
func (c *Cluster) gatherMachinesByAPI(all bool) (machines []*Machine, err error) { | ||
cli, err := client.NewEnvClient() | ||
if err != nil { | ||
return []*Machine{}, err | ||
} | ||
|
||
args := filters.NewArgs() | ||
args.Add("label", "org.weaveworks.owner=footloose") | ||
if !all { | ||
args.Add("label", "org.weaveworks.cluster="+c.spec.Cluster.Name) | ||
} | ||
ctx := context.Background() | ||
containers, err := cli.ContainerList(ctx, types.ContainerListOptions{ | ||
Filters: args, | ||
}) | ||
if err != nil { | ||
return []*Machine{}, err | ||
} | ||
|
||
for _, container := range containers { | ||
m := Machine{} | ||
spec := config.Machine{} | ||
m.name = container.Names[0] | ||
inspect, err := cli.ContainerInspect(ctx, container.ID) | ||
if err != nil { | ||
return []*Machine{}, err | ||
} | ||
m.hostname = inspect.Config.Hostname | ||
ports := make(map[int]int) | ||
for _, p := range container.Ports { | ||
ports[int(p.PrivatePort)] = int(p.PublicPort) | ||
} | ||
m.ports = ports | ||
spec.Cmd = container.Command | ||
spec.Image = container.Image | ||
var volumes []config.Volume | ||
for _, mount := range container.Mounts { | ||
v := config.Volume{ | ||
Type: string(mount.Type), | ||
Source: mount.Source, | ||
Destination: mount.Destination, | ||
ReadOnly: mount.RW, | ||
} | ||
volumes = append(volumes, v) | ||
} | ||
spec.Volumes = volumes | ||
m.spec = &spec | ||
machines = append(machines, &m) | ||
} | ||
return | ||
} | ||
|
||
func (c *Cluster) gatherMachinesByCluster() (machines []*Machine) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I saw that you already have forEachMachine but since I wanted each machine I extracted this. I could use the forEachMachine function but then I would have to resort to a package wide variable to track each machines inspect data that came back. That was my other idea in case I would loose the |
||
for _, template := range c.spec.Machines { | ||
for i := 0; i < template.Count; i++ { | ||
machine := c.machine(&template.Spec, i) | ||
machines = append(machines, machine) | ||
} | ||
} | ||
return | ||
} | ||
|
||
// io.Writer filter that writes that it receives to writer. Keeps track if it | ||
// has seen a write matching regexp. | ||
type matchFilter struct { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package cluster | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/apcera/termtables" | ||
"github.com/weaveworks/footloose/pkg/config" | ||
) | ||
|
||
// Formatter formats a slice of machines and outputs the result | ||
// in a given format. | ||
type Formatter interface { | ||
Format([]*Machine) error | ||
} | ||
|
||
// JSONFormatter formats a slice of machines into a JSON and | ||
// outputs it to stdout. | ||
type JSONFormatter struct{} | ||
|
||
// NormalFormatter formats a slice of machines into a colored | ||
// table like output and prints that to stdout. | ||
type NormalFormatter struct{} | ||
|
||
type port struct { | ||
Guest int `json:"guest"` | ||
Host int `json:"host"` | ||
} | ||
|
||
type status struct { | ||
Name string `json:"name"` | ||
Spec config.Machine `json:"spec"` | ||
Status string `json:"status"` | ||
Ports []port `json:"ports"` | ||
Hostname string `json:"hostname"` | ||
} | ||
|
||
// Format will output to stdout in JSON format. | ||
func (JSONFormatter) Format(machines []*Machine) error { | ||
statuses := make([]status, 0) | ||
for _, m := range machines { | ||
s := status{} | ||
s.Hostname = m.Hostname() | ||
s.Name = m.ContainerName() | ||
s.Spec = *m.spec | ||
state := "Stopped" | ||
if m.IsRunning() { | ||
state = "Running" | ||
} | ||
s.Status = state | ||
var ports []port | ||
for k, v := range m.ports { | ||
p := port{ | ||
Host: v, | ||
Guest: k, | ||
} | ||
ports = append(ports, p) | ||
} | ||
s.Ports = ports | ||
statuses = append(statuses, s) | ||
} | ||
m := struct { | ||
Machines []status `json:"machines"` | ||
}{ | ||
Machines: statuses, | ||
} | ||
ms, err := json.Marshal(m) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'll be nice to directly provide an indented output I think (MashalIndent) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure. I just thought that usually when you want JSON you want it because you would like to parse it. In which case indentation might cause a problem in certain cases. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. json parsers should really be able to handle the whitespace introduced by the indentation :) |
||
if err != nil { | ||
return err | ||
} | ||
fmt.Printf("%s", ms) | ||
return nil | ||
} | ||
|
||
// Format will output to stdout in table format. | ||
func (NormalFormatter) Format(machines []*Machine) error { | ||
table := termtables.CreateTable() | ||
table.AddHeaders("Name", "Hostname", "Ports", "Image", "Cmd", "Volumes", "State") | ||
for _, m := range machines { | ||
state := "Stopped" | ||
if m.IsRunning() { | ||
state = "Running" | ||
} | ||
var ports []string | ||
for k, v := range m.ports { | ||
p := fmt.Sprintf("%d->%d", k, v) | ||
ports = append(ports, p) | ||
} | ||
if len(ports) < 1 { | ||
for _, p := range m.spec.PortMappings { | ||
port := fmt.Sprintf("%d->%d", p.ContainerPort, 0) | ||
ports = append(ports, port) | ||
} | ||
} | ||
ps := strings.Join(ports, ",") | ||
var volumes []string | ||
for _, v := range m.spec.Volumes { | ||
vf := fmt.Sprintf("%s->%s", v.Source, v.Destination) | ||
volumes = append(volumes, vf) | ||
} | ||
vs := strings.Join(volumes, ",") | ||
table.AddRow(m.ContainerName(), m.hostname, ps, m.spec.Image, m.spec.Cmd, vs, state) | ||
} | ||
fmt.Println(table.Render()) | ||
return nil | ||
} | ||
|
||
func getFormatter(format string) (Formatter, error) { | ||
var formatter Formatter | ||
switch format { | ||
case "json": | ||
formatter = new(JSONFormatter) | ||
case "default": | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could use a better name, like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True. :) |
||
formatter = new(NormalFormatter) | ||
default: | ||
return nil, errors.New("unrecognised formatting method") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's nice to add information about what is wrong, say:
so the user knows at a glance which part of the command line is wrong. |
||
} | ||
return formatter, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
footloose config create --config %testName.footloose --name %testName --key %testName-key --image quay.io/footloose/ubuntu18.04 | ||
footloose create --config %testName.footloose | ||
footloose delete --config %testName.footloose | ||
%out footloose list --config %testName.footloose | ||
%out footloose list -f json --config %testName.footloose |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
+-----------------------------+----------+-------+-------------------------------+-----+---------+---------+ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right now, considering how the golden output works, I can only test for non running containers, since the running ones have dynamic data. Like the host port number which would be different on each test run. This is good enough for now... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm envisioning being able to query parts of the json like
we could use to test |
||
| Name | Hostname | Ports | Image | Cmd | Volumes | State | | ||
+-----------------------------+----------+-------+-------------------------------+-----+---------+---------+ | ||
| test-list-ubuntu18.04-node0 | node0 | 22->0 | quay.io/footloose/ubuntu18.04 | | | Stopped | | ||
+-----------------------------+----------+-------+-------------------------------+-----+---------+---------+ | ||
|
||
{"machines":[{"name":"test-list-ubuntu18.04-node0","spec":{"name":"node%d","image":"quay.io/footloose/ubuntu18.04","portMappings":[{"containerPort":22}]},"status":"Stopped","ports":null,"hostname":"node0"}]} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please use
--output, -o
here so we can reserve--format
for go templates.