Skip to content

Commit

Permalink
Merge pull request #13 from robscott/rs/containers
Browse files Browse the repository at this point in the history
Adding Container Level Output, YAML Formatting, and Sorting
  • Loading branch information
robscott authored Apr 5, 2019
2 parents 82b3974 + e04d6be commit 311014e
Show file tree
Hide file tree
Showing 17 changed files with 1,278 additions and 612 deletions.
7 changes: 5 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ jobs:
working_directory: /go/src/github.com/robscott/kube-capacity

docker:
- image: circleci/golang:1.11
- image: circleci/golang:1.12

steps:
- checkout
- run: go test -v ./pkg/...
- run: go get -u golang.org/x/lint/golint
- run: go list ./... | grep -v vendor | xargs golint -set_exit_status
- run: go list ./... | grep -v vendor | xargs go vet
- run: go test ./pkg/... -v -coverprofile cover.out

workflows:
version: 2
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2019 Rob Scott
Copyright 2019 Kube Capacity Authors

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ example-node-2 tiller tiller-deploy 140m (14%) 180m (18%)

It's worth noting that utilization numbers from pods will likely not add up to the total node utilization numbers. Unlike request and limit numbers where node and cluster level numbers represent a sum of pod values, node metrics come directly from metrics-server and will likely include other forms of resource utilization.

### Sorting
To highlight the nodes, pods, and containers with the highest metrics, you can sort by a variety of columns:

```
kube-capacity --util --sort cpu.util
NODE CPU REQUESTS CPU LIMITS CPU UTIL MEMORY REQUESTS MEMORY LIMITS MEMORY UTIL
* 560m (28%) 130m (7%) 40m (2%) 572Mi (9%) 770Mi (13%) 470Mi (8%)
example-node-2 340m (34%) 120m (12%) 30m (3%) 380Mi (13%) 410Mi (14%) 260Mi (9%)
example-node-1 220m (22%) 10m (1%) 10m (1%) 192Mi (6%) 360Mi (12%) 210Mi (7%)
```

### Filtering By Labels
For more advanced usage, kube-capacity also supports filtering by pod, namespace, and/or node labels. The following examples show how to use these filters:

Expand All @@ -91,25 +103,46 @@ kube-capacity --namespace-labels team=api
kube-capacity --node-labels kubernetes.io/role=node
```

## Prerequisites
Any commands requesting cluster utilization are dependent on [metrics-server](https://github.com/kubernetes-incubator/metrics-server) running on your cluster. If it's not already installed, you can install it with the official [helm chart](https://github.com/helm/charts/tree/master/stable/metrics-server).
### JSON and YAML Output
By default, kube-capacity will provide output in a table format. To view this data in JSON or YAML format, the output flag can be used. Here are some sample commands:
```
kube-capacity --pods --output json
kube-capacity --pods --containers --util --output yaml
```

## Flags Supported
```
-c, --containers includes containers in output
--context string context to use for Kubernetes config
-h, --help help for kube-capacity
-n, --namespace-labels string labels to filter namespaces with
--node-labels string labels to filter nodes with
-o, --output string output format for information
(supports: [table json yaml])
(default "table")
-l, --pod-labels string labels to filter pods with
-p, --pods includes pods in output
--sort string attribute to sort results be (supports:
[cpu.util cpu.request cpu.limit mem.util mem.request mem.limit name])
(default "name")
-u, --util includes resource utilization in output
```

## Prerequisites
Any commands requesting cluster utilization are dependent on [metrics-server](https://github.com/kubernetes-incubator/metrics-server) running on your cluster. If it's not already installed, you can install it with the official [helm chart](https://github.com/helm/charts/tree/master/stable/metrics-server).

## Similar Projects
There are already some great projects out there that have similar goals.

- [kube-resource-report](https://github.com/hjacobs/kube-resource-report): generates HTML/CSS report for resource requests and limits across multiple clusters.
- [kubetop](https://github.com/LeastAuthority/kubetop): a CLI similar to top for Kubernetes, focused on resource utilization (not requests and limits).

## Contributors

Although this project was originally developed by [robscott](https://github.com/robscott), there have been some great contributions from others:

- [endzyme](https://github.com/endzyme)
- [justinbarrick](https://github.com/justinbarrick)

## License
Apache License 2.0
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2019 Rob Scott
// Copyright 2019 Kube Capacity Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down
127 changes: 127 additions & 0 deletions pkg/capacity/capacity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2019 Kube Capacity Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package capacity

import (
"fmt"
"os"

"k8s.io/client-go/kubernetes"
metrics "k8s.io/metrics/pkg/client/clientset/versioned"

"github.com/robscott/kube-capacity/pkg/kube"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1"
)

// FetchAndPrint gathers cluster resource data and outputs it
func FetchAndPrint(showContainers, showPods, showUtil bool, podLabels, nodeLabels, namespaceLabels, kubeContext, output, sortBy string) {
clientset, err := kube.NewClientSet(kubeContext)
if err != nil {
fmt.Printf("Error connecting to Kubernetes: %v\n", err)
os.Exit(1)
}

podList, nodeList := getPodsAndNodes(clientset, podLabels, nodeLabels, namespaceLabels)
pmList := &v1beta1.PodMetricsList{}
if showUtil {
mClientset, err := kube.NewMetricsClientSet(kubeContext)
if err != nil {
fmt.Printf("Error connecting to Metrics API: %v\n", err)
os.Exit(4)
}

pmList = getMetrics(mClientset)
}

cm := buildClusterMetric(podList, pmList, nodeList)
printList(&cm, showContainers, showPods, showUtil, output, sortBy)
}

func getPodsAndNodes(clientset kubernetes.Interface, podLabels, nodeLabels, namespaceLabels string) (*corev1.PodList, *corev1.NodeList) {
nodeList, err := clientset.CoreV1().Nodes().List(metav1.ListOptions{
LabelSelector: nodeLabels,
})
if err != nil {
fmt.Printf("Error listing Nodes: %v\n", err)
os.Exit(2)
}

podList, err := clientset.CoreV1().Pods("").List(metav1.ListOptions{
LabelSelector: podLabels,
})
if err != nil {
fmt.Printf("Error listing Pods: %v\n", err)
os.Exit(3)
}

newPodItems := []corev1.Pod{}

nodes := map[string]bool{}
for _, node := range nodeList.Items {
nodes[node.GetName()] = true
}

for _, pod := range podList.Items {
if !nodes[pod.Spec.NodeName] {
continue
}

newPodItems = append(newPodItems, pod)
}

podList.Items = newPodItems

if namespaceLabels != "" {
namespaceList, err := clientset.CoreV1().Namespaces().List(metav1.ListOptions{
LabelSelector: namespaceLabels,
})
if err != nil {
fmt.Printf("Error listing Namespaces: %v\n", err)
os.Exit(3)
}

namespaces := map[string]bool{}
for _, ns := range namespaceList.Items {
namespaces[ns.GetName()] = true
}

newPodItems := []corev1.Pod{}

for _, pod := range podList.Items {
if !namespaces[pod.GetNamespace()] {
continue
}

newPodItems = append(newPodItems, pod)
}

podList.Items = newPodItems
}

return podList, nodeList
}

func getMetrics(mClientset *metrics.Clientset) *v1beta1.PodMetricsList {
pmList, err := mClientset.MetricsV1beta1().PodMetricses("").List(metav1.ListOptions{})
if err != nil {
fmt.Printf("Error getting Pod Metrics: %v\n", err)
fmt.Println("For this to work, metrics-server needs to be running in your cluster")
os.Exit(6)
}

return pmList
}
119 changes: 119 additions & 0 deletions pkg/capacity/capacity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Copyright 2019 Kube Capacity Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package capacity

import (
"testing"

"github.com/stretchr/testify/assert"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"k8s.io/client-go/kubernetes/fake"
)

func TestGetPodsAndNodes(t *testing.T) {
clientset := fake.NewSimpleClientset(
node("mynode", map[string]string{"hello": "world"}),
node("mynode2", map[string]string{"hello": "world", "moon": "lol"}),
namespace("default", map[string]string{"app": "true"}),
namespace("kube-system", map[string]string{"system": "true"}),
namespace("other", map[string]string{"app": "true", "system": "true"}),
namespace("another", map[string]string{"hello": "world"}),
pod("mynode", "default", "mypod", map[string]string{"a": "test"}),
pod("mynode2", "kube-system", "mypod1", map[string]string{"b": "test"}),
pod("mynode", "other", "mypod2", map[string]string{"c": "test"}),
pod("mynode2", "other", "mypod3", map[string]string{"d": "test"}),
pod("mynode2", "default", "mypod4", map[string]string{"e": "test"}),
pod("mynode", "another", "mypod5", map[string]string{"f": "test"}),
pod("mynode", "default", "mypod6", map[string]string{"g": "test"}),
)

podList, nodeList := getPodsAndNodes(clientset, "", "", "")
assert.Equal(t, []string{"mynode", "mynode2"}, listNodes(nodeList))
assert.Equal(t, []string{
"default/mypod", "kube-system/mypod1", "other/mypod2", "other/mypod3", "default/mypod4",
"another/mypod5", "default/mypod6",
}, listPods(podList))

podList, nodeList = getPodsAndNodes(clientset, "", "hello=world", "")
assert.Equal(t, []string{"mynode", "mynode2"}, listNodes(nodeList))
assert.Equal(t, []string{
"default/mypod", "kube-system/mypod1", "other/mypod2", "other/mypod3", "default/mypod4",
"another/mypod5", "default/mypod6",
}, listPods(podList))

podList, nodeList = getPodsAndNodes(clientset, "", "moon=lol", "")
assert.Equal(t, []string{"mynode2"}, listNodes(nodeList))
assert.Equal(t, []string{
"kube-system/mypod1", "other/mypod3", "default/mypod4",
}, listPods(podList))

podList, nodeList = getPodsAndNodes(clientset, "a=test", "", "")
assert.Equal(t, []string{"mynode", "mynode2"}, listNodes(nodeList))
assert.Equal(t, []string{
"default/mypod",
}, listPods(podList))

podList, nodeList = getPodsAndNodes(clientset, "a=test,b!=test", "", "app=true")
assert.Equal(t, []string{"mynode", "mynode2"}, listNodes(nodeList))
assert.Equal(t, []string{
"default/mypod",
}, listPods(podList))
}

func node(name string, labels map[string]string) *corev1.Node {
return &corev1.Node{
TypeMeta: metav1.TypeMeta{
Kind: "Node",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: labels,
},
}
}

func namespace(name string, labels map[string]string) *corev1.Namespace {
return &corev1.Namespace{
TypeMeta: metav1.TypeMeta{
Kind: "Namespace",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: labels,
},
}
}

func pod(node, namespace, name string, labels map[string]string) *corev1.Pod {
return &corev1.Pod{
TypeMeta: metav1.TypeMeta{
Kind: "Pod",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Labels: labels,
},
Spec: corev1.PodSpec{
NodeName: node,
},
}
}
Loading

0 comments on commit 311014e

Please sign in to comment.