diff --git a/.circleci/config.yml b/.circleci/config.yml index 1d8b3121..9142fc1e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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 diff --git a/README.md b/README.md index 6159ef9b..3ab6c48c 100644 --- a/README.md +++ b/README.md @@ -110,9 +110,6 @@ kube-capacity --pods --output json kube-capacity --pods --containers --util --output yaml ``` -## 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). - ## Flags Supported ``` -c, --containers includes containers in output @@ -131,6 +128,9 @@ Any commands requesting cluster utilization are dependent on [metrics-server](ht -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. diff --git a/pkg/capacity/capacity_test.go b/pkg/capacity/capacity_test.go index 10d627cc..2fd90a1e 100644 --- a/pkg/capacity/capacity_test.go +++ b/pkg/capacity/capacity_test.go @@ -15,189 +15,64 @@ package capacity import ( - "fmt" "testing" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" "k8s.io/client-go/kubernetes/fake" ) -func TestBuildClusterMetricEmpty(t *testing.T) { - cm := buildClusterMetric( - &corev1.PodList{}, &v1beta1.PodMetricsList{}, &corev1.NodeList{}, - ) - - expected := clusterMetric{ - cpu: &resourceMetric{ - resourceType: "cpu", - allocatable: resource.Quantity{}, - request: resource.Quantity{}, - limit: resource.Quantity{}, - utilization: resource.Quantity{}, - }, - memory: &resourceMetric{ - resourceType: "memory", - allocatable: resource.Quantity{}, - request: resource.Quantity{}, - limit: resource.Quantity{}, - utilization: resource.Quantity{}, - }, - nodeMetrics: map[string]*nodeMetric{}, - } - - assert.EqualValues(t, cm, expected) -} - -func TestBuildClusterMetricFull(t *testing.T) { - cm := buildClusterMetric( - &corev1.PodList{ - Items: []corev1.Pod{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "example-pod", - Namespace: "default", - }, - Spec: corev1.PodSpec{ - NodeName: "example-node-1", - Containers: []corev1.Container{ - { - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - "cpu": resource.MustParse("250m"), - "memory": resource.MustParse("250Mi"), - }, - Limits: corev1.ResourceList{ - "cpu": resource.MustParse("250m"), - "memory": resource.MustParse("500Mi"), - }, - }, - }, - { - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - "cpu": resource.MustParse("100m"), - "memory": resource.MustParse("150Mi"), - }, - Limits: corev1.ResourceList{ - "cpu": resource.MustParse("150m"), - "memory": resource.MustParse("200Mi"), - }, - }, - }, - }, - }, - }, - }, - }, &v1beta1.PodMetricsList{ - Items: []v1beta1.PodMetrics{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "example-pod", - Namespace: "default", - }, - Containers: []v1beta1.ContainerMetrics{ - { - Usage: corev1.ResourceList{ - "cpu": resource.MustParse("10m"), - "memory": resource.MustParse("188Mi"), - }, - }, - { - Usage: corev1.ResourceList{ - "cpu": resource.MustParse("13m"), - "memory": resource.MustParse("111Mi"), - }, - }, - }, - }, - }, - }, &corev1.NodeList{ - Items: []corev1.Node{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "example-node-1", - }, - Status: corev1.NodeStatus{ - Allocatable: corev1.ResourceList{ - "cpu": resource.MustParse("1000m"), - "memory": resource.MustParse("4000Mi"), - }, - }, - }, - }, - }, +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"}), ) - cpuExpected := &resourceMetric{ - allocatable: resource.MustParse("1000m"), - request: resource.MustParse("350m"), - limit: resource.MustParse("400m"), - utilization: resource.MustParse("23m"), - } - - memoryExpected := &resourceMetric{ - allocatable: resource.MustParse("4000Mi"), - request: resource.MustParse("400Mi"), - limit: resource.MustParse("700Mi"), - utilization: resource.MustParse("299Mi"), - } - - assert.NotNil(t, cm.cpu) - ensureEqualResourceMetric(t, cm.cpu, cpuExpected) - assert.NotNil(t, cm.memory) - ensureEqualResourceMetric(t, cm.memory, memoryExpected) - - assert.NotNil(t, cm.nodeMetrics["example-node-1"]) - assert.NotNil(t, cm.nodeMetrics["example-node-1"].cpu) - ensureEqualResourceMetric(t, cm.nodeMetrics["example-node-1"].cpu, cpuExpected) - assert.NotNil(t, cm.nodeMetrics["example-node-1"].memory) - ensureEqualResourceMetric(t, cm.nodeMetrics["example-node-1"].memory, memoryExpected) - - assert.Len(t, cm.nodeMetrics["example-node-1"].podMetrics, 1) - - pm := cm.nodeMetrics["example-node-1"].podMetrics - // Change to pod specific util numbers - cpuExpected.utilization = resource.MustParse("23m") - memoryExpected.utilization = resource.MustParse("299Mi") - - assert.NotNil(t, pm["default-example-pod"]) - assert.NotNil(t, pm["default-example-pod"].cpu) - ensureEqualResourceMetric(t, pm["default-example-pod"].cpu, cpuExpected) - assert.NotNil(t, pm["default-example-pod"].memory) - ensureEqualResourceMetric(t, pm["default-example-pod"].memory, memoryExpected) -} - -func ensureEqualResourceMetric(t *testing.T, actual *resourceMetric, expected *resourceMetric) { - assert.Equal(t, actual.allocatable.MilliValue(), expected.allocatable.MilliValue()) - assert.Equal(t, actual.utilization.MilliValue(), expected.utilization.MilliValue()) - assert.Equal(t, actual.request.MilliValue(), expected.request.MilliValue()) - assert.Equal(t, actual.limit.MilliValue(), expected.limit.MilliValue()) -} - -func listNodes(n *corev1.NodeList) []string { - nodes := []string{} - - for _, node := range n.Items { - nodes = append(nodes, node.GetName()) - } + 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)) - return nodes -} + 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)) -func listPods(p *corev1.PodList) []string { - pods := []string{} + 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)) - for _, pod := range p.Items { - pods = append(pods, fmt.Sprintf("%s/%s", pod.GetNamespace(), pod.GetName())) - } + podList, nodeList = getPodsAndNodes(clientset, "a=test", "", "") + assert.Equal(t, []string{"mynode", "mynode2"}, listNodes(nodeList)) + assert.Equal(t, []string{ + "default/mypod", + }, listPods(podList)) - return pods + 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 { @@ -242,53 +117,3 @@ func pod(node, namespace, name string, labels map[string]string) *corev1.Pod { }, } } - -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)) -} diff --git a/pkg/capacity/resources_test.go b/pkg/capacity/resources_test.go new file mode 100644 index 00000000..0b735f8b --- /dev/null +++ b/pkg/capacity/resources_test.go @@ -0,0 +1,199 @@ +// 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" + "testing" + + "github.com/stretchr/testify/assert" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func TestBuildClusterMetricEmpty(t *testing.T) { + cm := buildClusterMetric( + &corev1.PodList{}, &v1beta1.PodMetricsList{}, &corev1.NodeList{}, + ) + + expected := clusterMetric{ + cpu: &resourceMetric{ + resourceType: "cpu", + allocatable: resource.Quantity{}, + request: resource.Quantity{}, + limit: resource.Quantity{}, + utilization: resource.Quantity{}, + }, + memory: &resourceMetric{ + resourceType: "memory", + allocatable: resource.Quantity{}, + request: resource.Quantity{}, + limit: resource.Quantity{}, + utilization: resource.Quantity{}, + }, + nodeMetrics: map[string]*nodeMetric{}, + } + + assert.EqualValues(t, cm, expected) +} + +func TestBuildClusterMetricFull(t *testing.T) { + cm := buildClusterMetric( + &corev1.PodList{ + Items: []corev1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-pod", + Namespace: "default", + }, + Spec: corev1.PodSpec{ + NodeName: "example-node-1", + Containers: []corev1.Container{ + { + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + "cpu": resource.MustParse("250m"), + "memory": resource.MustParse("250Mi"), + }, + Limits: corev1.ResourceList{ + "cpu": resource.MustParse("250m"), + "memory": resource.MustParse("500Mi"), + }, + }, + }, + { + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + "cpu": resource.MustParse("100m"), + "memory": resource.MustParse("150Mi"), + }, + Limits: corev1.ResourceList{ + "cpu": resource.MustParse("150m"), + "memory": resource.MustParse("200Mi"), + }, + }, + }, + }, + }, + }, + }, + }, &v1beta1.PodMetricsList{ + Items: []v1beta1.PodMetrics{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-pod", + Namespace: "default", + }, + Containers: []v1beta1.ContainerMetrics{ + { + Usage: corev1.ResourceList{ + "cpu": resource.MustParse("10m"), + "memory": resource.MustParse("188Mi"), + }, + }, + { + Usage: corev1.ResourceList{ + "cpu": resource.MustParse("13m"), + "memory": resource.MustParse("111Mi"), + }, + }, + }, + }, + }, + }, &corev1.NodeList{ + Items: []corev1.Node{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "example-node-1", + }, + Status: corev1.NodeStatus{ + Allocatable: corev1.ResourceList{ + "cpu": resource.MustParse("1000m"), + "memory": resource.MustParse("4000Mi"), + }, + }, + }, + }, + }, + ) + + cpuExpected := &resourceMetric{ + allocatable: resource.MustParse("1000m"), + request: resource.MustParse("350m"), + limit: resource.MustParse("400m"), + utilization: resource.MustParse("23m"), + } + + memoryExpected := &resourceMetric{ + allocatable: resource.MustParse("4000Mi"), + request: resource.MustParse("400Mi"), + limit: resource.MustParse("700Mi"), + utilization: resource.MustParse("299Mi"), + } + + assert.NotNil(t, cm.cpu) + ensureEqualResourceMetric(t, cm.cpu, cpuExpected) + assert.NotNil(t, cm.memory) + ensureEqualResourceMetric(t, cm.memory, memoryExpected) + + assert.NotNil(t, cm.nodeMetrics["example-node-1"]) + assert.NotNil(t, cm.nodeMetrics["example-node-1"].cpu) + ensureEqualResourceMetric(t, cm.nodeMetrics["example-node-1"].cpu, cpuExpected) + assert.NotNil(t, cm.nodeMetrics["example-node-1"].memory) + ensureEqualResourceMetric(t, cm.nodeMetrics["example-node-1"].memory, memoryExpected) + + assert.Len(t, cm.nodeMetrics["example-node-1"].podMetrics, 1) + + pm := cm.nodeMetrics["example-node-1"].podMetrics + // Change to pod specific util numbers + cpuExpected.utilization = resource.MustParse("23m") + memoryExpected.utilization = resource.MustParse("299Mi") + + assert.NotNil(t, pm["default-example-pod"]) + assert.NotNil(t, pm["default-example-pod"].cpu) + ensureEqualResourceMetric(t, pm["default-example-pod"].cpu, cpuExpected) + assert.NotNil(t, pm["default-example-pod"].memory) + ensureEqualResourceMetric(t, pm["default-example-pod"].memory, memoryExpected) +} + +func ensureEqualResourceMetric(t *testing.T, actual *resourceMetric, expected *resourceMetric) { + assert.Equal(t, actual.allocatable.MilliValue(), expected.allocatable.MilliValue()) + assert.Equal(t, actual.utilization.MilliValue(), expected.utilization.MilliValue()) + assert.Equal(t, actual.request.MilliValue(), expected.request.MilliValue()) + assert.Equal(t, actual.limit.MilliValue(), expected.limit.MilliValue()) +} + +func listNodes(n *corev1.NodeList) []string { + nodes := []string{} + + for _, node := range n.Items { + nodes = append(nodes, node.GetName()) + } + + return nodes +} + +func listPods(p *corev1.PodList) []string { + pods := []string{} + + for _, pod := range p.Items { + pods = append(pods, fmt.Sprintf("%s/%s", pod.GetNamespace(), pod.GetName())) + } + + return pods +} diff --git a/pkg/capacity/table.go b/pkg/capacity/table.go index c26f4aa0..be5e8a6f 100644 --- a/pkg/capacity/table.go +++ b/pkg/capacity/table.go @@ -89,9 +89,15 @@ func (tp *tablePrinter) Print() { } func (tp *tablePrinter) printLine(tl *tableLine) { - lineItems := []string{tl.node, tl.namespace} + lineItems := tp.getLineItems(tl) + fmt.Fprintf(tp.w, strings.Join(lineItems[:], "\t ")+"\n") +} + +func (tp *tablePrinter) getLineItems(tl *tableLine) []string { + lineItems := []string{tl.node} if tp.showContainers || tp.showPods { + lineItems = append(lineItems, tl.namespace) lineItems = append(lineItems, tl.pod) } @@ -113,7 +119,7 @@ func (tp *tablePrinter) printLine(tl *tableLine) { lineItems = append(lineItems, tl.memoryUtil) } - fmt.Fprintf(tp.w, strings.Join(lineItems[:], "\t ")+"\n") + return lineItems } func (tp *tablePrinter) printClusterLine() { diff --git a/pkg/capacity/table_test.go b/pkg/capacity/table_test.go new file mode 100644 index 00000000..2e6760c2 --- /dev/null +++ b/pkg/capacity/table_test.go @@ -0,0 +1,112 @@ +// 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" +) + +func TestGetLineItems(t *testing.T) { + tpNone := &tablePrinter{ + showPods: false, + showUtil: false, + showContainers: false, + } + + tpSome := &tablePrinter{ + showPods: false, + showUtil: false, + showContainers: true, + } + + tpAll := &tablePrinter{ + showPods: true, + showUtil: true, + showContainers: true, + } + + tl := &tableLine{ + node: "example-node-1", + namespace: "example-namespace", + pod: "nginx-fsde", + container: "nginx", + cpuRequests: "100m", + cpuLimits: "200m", + cpuUtil: "14m", + memoryRequests: "1000Mi", + memoryLimits: "2000Mi", + memoryUtil: "326Mi", + } + + var testCases = []struct { + name string + tp *tablePrinter + tl *tableLine + expected []string + }{ + { + name: "all false", + tp: tpNone, + tl: tl, + expected: []string{ + "example-node-1", + "100m", + "200m", + "1000Mi", + "2000Mi", + }, + }, { + name: "some true", + tp: tpSome, + tl: tl, + expected: []string{ + "example-node-1", + "example-namespace", + "nginx-fsde", + "nginx", + "100m", + "200m", + "1000Mi", + "2000Mi", + }, + }, { + name: "all true", + tp: tpAll, + tl: tl, + expected: []string{ + "example-node-1", + "example-namespace", + "nginx-fsde", + "nginx", + "100m", + "200m", + "14m", + "1000Mi", + "2000Mi", + "326Mi", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + lineItems := tc.tp.getLineItems(tl) + assert.Len(t, lineItems, len(tc.expected)) + assert.ElementsMatch(t, lineItems, tc.expected) + }) + } +} diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go index 0480b214..09844b04 100644 --- a/pkg/cmd/version.go +++ b/pkg/cmd/version.go @@ -28,6 +28,6 @@ var versionCmd = &cobra.Command{ Use: "version", Short: "Print the version number of kube-capacity", Run: func(cmd *cobra.Command, args []string) { - fmt.Println("kube-capacity version 0.2.0") + fmt.Println("kube-capacity version 0.3.0") }, }