Skip to content

Commit

Permalink
Add hubble list namespaces command
Browse files Browse the repository at this point in the history
Signed-off-by: Chance Zibolski <chance.zibolski@gmail.com>
  • Loading branch information
chancez committed Jun 14, 2023
1 parent e715ad1 commit 1f20eb4
Show file tree
Hide file tree
Showing 593 changed files with 5,565 additions and 85,363 deletions.
18 changes: 18 additions & 0 deletions cmd/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,20 @@
package list

import (
"encoding/json"
"fmt"
"io"

"github.com/cilium/hubble/cmd/common/config"
"github.com/cilium/hubble/cmd/common/template"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)

var listOpts struct {
output string
}

// New creates a new list command.
func New(vp *viper.Viper) *cobra.Command {
listCmd := &cobra.Command{
Expand All @@ -23,6 +31,16 @@ func New(vp *viper.Viper) *cobra.Command {

listCmd.AddCommand(
newNodeCommand(vp),
newNamespacesCommand(vp),
)
return listCmd
}

func jsonOutput(buf io.Writer, v interface{}) error {
bs, err := json.MarshalIndent(v, "", " ")
if err != nil {
return err
}
_, err = fmt.Fprintln(buf, string(bs))
return err
}
124 changes: 124 additions & 0 deletions cmd/list/namespace_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Hubble

package list

import (
"bytes"
"testing"

observerpb "github.com/cilium/cilium/api/v1/observer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNamespaceOutputs(t *testing.T) {
var testCases = []struct {
name string
namespaces []*observerpb.Namespace
expectedJSONOutput string
expectedTableOutput string
expectedWideTableOutput string
}{
{
name: "multiple namespaces no cluster",
namespaces: []*observerpb.Namespace{
{Namespace: "foo"},
{Namespace: "bar"},
{Namespace: "baz"},
{Namespace: "faz"},
},
expectedJSONOutput: `[
{
"namespace": "foo"
},
{
"namespace": "bar"
},
{
"namespace": "baz"
},
{
"namespace": "faz"
}
]
`,
expectedTableOutput: `NAMESPACE
foo
bar
baz
faz
`,
expectedWideTableOutput: `NAMESPACE CLUSTER
foo N/A
bar N/A
baz N/A
faz N/A
`,
},
{
name: "multiple namespaces with cluster",
namespaces: []*observerpb.Namespace{
{Namespace: "foo", Cluster: "cluster-1"},
{Namespace: "bar", Cluster: "cluster-1"},
{Namespace: "baz", Cluster: "cluster-2"},
{Namespace: "faz", Cluster: "cluster-2"},
},
expectedJSONOutput: `[
{
"cluster": "cluster-1",
"namespace": "foo"
},
{
"cluster": "cluster-1",
"namespace": "bar"
},
{
"cluster": "cluster-2",
"namespace": "baz"
},
{
"cluster": "cluster-2",
"namespace": "faz"
}
]
`,
expectedTableOutput: `NAMESPACE
foo
bar
baz
faz
`,
expectedWideTableOutput: `NAMESPACE CLUSTER
foo cluster-1
bar cluster-1
baz cluster-2
faz cluster-2
`,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
defer func() {
listOpts.output = ""
}()
buf := bytes.Buffer{}

// json
require.NoError(t, jsonOutput(&buf, tc.namespaces), tc.name)
assert.Equal(t, tc.expectedJSONOutput, buf.String(), "json %s", tc.name)

// regular table
buf.Reset()
require.NoError(t, namespaceTableOutput(&buf, tc.namespaces), tc.name)
assert.Equal(t, tc.expectedTableOutput, buf.String(), "regular table %s", tc.name)

// wide table
listOpts.output = "wide"
buf.Reset()
require.NoError(t, namespaceTableOutput(&buf, tc.namespaces), tc.name)
assert.Equal(t, tc.expectedWideTableOutput, buf.String(), "wide table %s", tc.name)
})
}
}
101 changes: 101 additions & 0 deletions cmd/list/namespaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Hubble

package list

import (
"context"
"fmt"
"io"
"text/tabwriter"

observerpb "github.com/cilium/cilium/api/v1/observer"
"github.com/cilium/hubble/cmd/common/config"
"github.com/cilium/hubble/cmd/common/conn"
"github.com/cilium/hubble/cmd/common/template"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/spf13/viper"
"google.golang.org/grpc"
)

func newNamespacesCommand(vp *viper.Viper) *cobra.Command {
namespacesCmd := &cobra.Command{
Use: "namespaces",
Short: "List namespaces with recent flows",
RunE: func(cmd *cobra.Command, _ []string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
hubbleConn, err := conn.New(ctx, vp.GetString(config.KeyServer), vp.GetDuration(config.KeyTimeout))
if err != nil {
return err
}
defer hubbleConn.Close()
return runListNamespaces(ctx, cmd, hubbleConn)
},
}

// formatting flags
formattingFlags := pflag.NewFlagSet("Formatting", pflag.ContinueOnError)
formattingFlags.StringVarP(
&listOpts.output, "output", "o", "table",
`Specify the output format, one of:
json: JSON encoding
table: Tab-aligned columns
wide: Tab-aligned columns with additional information`)
namespacesCmd.Flags().AddFlagSet(formattingFlags)

// advanced completion for flags
namespacesCmd.RegisterFlagCompletionFunc("output", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return []string{
"json",
"table",
"wide",
}, cobra.ShellCompDirectiveDefault
})

template.RegisterFlagSets(namespacesCmd, formattingFlags, config.ServerFlags)
return namespacesCmd
}

func runListNamespaces(ctx context.Context, cmd *cobra.Command, conn *grpc.ClientConn) error {
req := &observerpb.GetNamespacesRequest{}
res, err := observerpb.NewObserverClient(conn).GetNamespaces(ctx, req)
if err != nil {
return err
}

namespaces := res.GetNamespaces()
switch listOpts.output {
case "json":
return jsonOutput(cmd.OutOrStdout(), namespaces)
case "table", "wide":
return namespaceTableOutput(cmd.OutOrStdout(), namespaces)
default:
return fmt.Errorf("unknown output format: %s", listOpts.output)
}
}

func namespaceTableOutput(buf io.Writer, namespaces []*observerpb.Namespace) error {
tw := tabwriter.NewWriter(buf, 2, 0, 3, ' ', 0)
// header
fmt.Fprint(tw, "NAMESPACE")
if listOpts.output == "wide" {
fmt.Fprint(tw, "\tCLUSTER")
}
fmt.Fprintln(tw)

// contents
for _, ns := range namespaces {
fmt.Fprint(tw, ns.Namespace)
if listOpts.output == "wide" {
cluster := ns.Cluster
if cluster == "" {
cluster = "N/A"
}
fmt.Fprint(tw, "\t", cluster)
}
fmt.Fprintln(tw)
}
return tw.Flush()
}
18 changes: 2 additions & 16 deletions cmd/list/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package list

import (
"context"
"encoding/json"
"fmt"
"io"
"sort"
Expand All @@ -26,10 +25,6 @@ import (

const notAvailable = "N/A"

var listOpts struct {
output string
}

func newNodeCommand(vp *viper.Viper) *cobra.Command {
listCmd := &cobra.Command{
Use: "nodes",
Expand Down Expand Up @@ -85,13 +80,13 @@ func runListNodes(ctx context.Context, cmd *cobra.Command, conn *grpc.ClientConn
case "json":
return jsonOutput(cmd.OutOrStdout(), nodes)
case "table", "wide":
return tableOutput(cmd.OutOrStdout(), nodes)
return nodeTableOutput(cmd.OutOrStdout(), nodes)
default:
return fmt.Errorf("unknown output format: %s", listOpts.output)
}
}

func tableOutput(buf io.Writer, nodes []*observerpb.Node) error {
func nodeTableOutput(buf io.Writer, nodes []*observerpb.Node) error {
tw := tabwriter.NewWriter(buf, 2, 0, 3, ' ', 0)
fmt.Fprint(tw, "NAME\tSTATUS\tAGE\tFLOWS/S\tCURRENT/MAX-FLOWS")
if listOpts.output == "wide" {
Expand Down Expand Up @@ -130,15 +125,6 @@ func tableOutput(buf io.Writer, nodes []*observerpb.Node) error {
return tw.Flush()
}

func jsonOutput(buf io.Writer, nodes []*observerpb.Node) error {
bs, err := json.MarshalIndent(nodes, "", " ")
if err != nil {
return err
}
_, err = fmt.Fprintln(buf, string(bs))
return err
}

func nodeStateToString(state relaypb.NodeState) string {
switch state {
case relaypb.NodeState_NODE_CONNECTED:
Expand Down
6 changes: 3 additions & 3 deletions cmd/list/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/stretchr/testify/require"
)

func TestOutputs(t *testing.T) {
func TestNodeOutputs(t *testing.T) {
var testCases = []struct {
name string
nodes []*observerpb.Node
Expand Down Expand Up @@ -186,13 +186,13 @@ foo Connected 16m40s 0.00 500/1000 ( 50.00%) 1 1.2.3.4 E

// regular table
buf.Reset()
require.NoError(t, tableOutput(&buf, tc.nodes), tc.name)
require.NoError(t, nodeTableOutput(&buf, tc.nodes), tc.name)
assert.Equal(t, tc.expectedTableOutput, buf.String(), "regular table %s", tc.name)

// wide table
listOpts.output = "wide"
buf.Reset()
require.NoError(t, tableOutput(&buf, tc.nodes), tc.name)
require.NoError(t, nodeTableOutput(&buf, tc.nodes), tc.name)
assert.Equal(t, tc.expectedWideTableOutput, buf.String(), "wide table %s", tc.name)
})
}
Expand Down
5 changes: 5 additions & 0 deletions cmd/observe/io_reader_observer.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ func (o *IOReaderObserver) GetNodes(_ context.Context, _ *observerpb.GetNodesReq
return nil, status.Errorf(codes.Unimplemented, "GetNodes not implemented")
}

// GetNamespaces is not implemented, and will throw an error if used.
func (o *IOReaderObserver) GetNamespaces(_ context.Context, _ *observerpb.GetNamespacesRequest, _ ...grpc.CallOption) (*observerpb.GetNamespacesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "GetNamespaces not implemented")
}

// ServerStatus is not implemented, and will throw an error if used.
func (o *IOReaderObserver) ServerStatus(_ context.Context, _ *observerpb.ServerStatusRequest, _ ...grpc.CallOption) (*observerpb.ServerStatusResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "ServerStatus not implemented")
Expand Down
Loading

0 comments on commit 1f20eb4

Please sign in to comment.