diff --git a/accounts/accounts.go b/accounts/accounts.go index ac8b0b0..1965e81 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -111,15 +111,16 @@ func printSamples(writer io.Writer, samples []Sample) error { workloads = append(workloads, w.ToString()) } data = append(data, ui.TableRow{ - ui.TableCell{Contents: sample.Container.Handle}, - ui.TableCell{Contents: string(sample.Labels.Type)}, ui.TableCell{Contents: strings.Join(workloads, ",")}, + ui.TableCell{Contents: string(sample.Labels.Type)}, + ui.TableCell{Contents: humanReadable(sample.Container.Stats.Memory)}, + ui.TableCell{Contents: sample.Container.Handle}, }) } table := ui.Table{ Headers: ui.TableRow{ ui.TableCell{ - Contents: "handle", + Contents: "workloads", Color: color.New(color.Bold), }, ui.TableCell{ @@ -127,7 +128,11 @@ func printSamples(writer io.Writer, samples []Sample) error { Color: color.New(color.Bold), }, ui.TableCell{ - Contents: "workloads", + Contents: "memory", + Color: color.New(color.Bold), + }, + ui.TableCell{ + Contents: "handle", Color: color.New(color.Bold), }, }, @@ -136,6 +141,19 @@ func printSamples(writer io.Writer, samples []Sample) error { return table.Render(writer, true) } +func humanReadable(bytes uint64) string { + KB := uint64(1) << 10 + MB := KB << 10 + switch { + case bytes > MB: + return fmt.Sprintf("%.1f MB", float64(bytes / MB) + float64(bytes % MB)/float64(MB)) + case bytes > KB: + return fmt.Sprintf("%.1f KB", float64(bytes / KB) + float64(bytes % KB)/float64(KB)) + default: + return fmt.Sprintf("%d B", bytes) + } +} + type Sample struct { Container Container Labels Labels @@ -158,6 +176,7 @@ type Container struct { } type Stats struct { + Memory uint64 } // a Workload is a description of a concourse core concept that corresponds to diff --git a/accounts/accounts_suite_test.go b/accounts/accounts_suite_test.go index dbd100d..f942afa 100644 --- a/accounts/accounts_suite_test.go +++ b/accounts/accounts_suite_test.go @@ -29,4 +29,7 @@ func TestAccounts(t *testing.T) { suite.Run(t, &K8sClientSuite{ Assertions: require.New(t), }) + suite.Run(t, &GardenConnectionSuite{ + Assertions: require.New(t), + }) } diff --git a/accounts/garden_connection_test.go b/accounts/garden_connection_test.go new file mode 100644 index 0000000..2ac1cec --- /dev/null +++ b/accounts/garden_connection_test.go @@ -0,0 +1,67 @@ +package accounts_test + +import ( + "net" + + "code.cloudfoundry.org/garden" + "code.cloudfoundry.org/garden/gardenfakes" + "code.cloudfoundry.org/garden/server" + "code.cloudfoundry.org/lager/lagertest" + "github.com/concourse/ft/accounts" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" +) + +type GardenConnectionSuite struct { + suite.Suite + *require.Assertions + gardenServer *server.GardenServer + backend *gardenfakes.FakeBackend + listener net.Listener +} + +func (s *GardenConnectionSuite) SetupTest() { + s.backend = new(gardenfakes.FakeBackend) + s.gardenServer = server.New( + "tcp", + "127.0.0.1:7777", + 0, + s.backend, + lagertest.NewTestLogger("test"), + ) + s.listener, _ = net.Listen("tcp", "127.0.0.1:7777") + go s.gardenServer.Serve(s.listener) +} + +func (s *GardenConnectionSuite) TearDownTest() { + s.gardenServer.Stop() + s.listener.Close() +} + +func (s *LANWorkerSuite) TestGetsAllMetrics() { + metricsEntry := garden.ContainerMetricsEntry{ + Metrics: garden.Metrics{ + MemoryStat: garden.ContainerMemoryStat{ + TotalActiveAnon: 123, + }, + }, + Err: nil, + } + s.backend.BulkMetricsReturns(map[string]garden.ContainerMetricsEntry{ + "container-handle": metricsEntry, + }, nil) + + connection := accounts.GardenConnection{ + Dialer: &accounts.LANGardenDialer{}, + } + metricsEntries, err := connection.AllMetrics() + + s.NoError(err) + s.Len(metricsEntries, 1) + s.Contains(metricsEntries, "container-handle") + s.Equal(metricsEntry, metricsEntries["container-handle"]) +} + +// a container "exists" if it appears in the output from List +// BulkMetrics fails if you ask about nonexistent containers +// BulkMetrics actually returns metrics for the containers you asked about diff --git a/accounts/worker.go b/accounts/worker.go index 95442cc..bea1f21 100644 --- a/accounts/worker.go +++ b/accounts/worker.go @@ -5,6 +5,7 @@ import ( "net/http" "time" + "code.cloudfoundry.org/garden" "code.cloudfoundry.org/garden/client/connection" "code.cloudfoundry.org/lager" v1 "k8s.io/api/core/v1" @@ -21,19 +22,39 @@ type GardenWorker struct { Dialer GardenDialer } -func (gw *GardenWorker) Containers(opts ...StatsOption) ([]Container, error) { - handles, err := connection.NewWithDialerAndLogger( +type GardenConnection struct { + Dialer GardenDialer +} + +func (gc GardenConnection) AllMetrics() (map[string]garden.ContainerMetricsEntry, error) { + connection := connection.NewWithDialerAndLogger( func(string, string) (net.Conn, error) { - return gw.Dialer.Dial() + return gc.Dialer.Dial() }, lager.NewLogger("garden-connection"), - ).List(nil) + ) + handles, err := connection.List(nil) + if err != nil { + return nil, err + } + return connection.BulkMetrics(handles) +} + +func (gw *GardenWorker) Containers(opts ...StatsOption) ([]Container, error) { + metricsEntries, err := GardenConnection{Dialer: gw.Dialer}.AllMetrics() if err != nil { return nil, err } containers := []Container{} - for _, handle := range handles { - containers = append(containers, Container{Handle: handle}) + for handle, metricsEntry := range metricsEntries { + memory := metricsEntry.Metrics.MemoryStat.TotalUsageTowardLimit + containers = append( + containers, + Container{ + Handle: handle, + Stats: Stats{Memory: memory}, + }, + ) } return containers, nil } diff --git a/accounts/worker_test.go b/accounts/worker_test.go index 20c0ad4..a9ca9f5 100644 --- a/accounts/worker_test.go +++ b/accounts/worker_test.go @@ -25,6 +25,8 @@ import ( "k8s.io/kubernetes/pkg/kubelet/server/streaming" ) +// TODO replace real garden server with fake gardenconnection + type LANWorkerSuite struct { suite.Suite *require.Assertions @@ -52,9 +54,25 @@ func (s *LANWorkerSuite) TearDownTest() { } func (s *LANWorkerSuite) TestLANWorkerListsContainers() { - fakeContainer := new(gardenfakes.FakeContainer) - fakeContainer.HandleReturns("container-handle") - s.backend.ContainersReturns([]garden.Container{fakeContainer}, nil) + worker := accounts.GardenWorker{ + Dialer: &accounts.LANGardenDialer{}, + } + worker.Containers() + + s.Equal(s.backend.ContainersCallCount(), 1) +} + +func (s *LANWorkerSuite) TestLANWorkerGetsMemory() { + s.backend.BulkMetricsReturns(map[string]garden.ContainerMetricsEntry{ + "container-handle": garden.ContainerMetricsEntry{ + Metrics: garden.Metrics{ + MemoryStat: garden.ContainerMemoryStat{ + TotalUsageTowardLimit: 123, + }, + }, + Err: nil, + }, + }, nil) worker := accounts.GardenWorker{ Dialer: &accounts.LANGardenDialer{}, @@ -64,6 +82,7 @@ func (s *LANWorkerSuite) TestLANWorkerListsContainers() { s.NoError(err) s.Len(containers, 1) s.Equal(containers[0].Handle, "container-handle") + s.Equal(uint64(123), containers[0].Stats.Memory) } type K8sGardenDialerSuite struct {