Skip to content

Commit

Permalink
Added namespace support to pod commands
Browse files Browse the repository at this point in the history
  • Loading branch information
daschott committed Sep 12, 2022
1 parent faa73bc commit 9557d3a
Show file tree
Hide file tree
Showing 11 changed files with 242 additions and 225 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ If you decide not to deploy the server as a container and manually download it t
## Wcnspect Client
The client needs to be executed as a standalone binary from either a Windows or a Linux VM in the same network (jumpbox).

The wcnspect client reads in the user's `.kube` config file and uses the `default` namespace.
The wcnspect client requires access to the [Kubernetes cluster config](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/).

By default, Wcnspect client will search for a file named `config` in the `$HOME/.kube` directory. Otherwise, it will use the $KUBECONFIG environment variable.

By default, most commands pull information from *all* Windows nodes.
Consequently, when using commands, the user should reference node names and pod names for better filtering of results.
Expand Down Expand Up @@ -116,9 +118,7 @@ Currently, this project's code makes the following assumptions:
* When applying `wcnspectserv-daemon.yml`, the user has access to the ACR referenced in the file.

## TODO
* Read in `$KUBECONFIG` env var.
* Support for other ports on client side.
* Namespace support for pods.
* Support for other ports on the Wcnspect client.


## Contributing
Expand Down
44 changes: 28 additions & 16 deletions cmd/wcnspect/cmd/capture.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import (
"sync"
"syscall"

"github.com/microsoft/wcnspect/common"
"github.com/microsoft/wcnspect/pkg/client"
"github.com/microsoft/wcnspect/pkg/k8spi"
"github.com/microsoft/wcnspect/pkg/k8sapi"
pb "github.com/microsoft/wcnspect/rpc"
v1 "k8s.io/api/core/v1"

"github.com/spf13/cobra"
)
Expand All @@ -28,6 +30,7 @@ type captureCmd struct {

packetType string
countersOnly bool
namespace string

*baseBuilderCmd
}
Expand All @@ -39,14 +42,14 @@ func (b *commandsBuilder) newCaptureCmd() *captureCmd {
Use: "capture",
Short: "The 'capture' command will run a packet capture on all windows nodes.",
Long: `The 'capture' command will run a packet capture on all windows nodes. For example:
'wcnspect capture pods {pods} --protocols TCP -d 10'.`,
'wcnspect capture pods {pod1,pod2} --protocols TCP -d 10'.`,
}

captureTypes := []string{"all", "nodes", "pods"}
captureHelp := map[string]string{
"all": "Runs on all windows nodes in the AKS cluster.",
"nodes": "Specify which nodes wcnspect should send requests to using node names.",
"pods": "Specify which pods the capture should filter on. Supports up to two pod names. Automatically defines nodes to capture on.",
"nodes": "Specify which nodes wcnspect should send requests to using comma-separated node names.",
"pods": "Specify which pods the capture should filter on. Supports up to two comma-separated pod names.",
}
for _, name := range captureTypes {
subcmd := &cobra.Command{
Expand All @@ -56,10 +59,8 @@ func (b *commandsBuilder) newCaptureCmd() *captureCmd {
cc.printCapture(cmd.Name(), args)
},
}

cmd.AddCommand(subcmd)
}

cmd.PersistentFlags().Int32VarP(&cc.time, "time", "d", 0, "Time to run packet capture for (in seconds). Runs indefinitely given 0.")

cmd.PersistentFlags().StringSliceVarP(&cc.ips, "ips", "i", []string{}, "Match source or destination IP address. CIDR supported.")
Expand All @@ -69,20 +70,22 @@ func (b *commandsBuilder) newCaptureCmd() *captureCmd {

cmd.PersistentFlags().StringVar(&cc.packetType, "type", "all", "Select which packets to capture. Can be all, flow, or drop.")
cmd.PersistentFlags().BoolVar(&cc.countersOnly, "counters-only", false, "Collect packet counters only. No packet logging.")

cmd.PersistentFlags().StringVarP(&cc.namespace, "namespace", "n", common.DefaultNamespace, "Specify Kubernetes namespace to filter pods on.")
cc.baseBuilderCmd = b.newBuilderCmd(cmd)

return cc
}

func (cc *captureCmd) printCapture(subcmd string, endpoints []string) {
cc.validateArgs()

targetNodes := cc.getWinNodes()
var targetNodes []v1.Node
// Store mapping of NodeName => Pod IPs
hostMap := make(map[string][]string)

// Revise nodes and pods arguments based on command name
switch subcmd {
default:
targetNodes = cc.getWinNodes()
case "nodes":
if len(endpoints) == 0 {
log.Fatal("must pass node names when using 'wcnspect capture nodes ...'")
Expand All @@ -92,20 +95,29 @@ func (cc *captureCmd) printCapture(subcmd string, endpoints []string) {
if err := client.ValidateNodes(nodes, cc.getWinNodeNames()); err != nil {
log.Fatal(err)
}

targetNodes = cc.getNodes(nodes)
case "pods":
// Use namespace command
//cc.cmd.PersistentFlags().StringVar(&cc.namespace, "namespace", common.DefaultNamespace, "Optionally specify Kubernetes namespace to filter pods on.")
if len(endpoints) == 0 {
log.Fatal("must pass pod names when using 'wcnspect capture pods ...'")
}

pods := strings.Split(endpoints[0], ",")
if err := client.ValidatePods(pods, cc.getPodNames()); err != nil {
log.Fatal(err)
// Namespace
ns := k8sclient.GetNamespace(cc.namespace)
// Loop over Pod, Node
var p *v1.Pod
var nodeName string
for _, podName := range pods {
p = k8sclient.GetPod(podName, ns.GetName())
nodeName = p.Spec.NodeName
podIP := p.Status.PodIP
if nodeName != "" {
hostMap[nodeName] = append(hostMap[nodeName], podIP)
targetNodes = append(targetNodes, cc.getNode(nodeName))
}
}

hostMap = cc.getNodePodMap(pods)
targetNodes = cc.getPodsNodes(pods)
}

// Capture any sigint to send a StopCapture request
Expand All @@ -121,7 +133,7 @@ func (cc *captureCmd) printCapture(subcmd string, endpoints []string) {
for _, node := range targetNodes {
wg.Add(1)

name, ip := node.GetName(), k8spi.RetrieveInternalIP(node)
name, ip := node.GetName(), k8sapi.RetrieveInternalIP(node)

c, closeClient := client.CreateConnection(ip)
defer closeClient()
Expand Down
90 changes: 11 additions & 79 deletions cmd/wcnspect/cmd/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,14 @@
package cmd

import (
"context"
"fmt"
"log"
"path/filepath"

"github.com/microsoft/wcnspect/common"
"github.com/microsoft/wcnspect/pkg/comprise"
"github.com/microsoft/wcnspect/pkg/k8sapi"
"github.com/spf13/cobra"

v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
)

type commandsBuilder struct {
Expand Down Expand Up @@ -93,16 +87,8 @@ func (b *commandsBuilder) newwcnspectCmd() *wcnspectCmd {
Short: "wcnspect is an advanced distributed packet capture and HNS log collection tool.",
Long: `An advanced distributed packet capture and HNS log collection tool made with Go (^_^)`,
})

if home := homedir.HomeDir(); home != "" {
cc.cmd.PersistentFlags().StringVar(&cc.kubeconfig, "kubeconfig", filepath.Join(home, ".kube", "config"), "Optionally specify absolute path to the kubeconfig file.")
} else {
cc.cmd.PersistentFlags().StringVar(&cc.kubeconfig, "kubeconfig", "", "Specify absolute path to the kubeconfig file.")
cc.cmd.MarkPersistentFlagRequired("kubeconfig")
}

cc.cmd.PersistentFlags().StringVar(&cc.kubeconfig, "kubeconfig", "", "Specify absolute path to the kubeconfig file.")
cc.cmd.CompletionOptions.DisableDefaultCmd = true

cc.initializeAKSClusterValues()

return cc
Expand All @@ -112,58 +98,27 @@ type wcnspectBuilderCommon struct {
kubeconfig string

winNodeNames map[string]v1.Node // node name -> v1.Node
podNames map[string]v1.Pod // pod name -> v1.Pod

podsNode map[string]v1.Node // pod name -> v1.Node (pod's node)
}

func (cc *wcnspectBuilderCommon) initializeAKSClusterValues() {
// Use the current context in kubeconfig
config, err := clientcmd.BuildConfigFromFlags("", cc.kubeconfig)
if err != nil {
log.Fatal(err)
}

// Create the clientset
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
k8sclient = k8sapi.New(cc.kubeconfig)

// Pull windows nodes
nodes, err := clientset.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{LabelSelector: "kubernetes.io/os=windows"})
if err != nil {
log.Fatal(err)
}
nodes := k8sclient.GetAllNodesWindows()

// Pull pods while creating mapping of node : pods
cc.winNodeNames = make(map[string]v1.Node)
cc.podNames = make(map[string]v1.Pod)
cc.podsNode = make(map[string]v1.Node)

for _, node := range nodes.Items {
nodeName := node.GetName()

// Set node names to node items
cc.winNodeNames[nodeName] = node

// Pull pods
filter := fmt.Sprintf("spec.nodeName=%s", nodeName)
pods, err := clientset.CoreV1().Pods(common.Namespace).List(context.TODO(), metav1.ListOptions{FieldSelector: filter})
if err != nil {
log.Fatal(err)
}

// Set pods' names to node
for _, pod := range pods.Items {
podName := pod.GetName()
cc.podsNode[podName] = node
cc.podNames[podName] = pod
}
}

// Validate there is at least one windows node
if len(cc.winNodeNames) == 0 {
log.Fatal("no windows nodes exist")
log.Fatal("no Windows nodes exist")
}
}

Expand All @@ -174,38 +129,15 @@ func (cc *wcnspectBuilderCommon) getNodes(nodeNames []string) (ret []v1.Node) {
return
}

/* Takes a list of pod names. Creates a map with these pods and returns it.
Returns a map of node names to a list of pod ips.
*/
func (cc *wcnspectBuilderCommon) getNodePodMap(podNames []string) map[string][]string {
ret := make(map[string][]string)

for _, podName := range podNames {
node := cc.podsNode[podName]

nodeName := node.GetName()
if nodeName != "" {
ret[nodeName] = append(ret[nodeName], cc.podNames[podName].Status.PodIP)
}
}

return ret
}

/*
Returns a list of the nodes associated with the passed pod names.
*/
func (cc *wcnspectBuilderCommon) getPodsNodes(podNames []string) (ret []v1.Node) {
for _, name := range podNames {
ret = append(ret, cc.podsNode[name])
func (cc *wcnspectBuilderCommon) getNode(nodeName string) (node v1.Node) {
if n, ok := cc.winNodeNames[nodeName]; ok {
node = n
} else {
log.Fatalf(fmt.Sprintf("Windows node %s not found", nodeName))
}
return
}

func (cc *wcnspectBuilderCommon) getPodNames() []string {
return comprise.Keys(cc.podNames)
}

func (cc *wcnspectBuilderCommon) getWinNodes() []v1.Node {
//FIXME: the below line should be commented out if not testing on local
// return []v1.Node{{ObjectMeta: metav1.ObjectMeta{Name: "localhost"}, Status: v1.NodeStatus{Addresses: []v1.NodeAddress{{Type: "InternalIP", Address: "0.0.0.0"}}}}}
Expand Down
4 changes: 2 additions & 2 deletions cmd/wcnspect/cmd/counter.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"sync"

"github.com/microsoft/wcnspect/pkg/client"
"github.com/microsoft/wcnspect/pkg/k8spi"
"github.com/microsoft/wcnspect/pkg/k8sapi"
pb "github.com/microsoft/wcnspect/rpc"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -58,7 +58,7 @@ func (cc *counterCmd) printCounters() {
for _, node := range targetNodes {
wg.Add(1)

name, ip := node.GetName(), k8spi.RetrieveInternalIP(node)
name, ip := node.GetName(), k8sapi.RetrieveInternalIP(node)

c, closeClient := client.CreateConnection(ip)
defer closeClient()
Expand Down
4 changes: 2 additions & 2 deletions cmd/wcnspect/cmd/hns.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"sync"

"github.com/microsoft/wcnspect/pkg/client"
"github.com/microsoft/wcnspect/pkg/k8spi"
"github.com/microsoft/wcnspect/pkg/k8sapi"
pb "github.com/microsoft/wcnspect/rpc"

"github.com/spf13/cobra"
Expand Down Expand Up @@ -74,7 +74,7 @@ func (cc *hnsCmd) printLogs(subcmd string) {
for _, node := range targetNodes {
wg.Add(1)

name, ip := node.GetName(), k8spi.RetrieveInternalIP(node)
name, ip := node.GetName(), k8sapi.RetrieveInternalIP(node)

c, closeClient := client.CreateConnection(ip)
defer closeClient()
Expand Down
Loading

0 comments on commit 9557d3a

Please sign in to comment.