├── pkg ├── util │ ├── validation.go │ ├── validation_test.go │ ├── util.go │ └── util_test.go ├── table │ ├── table.go │ └── table_test.go ├── constants │ └── constants.go └── cmd │ ├── showpods_test.go │ ├── showpods.go │ ├── showfree.go │ ├── showfree_test.go │ ├── root.go │ └── root_test.go ├── .gitignore ├── .travis.yml ├── .goreleaser.yml ├── Makefile ├── cmd └── kubectl-free │ └── kubectl-free.go ├── LICENSE ├── go.mod ├── README.md └── go.sum /pkg/util/validation.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func ValidateThreshold(w, c int64) error { 8 | if w > c { 9 | return fmt.Errorf( 10 | "can not set critical threshold less than warn threshold (warn:%d crit:%d)", w, c, 11 | ) 12 | } 13 | 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | /kubectl-dfi 15 | _output/ 16 | coverage.txt 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.12.7 5 | 6 | env: 7 | - GO111MODULE=on 8 | 9 | install: true 10 | 11 | before_script: 12 | - go install github.com/golangci/golangci-lint/cmd/golangci-lint 13 | 14 | script: 15 | - golangci-lint run -E stylecheck -E gocritic 16 | - diff -u <(echo -n) <(gofmt -d .) 17 | - go test -v -race -coverprofile coverage.txt $(go list ./... | grep -v /constants | grep -v /cmd/kubectl-free) 18 | 19 | after_script: 20 | - bash <(curl -s https://codecov.io/bash) 21 | 22 | deploy: 23 | - provider: script 24 | skip_cleanup: true 25 | script: curl -sL https://git.io/goreleaser | bash 26 | on: 27 | tags: true 28 | 29 | -------------------------------------------------------------------------------- /pkg/util/validation_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestValidateThreshold(t *testing.T) { 10 | 11 | var tests = []struct { 12 | description string 13 | warn int64 14 | crit int64 15 | expected error 16 | }{ 17 | {"warn:0 crit:0", 0, 0, nil}, 18 | {"warn:10 crit:20", 10, 20, nil}, 19 | {"warn:20 crit:10", 20, 10, fmt.Errorf("can not set critical threshold less than warn threshold (warn:20 crit:10)")}, 20 | } 21 | 22 | for _, test := range tests { 23 | 24 | t.Run(test.description, func(t *testing.T) { 25 | actual := ValidateThreshold(test.warn, test.crit) 26 | if !reflect.DeepEqual(actual, test.expected) { 27 | t.Errorf("expected(%v) differ (got: %v)", test.expected, actual) 28 | } 29 | }) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | --- 2 | env: 3 | - GO111MODULE=on 4 | before: 5 | hooks: 6 | - go mod download 7 | builds: 8 | - 9 | env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - linux 13 | - darwin 14 | - windows 15 | goarch: 16 | - amd64 17 | main: cmd/kubectl-free/kubectl-free.go 18 | binary: kubectl-free 19 | archives: 20 | - 21 | name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 22 | replacements: 23 | darwin: Darwin 24 | linux: Linux 25 | windows: Windows 26 | amd64: x86_64 27 | wrap_in_directory: true 28 | format: zip 29 | files: 30 | - LICENSE 31 | - README.md 32 | checksum: 33 | name_template: "{{ .ProjectName }}_checksums.txt" 34 | algorithm: sha256 35 | changelog: 36 | sort: asc 37 | filters: 38 | exclude: 39 | - '^docs:' 40 | - '^test:' 41 | -------------------------------------------------------------------------------- /pkg/table/table.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/makocchi-git/kubectl-free/pkg/util" 8 | 9 | "k8s.io/kubernetes/pkg/printers" 10 | ) 11 | 12 | // OutputTable is struct of tables for outputs 13 | type OutputTable struct { 14 | Header []string 15 | Rows []string 16 | Output io.Writer 17 | } 18 | 19 | // NewOutputTable is an instance of OutputTable 20 | func NewOutputTable(o io.Writer) *OutputTable { 21 | return &OutputTable{ 22 | Output: o, 23 | } 24 | } 25 | 26 | // Print shows table output 27 | func (t *OutputTable) Print() { 28 | 29 | // get printer 30 | printer := printers.GetNewTabWriter(t.Output) 31 | 32 | // write header 33 | if len(t.Header) > 0 { 34 | fmt.Fprintln(printer, util.JoinTab(t.Header)) 35 | } 36 | 37 | // write rows 38 | for _, row := range t.Rows { 39 | fmt.Fprintln(printer, row) 40 | } 41 | 42 | // finish 43 | printer.Flush() 44 | } 45 | 46 | // AddRow adds row to table 47 | func (t *OutputTable) AddRow(s []string) { 48 | t.Rows = append(t.Rows, util.JoinTab(s)) 49 | } 50 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash -o pipefail 2 | 3 | GO ?= go 4 | GOLINT ?= golangci-lint 5 | 6 | COMMIT_HASH := $(shell git rev-parse --short HEAD 2> /dev/null || true) 7 | GIT_TAG := $(shell git describe --tags --dirty --always) 8 | 9 | LDFLAGS := -ldflags '-X main.commit=${COMMIT_HASH} -X main.date=$(shell date +%s) -X main.version=${GIT_TAG}' 10 | TESTPACKAGES := $(shell go list ./... | grep -v /constants | grep -v /cmd/) 11 | 12 | kubectl_free ?= _output/kubectl-free 13 | 14 | .PHONY: build 15 | build: clean ${kubectl_free} 16 | 17 | ${kubectl_free}: 18 | GO111MODULE=on CGO_ENABLED=0 $(GO) build ${LDFLAGS} -o $@ ./cmd/kubectl-free/kubectl-free.go 19 | 20 | .PHONY: clean 21 | clean: 22 | rm -Rf _output 23 | 24 | .PHONY: test 25 | test: 26 | GO111MODULE=on $(GO) test -count=1 -v -race $(TESTPACKAGES) 27 | 28 | .PHONY: lint-install 29 | lint-install: 30 | GO111MODULE=on ${GO} install github.com/golangci/golangci-lint/cmd/golangci-lint 31 | 32 | .PHONY: lint 33 | lint: 34 | GO111MODULE=on ${GOLINT} run -E stylecheck -E gocritic 35 | 36 | .PHONY: fmt 37 | fmt: 38 | ${GO} fmt ./cmd/... ./pkg/... 39 | -------------------------------------------------------------------------------- /cmd/kubectl-free/kubectl-free.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/makocchi-git/kubectl-free/pkg/cmd" 7 | "github.com/spf13/pflag" 8 | 9 | "k8s.io/cli-runtime/pkg/genericclioptions" 10 | cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" 11 | ) 12 | 13 | var ( 14 | // for goreleaser 15 | // https://goreleaser.com/customization/#Builds 16 | version = "master" 17 | commit = "unknown" 18 | date = "unknown" 19 | ) 20 | 21 | func main() { 22 | flags := pflag.NewFlagSet("kubectl-free", pflag.ExitOnError) 23 | pflag.CommandLine = flags 24 | 25 | kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() 26 | kubeConfigFlags.AddFlags(flags) 27 | matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags) 28 | 29 | f := cmdutil.NewFactory(matchVersionKubeConfigFlags) 30 | 31 | root := cmd.NewCmdFree( 32 | f, 33 | genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}, 34 | version, 35 | commit, 36 | date, 37 | ) 38 | if err := root.Execute(); err != nil { 39 | os.Exit(1) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 makocchi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/makocchi-git/kubectl-free 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect 7 | github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e // indirect 8 | github.com/docker/docker v0.0.0-00010101000000-000000000000 // indirect 9 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect 10 | github.com/fatih/camelcase v1.0.0 // indirect 11 | github.com/golangci/golangci-lint v0.0.0-00010101000000-000000000000 // indirect 12 | github.com/gookit/color v1.1.7 13 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 14 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect 15 | github.com/russross/blackfriday v0.0.0-00010101000000-000000000000 // indirect 16 | github.com/spf13/cobra v0.0.2 17 | github.com/spf13/pflag v1.0.3 18 | gotest.tools v2.2.0+incompatible // indirect 19 | k8s.io/api v0.0.0-20190726022912-69e1bce1dad5 20 | k8s.io/apimachinery v0.0.0-20190726022757-641a75999153 21 | k8s.io/cli-runtime v0.0.0-20190531135611-d60f41fb4dc3 22 | k8s.io/client-go v11.0.0+incompatible 23 | k8s.io/kubernetes v1.14.3 24 | k8s.io/metrics v0.0.0-20190726024513-9140f5fe6ab8 25 | ) 26 | 27 | replace ( 28 | github.com/Sirupsen/logrus => github.com/sirupsen/logrus v1.4.2 29 | github.com/docker/docker => github.com/moby/moby v0.7.3-0.20190607191414-238f8eaa31aa 30 | github.com/golangci/golangci-lint => github.com/golangci/golangci-lint v1.17.1 31 | github.com/russross/blackfriday => github.com/russross/blackfriday v1.5.2 32 | k8s.io/client-go => k8s.io/client-go v0.0.0-20190531132438-d58e65e5f4b1 33 | k8s.io/kubernetes => k8s.io/kubernetes v1.14.2 34 | ) 35 | -------------------------------------------------------------------------------- /pkg/table/table_test.go: -------------------------------------------------------------------------------- 1 | package table 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestNewOutputTable(t *testing.T) { 11 | expected := &OutputTable{ 12 | Output: os.Stdout, 13 | } 14 | 15 | actual := NewOutputTable(os.Stdout) 16 | 17 | if !reflect.DeepEqual(actual, expected) { 18 | t.Errorf("expected(%#v) differ (got: %#v)", expected, actual) 19 | return 20 | } 21 | 22 | } 23 | 24 | func TestPrint(t *testing.T) { 25 | 26 | buffer := &bytes.Buffer{} 27 | 28 | var tests = []struct { 29 | description string 30 | rows []string 31 | expected string 32 | }{ 33 | {"1 row", []string{"1\t2"}, "a b\n1 2\n"}, 34 | {"2 rows", []string{"1\t2", "3\t4"}, "a b\n1 2\n3 4\n"}, 35 | } 36 | 37 | for _, test := range tests { 38 | 39 | buffer.Reset() 40 | 41 | t.Run(test.description, func(t *testing.T) { 42 | table := &OutputTable{ 43 | Header: []string{"a", "b"}, 44 | Rows: test.rows, 45 | Output: buffer, 46 | } 47 | 48 | table.Print() 49 | 50 | if buffer.String() != test.expected { 51 | t.Errorf( 52 | "[%s] expected(%s) differ (got: %s)", 53 | test.description, 54 | test.expected, 55 | buffer.String(), 56 | ) 57 | return 58 | } 59 | }) 60 | } 61 | } 62 | 63 | func TestAddRow(t *testing.T) { 64 | table := &OutputTable{} 65 | table.AddRow([]string{"1", "2", "3"}) 66 | table.AddRow([]string{"4", "5", "6"}) 67 | 68 | rows := table.Rows 69 | 70 | if rows[0] != "1\t2\t3" { 71 | t.Errorf("expected(1\t2\t3) differ (got: %s)", rows[0]) 72 | return 73 | } 74 | if rows[1] != "4\t5\t6" { 75 | t.Errorf("expected(4\t5\t6) differ (got: %s)", rows[1]) 76 | return 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /pkg/constants/constants.go: -------------------------------------------------------------------------------- 1 | // Package constants has various definitions 2 | package constants 3 | 4 | const ( 5 | 6 | // UnitBytes is bytes for "Bytes" output 7 | UnitBytes = 1 8 | 9 | // 10 | // International System of Units (SI prefix) 11 | // 12 | 13 | // UnitKiloBytes is bytes for "KiBytes" output 14 | UnitKiloBytes = UnitBytes * 1000 15 | 16 | // UnitMegaBytes is bytes for "MiBytes" output 17 | UnitMegaBytes = UnitKiloBytes * 1000 18 | 19 | // UnitGigaBytes is bytes for "GiBytes" output 20 | UnitGigaBytes = UnitMegaBytes * 1000 21 | 22 | // UnitBytesStr is unit string for bytes 23 | UnitBytesStr = "B" 24 | 25 | // UnitKiloBytesStr is unit string for kilobytes 26 | UnitKiloBytesStr = "K" 27 | 28 | // UnitMegaBytesStr is unit string for megabytes 29 | UnitMegaBytesStr = "M" 30 | 31 | // UnitGigaBytesStr is unit string for gigabytes 32 | UnitGigaBytesStr = "G" 33 | 34 | // 35 | // Binary prefix 36 | // 37 | 38 | // UnitKibiBytes is bytes for "KiBytes" output 39 | UnitKibiBytes = UnitBytes * 1024 40 | 41 | // UnitMibiBytes is bytes for "MiBytes" output 42 | UnitMibiBytes = UnitKiloBytes * 1024 43 | 44 | // UnitGibiBytes is bytes for "GiBytes" output 45 | UnitGibiBytes = UnitMegaBytes * 1024 46 | 47 | // UnitKibiBytesStr is unit string for kilobytes 48 | UnitKibiBytesStr = "Ki" 49 | 50 | // UnitMibiBytesStr is unit string for megabytes 51 | UnitMibiBytesStr = "Mi" 52 | 53 | // UnitGibiBytesStr is unit string for gigabytes 54 | UnitGibiBytesStr = "Gi" 55 | 56 | // 57 | // Emoji 58 | // 59 | 60 | // EmojiReady is smile emoji for node status 61 | EmojiReady = "😃" 62 | 63 | // EmojiNotReady is crying emoji for node status 64 | EmojiNotReady = "😭" 65 | 66 | // EmojiPodRunning is running emoji for pod status 67 | EmojiPodRunning = "✅" 68 | 69 | // EmojiPodSucceeded is succeeded emoji for pod status 70 | EmojiPodSucceeded = "⭕" 71 | 72 | // EmojiPodPending is pending emoji for pod status 73 | EmojiPodPending = "🚫" 74 | 75 | // EmojiPodFailed is faled emoji for pod status 76 | EmojiPodFailed = "❌" 77 | 78 | // EmojiPodUnknown is unknown emoji for pod status 79 | EmojiPodUnknown = "❓" 80 | ) 81 | -------------------------------------------------------------------------------- /pkg/cmd/showpods_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/makocchi-git/kubectl-free/pkg/table" 9 | 10 | v1 "k8s.io/api/core/v1" 11 | fake "k8s.io/client-go/kubernetes/fake" 12 | fakemetrics "k8s.io/metrics/pkg/client/clientset/versioned/fake" 13 | ) 14 | 15 | func TestShowPodsOnNode(t *testing.T) { 16 | 17 | var tests = []struct { 18 | description string 19 | listContainer bool 20 | listAll bool 21 | nometrics bool 22 | expected []string 23 | }{ 24 | { 25 | "list container with metrics", 26 | false, 27 | false, 28 | false, 29 | []string{ 30 | "node2 default pod2 2.3.4.5 Running container2a - 500m 500m - 1K 1K", 31 | "", 32 | }, 33 | }, 34 | { 35 | "list container: false, list all: false", 36 | false, 37 | false, 38 | true, 39 | []string{ 40 | "node2 default pod2 2.3.4.5 Running container2a 500m 500m 1K 1K", 41 | "", 42 | }, 43 | }, 44 | { 45 | "list container: true, list all: false", 46 | true, 47 | false, 48 | true, 49 | []string{ 50 | "node2 default pod2 2.3.4.5 Running container2a 500m 500m 1K 1K nginx:latest", 51 | "", 52 | }, 53 | }, 54 | { 55 | "list container: false, list all: true", 56 | false, 57 | true, 58 | true, 59 | []string{ 60 | "node2 default pod2 2.3.4.5 Running container2a 500m 500m 1K 1K", 61 | "node2 default pod2 2.3.4.5 Running container2b - - - -", 62 | "", 63 | }, 64 | }, 65 | { 66 | "list container: true, list all: true", 67 | true, 68 | true, 69 | true, 70 | []string{ 71 | "node2 default pod2 2.3.4.5 Running container2a 500m 500m 1K 1K nginx:latest", 72 | "node2 default pod2 2.3.4.5 Running container2b - - - - busybox:latest", 73 | "", 74 | }, 75 | }, 76 | } 77 | 78 | for _, test := range tests { 79 | t.Run(test.description, func(t *testing.T) { 80 | buffer := &bytes.Buffer{} 81 | fakeClient := fake.NewSimpleClientset(&testPods[1]) 82 | fakeMetricsNodeClient := fakemetrics.NewSimpleClientset(&testNodeMetrics.Items[0]) 83 | fakeMetricsPodClient := fakemetrics.NewSimpleClientset(testPodMetrics) 84 | 85 | o := &FreeOptions{ 86 | table: table.NewOutputTable(buffer), 87 | noHeaders: true, 88 | noMetrics: test.nometrics, 89 | nocolor: true, 90 | listContainerImage: test.listContainer, 91 | listAll: test.listAll, 92 | podClient: fakeClient.CoreV1().Pods(""), 93 | metricsPodClient: fakeMetricsPodClient.MetricsV1beta1().PodMetricses("default"), 94 | metricsNodeClient: fakeMetricsNodeClient.MetricsV1beta1().NodeMetricses(), 95 | } 96 | 97 | if err := o.showPodsOnNode([]v1.Node{testNodes[1]}); err != nil { 98 | t.Errorf("unexpected error: %v", err) 99 | return 100 | } 101 | 102 | expected := strings.Join(test.expected, "\n") 103 | actual := buffer.String() 104 | if actual != expected { 105 | t.Errorf("[%s] expected(%s) differ (got: %s)", test.description, expected, actual) 106 | return 107 | } 108 | }) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /pkg/cmd/showpods.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/makocchi-git/kubectl-free/pkg/util" 7 | 8 | v1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/util/duration" 11 | metricsapiv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" 12 | ) 13 | 14 | func (o *FreeOptions) showPodsOnNode(nodes []v1.Node) error { 15 | 16 | // set table header 17 | if !o.noHeaders { 18 | o.table.Header = o.listTableHeaders 19 | } 20 | 21 | // get pod metrics 22 | var podMetrics *metricsapiv1beta1.PodMetricsList 23 | if !o.noMetrics && o.metricsPodClient != nil { 24 | podMetrics, _ = o.metricsPodClient.List(metav1.ListOptions{}) 25 | } 26 | 27 | // node loop 28 | for _, node := range nodes { 29 | 30 | // node name 31 | nodeName := node.ObjectMeta.Name 32 | 33 | // get pods on node 34 | pods, perr := util.GetPods(o.podClient, nodeName) 35 | if perr != nil { 36 | return perr 37 | } 38 | 39 | // node loop 40 | for _, pod := range pods.Items { 41 | 42 | var containerCPUUsed int64 43 | var containerMEMUsed int64 44 | 45 | // pod information 46 | podName := pod.ObjectMeta.Name 47 | podNamespace := pod.ObjectMeta.Namespace 48 | podIP := pod.Status.PodIP 49 | podStatus := util.GetPodStatus(string(pod.Status.Phase), o.nocolor, o.emojiStatus) 50 | podCreationTime := pod.ObjectMeta.CreationTimestamp.UTC() 51 | podCreationTimeDiff := time.Since(podCreationTime) 52 | podAge := "" 53 | if !podCreationTime.IsZero() { 54 | podAge = duration.HumanDuration(podCreationTimeDiff) 55 | } 56 | 57 | // container loop 58 | for _, container := range pod.Spec.Containers { 59 | containerName := container.Name 60 | containerImage := container.Image 61 | cCpuRequested := container.Resources.Requests.Cpu().MilliValue() 62 | cCpuLimit := container.Resources.Limits.Cpu().MilliValue() 63 | cMemRequested := container.Resources.Requests.Memory().Value() 64 | cMemLimit := container.Resources.Limits.Memory().Value() 65 | 66 | if !o.noMetrics && podMetrics != nil { 67 | containerCPUUsed, containerMEMUsed = util.GetContainerMetrics(podMetrics, podName, containerName) 68 | } 69 | 70 | // skip if the requested/limit resources are not set 71 | if !o.listAll { 72 | if cCpuRequested == 0 && cCpuLimit == 0 && cMemRequested == 0 && cMemLimit == 0 { 73 | continue 74 | } 75 | } 76 | 77 | row := []string{ 78 | nodeName, // node name 79 | podNamespace, // namespace 80 | podName, // pod name 81 | podAge, // pod age 82 | podIP, // pod ip 83 | podStatus, // pod status 84 | containerName, // container name 85 | } 86 | 87 | if !o.noMetrics { 88 | row = append(row, o.toMilliUnitOrDash(containerCPUUsed)) 89 | } 90 | 91 | row = append( 92 | row, 93 | o.toMilliUnitOrDash(cCpuRequested), // container CPU requested 94 | o.toMilliUnitOrDash(cCpuLimit), // container CPU limit 95 | ) 96 | 97 | if !o.noMetrics { 98 | row = append(row, o.toUnitOrDash(containerMEMUsed)) 99 | } 100 | 101 | row = append( 102 | row, 103 | o.toUnitOrDash(cMemRequested), // Memory requested 104 | o.toUnitOrDash(cMemLimit), // Memory limit 105 | ) 106 | 107 | if o.listContainerImage { 108 | row = append(row, containerImage) 109 | } 110 | 111 | o.table.AddRow(row) 112 | } 113 | } 114 | } 115 | o.table.Print() 116 | 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /pkg/cmd/showfree.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/makocchi-git/kubectl-free/pkg/util" 8 | 9 | v1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | ) 12 | 13 | // showFree prints requested and allocatable resources 14 | func (o *FreeOptions) showFree(nodes []v1.Node) error { 15 | 16 | // set table header 17 | if !o.noHeaders { 18 | o.table.Header = o.freeTableHeaders 19 | } 20 | 21 | // node loop 22 | for _, node := range nodes { 23 | 24 | var ( 25 | cpuMetricsUsed int64 26 | memMetricsUsed int64 27 | cpuMetricsUsedP int64 28 | memMetricsUsedP int64 29 | ) 30 | 31 | // node name 32 | nodeName := node.ObjectMeta.Name 33 | 34 | // node status 35 | nodeStatus, err := util.GetNodeStatus(node, o.emojiStatus) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | util.SetNodeStatusColor(&nodeStatus, o.nocolor) 41 | 42 | // get pods on node 43 | pods, perr := util.GetPods(o.podClient, nodeName) 44 | if perr != nil { 45 | return perr 46 | } 47 | 48 | // calculate requested resources by pods 49 | cpuRequested, memRequested, cpuLimited, memLimited := util.GetPodResources(*pods) 50 | 51 | // get cpu allocatable 52 | cpuAllocatable := node.Status.Allocatable.Cpu().MilliValue() 53 | 54 | // get memoly allocatable 55 | memAllocatable := node.Status.Allocatable.Memory().Value() 56 | 57 | // get usage 58 | cpuRequestedP := util.GetPercentage(cpuRequested, cpuAllocatable) 59 | cpuLimitedP := util.GetPercentage(cpuLimited, cpuAllocatable) 60 | memRequestedP := util.GetPercentage(memRequested, memAllocatable) 61 | memLimitedP := util.GetPercentage(memLimited, memAllocatable) 62 | 63 | // get metrics 64 | if !o.noMetrics && o.metricsNodeClient != nil { 65 | nodeMetrics, err := o.metricsNodeClient.Get(nodeName, metav1.GetOptions{}) 66 | if err == nil { 67 | cpuMetricsUsed = nodeMetrics.Usage.Cpu().MilliValue() 68 | memMetricsUsed = nodeMetrics.Usage.Memory().Value() 69 | cpuMetricsUsedP = util.GetPercentage(cpuMetricsUsed, cpuAllocatable) 70 | memMetricsUsedP = util.GetPercentage(memMetricsUsed, memAllocatable) 71 | } 72 | // ignore fetching metrics error 73 | } 74 | 75 | // create table row 76 | // basic row 77 | row := []string{ 78 | nodeName, // node name 79 | nodeStatus, // node status 80 | } 81 | 82 | // cpu 83 | if !o.noMetrics { 84 | row = append(row, o.toMilliUnitOrDash(cpuMetricsUsed)) // cpu used (from metrics) 85 | } 86 | row = append( 87 | row, 88 | o.toMilliUnitOrDash(cpuRequested), // cpu requested 89 | o.toMilliUnitOrDash(cpuLimited), // cpu limited 90 | o.toMilliUnitOrDash(cpuAllocatable), // cpu allocatable 91 | ) 92 | if !o.noMetrics { 93 | row = append(row, o.toColorPercent(cpuMetricsUsedP)) // cpu used % 94 | } 95 | row = append( 96 | row, 97 | o.toColorPercent(cpuRequestedP), // cpu requested % 98 | o.toColorPercent(cpuLimitedP), // cpu limited % 99 | ) 100 | 101 | // mem 102 | if !o.noMetrics { 103 | row = append(row, o.toUnitOrDash(memMetricsUsed)) // mem used (from metrics) 104 | } 105 | row = append( 106 | row, 107 | o.toUnitOrDash(memRequested), // mem requested 108 | o.toUnitOrDash(memLimited), // mem limited 109 | o.toUnitOrDash(memAllocatable), // mem allocatable 110 | ) 111 | if !o.noMetrics { 112 | row = append(row, o.toColorPercent(memMetricsUsedP)) // mem used % 113 | } 114 | row = append( 115 | row, 116 | o.toColorPercent(memRequestedP), // mem requested % 117 | o.toColorPercent(memLimitedP), // mem limited % 118 | ) 119 | 120 | // show pod and container (--pod option) 121 | if o.pod { 122 | 123 | // pod count 124 | podCount := util.GetPodCount(*pods) 125 | 126 | // container count 127 | containerCount := util.GetContainerCount(*pods) 128 | 129 | // get pod allocatable 130 | podAllocatable := node.Status.Allocatable.Pods().Value() 131 | 132 | row = append( 133 | row, 134 | fmt.Sprintf("%d", podCount), // pod used 135 | strconv.FormatInt(podAllocatable, 10), // pod allocatable 136 | fmt.Sprintf("%d", containerCount), // containers 137 | ) 138 | } 139 | 140 | o.table.AddRow(row) 141 | } 142 | 143 | o.table.Print() 144 | 145 | return nil 146 | } 147 | -------------------------------------------------------------------------------- /pkg/cmd/showfree_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/makocchi-git/kubectl-free/pkg/table" 9 | 10 | v1 "k8s.io/api/core/v1" 11 | fake "k8s.io/client-go/kubernetes/fake" 12 | ) 13 | 14 | func TestShowFree(t *testing.T) { 15 | 16 | var tests = []struct { 17 | description string 18 | pod bool 19 | namespace string 20 | noheader bool 21 | nometrics bool 22 | expected []string 23 | expectedErr error 24 | }{ 25 | { 26 | "default free", 27 | false, 28 | "default", 29 | true, 30 | true, 31 | []string{ 32 | "node1 Ready 1 2 4 25% 50% 1K 2K 4K 25% 50%", 33 | "", 34 | }, 35 | nil, 36 | }, 37 | { 38 | "default free with metrics", 39 | false, 40 | "default", 41 | true, 42 | false, 43 | []string{ 44 | "node1 Ready 100m 1 2 4 2% 25% 50% 1K 1K 2K 4K 25% 25% 50%", 45 | "", 46 | }, 47 | nil, 48 | }, 49 | { 50 | "default free --pod", 51 | true, 52 | "default", 53 | true, 54 | true, 55 | []string{ 56 | "node1 Ready 1 2 4 25% 50% 1K 2K 4K 25% 50% 1 110 1", 57 | "", 58 | }, 59 | nil, 60 | }, 61 | { 62 | "awesome-ns free", 63 | true, 64 | "awesome-ns", 65 | true, 66 | true, 67 | []string{ 68 | "node1 Ready 200m 200m 4 5% 5% 0K 0K 4K 7% 7% 1 110 2", 69 | "", 70 | }, 71 | nil, 72 | }, 73 | } 74 | 75 | for _, test := range tests { 76 | t.Run(test.description, func(t *testing.T) { 77 | 78 | fakeNodeClient := fake.NewSimpleClientset(&testNodes[0]) 79 | fakePodClient := fake.NewSimpleClientset(&testPods[0], &testPods[2]) 80 | fakeMetricsPodClient := prepareTestPodMetricsClient() 81 | fakeMetricsNodeClient := prepareTestNodeMetricsClient() 82 | 83 | buffer := &bytes.Buffer{} 84 | o := &FreeOptions{ 85 | nocolor: true, 86 | table: table.NewOutputTable(buffer), 87 | list: false, 88 | pod: test.pod, 89 | noHeaders: true, 90 | noMetrics: test.nometrics, 91 | nodeClient: fakeNodeClient.CoreV1().Nodes(), 92 | podClient: fakePodClient.CoreV1().Pods(test.namespace), 93 | metricsPodClient: fakeMetricsPodClient.MetricsV1beta1().PodMetricses("default"), 94 | metricsNodeClient: fakeMetricsNodeClient.MetricsV1beta1().NodeMetricses(), 95 | } 96 | 97 | if err := o.showFree([]v1.Node{testNodes[0]}); err != nil { 98 | t.Errorf("unexpected error: %v", err) 99 | return 100 | } 101 | 102 | e := strings.Join(test.expected, "\n") 103 | if buffer.String() != e { 104 | t.Errorf("expected(%s) differ (got: %s)", e, buffer.String()) 105 | return 106 | } 107 | 108 | }) 109 | } 110 | 111 | t.Run("Allnamespace", func(t *testing.T) { 112 | 113 | fakeNodeClient := fake.NewSimpleClientset(&testNodes[0]) 114 | fakePodClient := fake.NewSimpleClientset(&testPods[0], &testPods[1], &testPods[2]) 115 | fakeMetricsPodClient := prepareTestPodMetricsClient() 116 | fakeMetricsNodeClient := prepareTestNodeMetricsClient() 117 | 118 | buffer := &bytes.Buffer{} 119 | o := &FreeOptions{ 120 | nocolor: true, 121 | table: table.NewOutputTable(buffer), 122 | list: false, 123 | pod: false, 124 | allNamespaces: true, 125 | noHeaders: true, 126 | noMetrics: true, 127 | nodeClient: fakeNodeClient.CoreV1().Nodes(), 128 | podClient: fakePodClient.CoreV1().Pods(""), 129 | metricsPodClient: fakeMetricsPodClient.MetricsV1beta1().PodMetricses("default"), 130 | metricsNodeClient: fakeMetricsNodeClient.MetricsV1beta1().NodeMetricses(), 131 | } 132 | 133 | if err := o.showFree([]v1.Node{testNodes[0]}); err != nil { 134 | t.Errorf("unexpected error: %v", err) 135 | return 136 | } 137 | 138 | expected := []string{ 139 | "node1 Ready 1700m 2700m 4 42% 67% 2K 3K 4K 57% 82%", 140 | "", 141 | } 142 | e := strings.Join(expected, "\n") 143 | if buffer.String() != e { 144 | t.Errorf("expected(%s) differ (got: %s)", e, buffer.String()) 145 | return 146 | } 147 | 148 | }) 149 | } 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kubectl free 2 | 3 | [![Build Status](https://travis-ci.org/makocchi-git/kubectl-free.svg?branch=master)](https://travis-ci.org/makocchi-git/kubectl-free) 4 | [![Maintainability](https://api.codeclimate.com/v1/badges/b92591d00becc95b11ca/maintainability)](https://codeclimate.com/github/makocchi-git/kubectl-free/maintainability) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/makocchi-git/kubectl-free)](https://goreportcard.com/report/github.com/makocchi-git/kubectl-free) 6 | [![codecov](https://codecov.io/gh/makocchi-git/kubectl-free/branch/master/graph/badge.svg)](https://codecov.io/gh/makocchi-git/kubectl-free) 7 | [![kubectl plugin](https://img.shields.io/badge/kubectl-plugin-blue.svg)](https://github.com/topics/kubectl-plugin) 8 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) 9 | 10 | Print pod resources/limits usage on Kubernetes node(s) like a linux "free" command. 11 | 12 | ```shell 13 | $ kubectl free 14 | NAME STATUS CPU/use CPU/req CPU/lim CPU/alloc CPU/use% CPU/req% CPU/lim% MEM/use MEM/req MEM/lim MEM/alloc MEM/use% MEM/req% MEM/lim% 15 | node1 Ready 58m 704m 304m 3600m 1% 19% 8% 2144333K 807403K 375390K 5943857K 36% 13% 6% 16 | node2 Ready 235m 350m 2100m 3600m 6% 9% 58% 2061467K 260046K 1304428K 5943857K 34% 4% 21% 17 | node3 Ready 222m 2030m 12900m 3600m 6% 56% 358% 2935312K 3736783K 8347396K 5943865K 49% 62% 140% 18 | ``` 19 | 20 | And list containers of pod on Kubernetes node(s). 21 | 22 | ```shell 23 | $ kubectl free --list node1 --all-namespaces 24 | NODE NAME NAMESPACE POD NAME POD AGE POD IP POD STATUS CONTAINER CPU/use CPU/req CPU/lim MEM/use MEM/req MEM/lim 25 | node1 default nginx-7cdbd8cdc9-q2bbg 3d22h 10.112.2.43 Running nginx 2m 100m 2 27455K 134217K 1073741K 26 | node1 kube-system coredns-69dc677c56-chfcm 9d 10.112.3.2 Running coredns 3m 100m - 17420K 73400K 178257K 27 | node1 kube-system kube-flannel-ds-amd64-4b4s2 9d 10.1.2.3 Running kube-flannel 4m 100m 100m 13877K 52428K 52428K 28 | node1 kube-system kube-state-metrics-69bcc79474-wvmmk 9d 10.112.3.3 Running kube-state-metrics 11m 104m 104m 33382K 113246K 113246K 29 | node1 kube-system kube-state-metrics-69bcc79474-wvmmk 9d 10.112.3.3 Running addon-resizer 1m 100m 100m 8511K 31457K 31457K 30 | ... 31 | ``` 32 | 33 | ## Install 34 | 35 | `kubectl-free` binary is available at [release page](https://github.com/makocchi-git/kubectl-free/releases) or you can make binary. 36 | 37 | ```shell 38 | $ make 39 | $ mv _output/kubectl-free /usr/local/bin/. 40 | ``` 41 | 42 | ``` 43 | # Happy free time! 44 | $ kubectl free 45 | ``` 46 | 47 | ## Usage 48 | 49 | ```shell 50 | # Show pod resource usage of Kubernetes nodes (default namespace is "default"). 51 | kubectl free 52 | 53 | # Show pod resource usage of Kubernetes nodes (all namespaces). 54 | kubectl free --all-namespaces 55 | 56 | # Show pod resource usage of Kubernetes nodes with number of pods and containers. 57 | kubectl free --pod 58 | 59 | # Using label selector. 60 | kubectl free -l key=value 61 | 62 | # Print raw(bytes) usage. 63 | kubectl free --bytes --without-unit 64 | 65 | # Using binary prefix unit (GiB, MiB, etc) 66 | kubectl free -g -B 67 | 68 | # List resources of containers in pods on nodes. 69 | kubectl free --list 70 | 71 | # List resources of containers in pods on nodes with image information. 72 | kubectl free --list --list-image 73 | 74 | # Print container even if that has no resources/limits. 75 | kubectl free --list --list-all 76 | 77 | # Do you like emoji? 😃 78 | kubectl free --emoji 79 | kubectl free --list --emoji 80 | ``` 81 | 82 | ## Notice 83 | 84 | ~~This plugin shows just sum of requested(limited) resources, **not a real usage**. 85 | I recommend to use `kubectl free` with `kubectl top`.~~ 86 | 87 | kubectl free v0.2.0 supports printing real usages from metrics server in a target cluster. 88 | You can disable printing usage with `--no-metrics` option. 89 | 90 | ## License 91 | 92 | This software is released under the MIT License. 93 | -------------------------------------------------------------------------------- /pkg/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | color "github.com/gookit/color" 8 | v1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | clientv1 "k8s.io/client-go/kubernetes/typed/core/v1" 11 | metricsapiv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" 12 | 13 | "github.com/makocchi-git/kubectl-free/pkg/constants" 14 | ) 15 | 16 | // GetSiUnit defines unit for usage (SI prefix) 17 | // If multiple options are selected, returns a biggest unit 18 | func GetSiUnit(b, k, m, g bool) (int64, string) { 19 | 20 | if g { 21 | return constants.UnitGigaBytes, constants.UnitGigaBytesStr 22 | } 23 | 24 | if m { 25 | return constants.UnitMegaBytes, constants.UnitMegaBytesStr 26 | } 27 | 28 | if k { 29 | return constants.UnitKiloBytes, constants.UnitKiloBytesStr 30 | } 31 | 32 | if b { 33 | return constants.UnitBytes, constants.UnitBytesStr 34 | } 35 | 36 | // default output is "kilobytes" 37 | return constants.UnitKiloBytes, constants.UnitKiloBytesStr 38 | } 39 | 40 | // GetBinUnit defines unit for usage (Binary prefix) 41 | // If multiple options are selected, returns a biggest unit 42 | func GetBinUnit(b, k, m, g bool) (int64, string) { 43 | 44 | if g { 45 | return constants.UnitGibiBytes, constants.UnitGibiBytesStr 46 | } 47 | 48 | if m { 49 | return constants.UnitMibiBytes, constants.UnitMibiBytesStr 50 | } 51 | 52 | if k { 53 | return constants.UnitKibiBytes, constants.UnitKibiBytesStr 54 | } 55 | 56 | if b { 57 | return constants.UnitBytes, constants.UnitBytesStr 58 | } 59 | 60 | // default output is "kibibytes" 61 | return constants.UnitKibiBytes, constants.UnitKibiBytesStr 62 | } 63 | 64 | // JoinTab joins string slice with tab 65 | func JoinTab(s []string) string { 66 | return strings.Join(s, "\t") 67 | } 68 | 69 | // SetPercentageColor returns colored string 70 | // percentage < warn : Green 71 | // warn < percentage < crit : Yellow 72 | // crit < percentage : Red 73 | func SetPercentageColor(s *string, p, warn, crit int64) { 74 | green := color.FgGreen.Render 75 | yellow := color.FgYellow.Render 76 | red := color.FgRed.Render 77 | 78 | if p < warn { 79 | *s = green(*s) 80 | return 81 | } 82 | 83 | if p < crit { 84 | *s = yellow(*s) 85 | return 86 | } 87 | 88 | *s = red(*s) 89 | } 90 | 91 | // SetNodeStatusColor defined color of node status 92 | func SetNodeStatusColor(status *string, nocolor bool) { 93 | 94 | if nocolor { 95 | // nothing to do 96 | return 97 | } 98 | 99 | if *status != "Ready" { 100 | // Red 101 | *status = color.FgRed.Render(*status) 102 | return 103 | } 104 | 105 | // Green 106 | *status = color.FgGreen.Render(*status) 107 | 108 | } 109 | 110 | // GetPodStatus defined pod status with color 111 | func GetPodStatus(status string, nocolor, emoji bool) string { 112 | 113 | var s string 114 | 115 | switch status { 116 | case string(v1.PodRunning): 117 | if emoji { 118 | s = constants.EmojiPodRunning 119 | } else { 120 | s = string(v1.PodRunning) 121 | } 122 | 123 | if !nocolor { 124 | Green(&s) 125 | } 126 | case string(v1.PodSucceeded): 127 | if emoji { 128 | s = constants.EmojiPodSucceeded 129 | } else { 130 | s = string(v1.PodSucceeded) 131 | } 132 | 133 | if !nocolor { 134 | Green(&s) 135 | } 136 | case string(v1.PodPending): 137 | if emoji { 138 | s = constants.EmojiPodPending 139 | } else { 140 | s = string(v1.PodPending) 141 | } 142 | 143 | if !nocolor { 144 | Yellow(&s) 145 | } 146 | case string(v1.PodFailed): 147 | if emoji { 148 | s = constants.EmojiPodFailed 149 | } else { 150 | s = string(v1.PodFailed) 151 | } 152 | 153 | if !nocolor { 154 | Red(&s) 155 | } 156 | default: 157 | if emoji { 158 | s = constants.EmojiPodUnknown 159 | } else { 160 | s = "Unknown" 161 | } 162 | 163 | if !nocolor { 164 | DefaultColor(&s) 165 | } 166 | } 167 | 168 | return s 169 | } 170 | 171 | // GetNodes returns node objects 172 | func GetNodes(c clientv1.NodeInterface, args []string, label string) ([]v1.Node, error) { 173 | nodes := []v1.Node{} 174 | 175 | if len(args) > 0 { 176 | for _, a := range args { 177 | n, nerr := c.Get(a, metav1.GetOptions{}) 178 | if nerr != nil { 179 | return nodes, fmt.Errorf("failed to get node: %v", nerr) 180 | } 181 | nodes = append(nodes, *n) 182 | } 183 | } else { 184 | na, naerr := c.List(metav1.ListOptions{LabelSelector: label}) 185 | if naerr != nil { 186 | return nodes, fmt.Errorf("failed to list nodes: %v", naerr) 187 | } 188 | nodes = append(nodes, na.Items...) 189 | } 190 | 191 | return nodes, nil 192 | } 193 | 194 | // GetNodeStatus returns node status 195 | func GetNodeStatus(node v1.Node, emoji bool) (string, error) { 196 | status := "NotReady" 197 | 198 | for _, condition := range node.Status.Conditions { 199 | if condition.Type == v1.NodeReady && condition.Status == v1.ConditionTrue { 200 | status = "Ready" 201 | } 202 | } 203 | 204 | if emoji { 205 | switch status { 206 | case "Ready": 207 | status = constants.EmojiReady 208 | case "NotReady": 209 | status = constants.EmojiNotReady 210 | } 211 | } 212 | 213 | return status, nil 214 | } 215 | 216 | // GetPods returns node objects 217 | func GetPods(c clientv1.PodInterface, nodeName string) (*v1.PodList, error) { 218 | 219 | pods, err := c.List(metav1.ListOptions{FieldSelector: "spec.nodeName=" + nodeName}) 220 | if err != nil { 221 | return pods, fmt.Errorf("failed to get pods: %s", err) 222 | } 223 | 224 | return pods, nil 225 | } 226 | 227 | // GetContainerMetrics returns container metrics usage 228 | func GetContainerMetrics(metrics *metricsapiv1beta1.PodMetricsList, podName, containerName string) (cpu, mem int64) { 229 | 230 | var c int64 231 | var m int64 232 | 233 | for _, pod := range metrics.Items { 234 | if pod.ObjectMeta.Name == podName { 235 | for _, container := range pod.Containers { 236 | if container.Name == containerName { 237 | c = container.Usage.Cpu().MilliValue() 238 | m = container.Usage.Memory().Value() 239 | break 240 | } 241 | } 242 | } 243 | } 244 | 245 | // if no metrics found, return 0 0 246 | return c, m 247 | } 248 | 249 | // GetPodResources returns sum of requested/limit resources 250 | func GetPodResources(pods v1.PodList) (int64, int64, int64, int64) { 251 | var rc, rm, lc, lm int64 252 | 253 | for _, pod := range pods.Items { 254 | 255 | // skip if pod status is not running 256 | if pod.Status.Phase != v1.PodRunning { 257 | continue 258 | } 259 | 260 | for _, container := range pod.Spec.Containers { 261 | rc += container.Resources.Requests.Cpu().MilliValue() 262 | lc += container.Resources.Limits.Cpu().MilliValue() 263 | rm += container.Resources.Requests.Memory().Value() 264 | lm += container.Resources.Limits.Memory().Value() 265 | } 266 | } 267 | 268 | return rc, rm, lc, lm 269 | } 270 | 271 | // GetPodCount returns count of pods 272 | func GetPodCount(pods v1.PodList) int { 273 | return len(pods.Items) 274 | } 275 | 276 | // GetContainerCount returns count of containers 277 | func GetContainerCount(pods v1.PodList) int { 278 | var c int 279 | for _, pod := range pods.Items { 280 | c += len(pod.Spec.Containers) 281 | } 282 | return c 283 | } 284 | 285 | // GetPercentage returns (a*100)/b 286 | func GetPercentage(a, b int64) int64 { 287 | // avoid 0 divide 288 | if b == 0 { 289 | return 0 290 | } 291 | return (a * 100) / b 292 | } 293 | 294 | // DefaultColor set default color 295 | func DefaultColor(s *string) { 296 | // add dummy escape code 297 | *s = color.FgDefault.Render(*s) 298 | } 299 | 300 | // Green is coloring string to green 301 | func Green(s *string) { 302 | *s = color.FgGreen.Render(*s) 303 | } 304 | 305 | // Red is coloring string to red 306 | func Red(s *string) { 307 | *s = color.FgRed.Render(*s) 308 | } 309 | 310 | // Yellow is coloring string to yellow 311 | func Yellow(s *string) { 312 | *s = color.FgYellow.Render(*s) 313 | } 314 | -------------------------------------------------------------------------------- /pkg/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "strconv" 7 | 8 | "github.com/makocchi-git/kubectl-free/pkg/table" 9 | "github.com/makocchi-git/kubectl-free/pkg/util" 10 | 11 | "github.com/spf13/cobra" 12 | v1 "k8s.io/api/core/v1" 13 | "k8s.io/apimachinery/pkg/api/resource" 14 | "k8s.io/cli-runtime/pkg/genericclioptions" 15 | clientv1 "k8s.io/client-go/kubernetes/typed/core/v1" 16 | "k8s.io/client-go/rest" 17 | cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" 18 | "k8s.io/kubernetes/pkg/kubectl/util/templates" 19 | metrics "k8s.io/metrics/pkg/client/clientset/versioned" 20 | metricsv1beta1 "k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1" 21 | 22 | // Initialize all known client auth plugins. 23 | _ "k8s.io/client-go/plugin/pkg/client/auth" 24 | ) 25 | 26 | var ( 27 | // DfLong defines long description 28 | freeLong = templates.LongDesc(` 29 | Show various requested resources on Kubernetes nodes. 30 | `) 31 | 32 | // DfExample defines command examples 33 | freeExample = templates.Examples(` 34 | # Show pod resource usage of Kubernetes nodes (default namespace is "default"). 35 | kubectl free 36 | 37 | # Show pod resource usage of Kubernetes nodes (all namespaces). 38 | kubectl free --all-namespaces 39 | 40 | # Show pod resource usage of Kubernetes nodes with number of pods and containers. 41 | kubectl free --pod 42 | 43 | # Using label selector. 44 | kubectl free -l key=value 45 | 46 | # Print raw(bytes) usage. 47 | kubectl free --bytes --without-unit 48 | 49 | # Using binary prefix unit (GiB, MiB, etc) 50 | kubectl free -g -B 51 | 52 | # List resources of containers in pods on nodes. 53 | kubectl free --list 54 | 55 | # List resources of containers in pods on nodes with image information. 56 | kubectl free --list --list-image 57 | 58 | # Print container even if that has no resources/limits. 59 | kubectl free --list --list-all 60 | 61 | # Do you like emoji? 😃 62 | kubectl free --emoji 63 | kubectl free --list --emoji 64 | `) 65 | ) 66 | 67 | // FreeOptions is struct of df options 68 | type FreeOptions struct { 69 | configFlags *genericclioptions.ConfigFlags 70 | genericclioptions.IOStreams 71 | 72 | // general options 73 | labelSelector string 74 | table *table.OutputTable 75 | pod bool 76 | emojiStatus bool 77 | allNamespaces bool 78 | noHeaders bool 79 | noMetrics bool 80 | 81 | // unit options 82 | bytes bool 83 | kByte bool 84 | mByte bool 85 | gByte bool 86 | withoutUnit bool 87 | binPrefix bool 88 | 89 | // color output options 90 | nocolor bool 91 | warnThreshold int64 92 | critThreshold int64 93 | 94 | // list options 95 | list bool 96 | listContainerImage bool 97 | listAll bool 98 | 99 | // k8s clients 100 | nodeClient clientv1.NodeInterface 101 | podClient clientv1.PodInterface 102 | metricsPodClient metricsv1beta1.PodMetricsInterface 103 | metricsNodeClient metricsv1beta1.NodeMetricsInterface 104 | 105 | // table headers 106 | freeTableHeaders []string 107 | listTableHeaders []string 108 | } 109 | 110 | // NewFreeOptions is an instance of FreeOptions 111 | func NewFreeOptions(streams genericclioptions.IOStreams) *FreeOptions { 112 | return &FreeOptions{ 113 | configFlags: genericclioptions.NewConfigFlags(true), 114 | bytes: false, 115 | kByte: false, 116 | mByte: false, 117 | gByte: false, 118 | withoutUnit: false, 119 | binPrefix: false, 120 | nocolor: false, 121 | warnThreshold: 25, 122 | critThreshold: 50, 123 | IOStreams: streams, 124 | labelSelector: "", 125 | list: false, 126 | listContainerImage: false, 127 | listAll: false, 128 | pod: false, 129 | emojiStatus: false, 130 | table: table.NewOutputTable(os.Stdout), 131 | allNamespaces: false, 132 | noHeaders: false, 133 | noMetrics: false, 134 | } 135 | } 136 | 137 | // NewCmdFree is a cobra command wrapping 138 | func NewCmdFree(f cmdutil.Factory, streams genericclioptions.IOStreams, version, commit, date string) *cobra.Command { 139 | o := NewFreeOptions(streams) 140 | 141 | cmd := &cobra.Command{ 142 | Use: "kubectl free", 143 | Short: "Show various requested resources on Kubernetes nodes.", 144 | Long: freeLong, 145 | Example: freeExample, 146 | Version: version, 147 | Run: func(c *cobra.Command, args []string) { 148 | cmdutil.CheckErr(o.Complete(f, c, args)) 149 | cmdutil.CheckErr(o.Validate()) 150 | cmdutil.CheckErr(o.Run(args)) 151 | }, 152 | } 153 | 154 | // bool options 155 | cmd.Flags().BoolVarP(&o.bytes, "bytes", "b", o.bytes, `Use 1-byte (1-Byte) blocks rather than the default.`) 156 | cmd.Flags().BoolVarP(&o.kByte, "kilobytes", "k", o.kByte, `Use 1024-byte (1-Kbyte) blocks rather than the default.`) 157 | cmd.Flags().BoolVarP(&o.mByte, "megabytes", "m", o.mByte, `Use 1048576-byte (1-Mbyte) blocks rather than the default.`) 158 | cmd.Flags().BoolVarP(&o.gByte, "gigabytes", "g", o.gByte, `Use 1073741824-byte (1-Gbyte) blocks rather than the default.`) 159 | cmd.Flags().BoolVarP(&o.binPrefix, "binary-prefix", "B", o.binPrefix, `Use 1024 for basic unit calculation instead of 1000. (print like "KiB")`) 160 | cmd.Flags().BoolVarP(&o.withoutUnit, "without-unit", "", o.withoutUnit, `Do not print size with unit string.`) 161 | cmd.Flags().BoolVarP(&o.nocolor, "no-color", "", o.nocolor, `Print without ansi color.`) 162 | cmd.Flags().BoolVarP(&o.pod, "pod", "p", o.pod, `Show pod count and limit.`) 163 | cmd.Flags().BoolVarP(&o.list, "list", "", o.list, `Show container list on node.`) 164 | cmd.Flags().BoolVarP(&o.listContainerImage, "list-image", "", o.listContainerImage, `Show pod list on node with container image.`) 165 | cmd.Flags().BoolVarP(&o.listAll, "list-all", "", o.listAll, `Show pods even if they have no requests/limit`) 166 | cmd.Flags().BoolVarP(&o.emojiStatus, "emoji", "", o.emojiStatus, `Let's smile!! 😃 😭`) 167 | cmd.Flags().BoolVarP(&o.allNamespaces, "all-namespaces", "", o.allNamespaces, `If present, list pod resources(limits) across all namespaces. Namespace in current context is ignored even if specified with --namespace.`) 168 | cmd.Flags().BoolVarP(&o.noHeaders, "no-headers", "", o.noHeaders, `Do not print table headers.`) 169 | cmd.Flags().BoolVarP(&o.noMetrics, "no-metrics", "", o.noMetrics, `Do not print node/pods/containers usage from metrics-server.`) 170 | 171 | // int64 options 172 | cmd.Flags().Int64VarP(&o.warnThreshold, "warn-threshold", "", o.warnThreshold, `Threshold of warn(yellow) color for USED column.`) 173 | cmd.Flags().Int64VarP(&o.critThreshold, "crit-threshold", "", o.critThreshold, `Threshold of critical(red) color for USED column.`) 174 | 175 | // string option 176 | cmd.Flags().StringVarP(&o.labelSelector, "selector", "l", o.labelSelector, `Selector (label query) to filter on.`) 177 | 178 | o.configFlags.AddFlags(cmd.Flags()) 179 | 180 | // add the klog flags 181 | cmd.PersistentFlags().AddGoFlagSet(flag.CommandLine) 182 | 183 | // version command template 184 | cmd.SetVersionTemplate("Version: " + version + ", GitCommit: " + commit + ", BuildDate: " + date + "\n") 185 | 186 | return cmd 187 | } 188 | 189 | // Complete prepares k8s clients 190 | func (o *FreeOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { 191 | 192 | // get k8s client 193 | client, err := f.KubernetesClientSet() 194 | if err != nil { 195 | return err 196 | } 197 | 198 | // node client 199 | o.nodeClient = client.CoreV1().Nodes() 200 | 201 | // metric client 202 | config, err := f.ToRESTConfig() 203 | if err != nil { 204 | return err 205 | } 206 | 207 | mclient, err := o.setMetricsClient(config) 208 | if err != nil { 209 | return err 210 | } 211 | 212 | // pod and metrics client 213 | if o.allNamespaces { 214 | // --all-namespace flag 215 | o.podClient = client.CoreV1().Pods(v1.NamespaceAll) 216 | o.metricsPodClient = mclient.MetricsV1beta1().PodMetricses(v1.NamespaceAll) 217 | } else { 218 | if *o.configFlags.Namespace == "" { 219 | // default namespace is "default" 220 | o.podClient = client.CoreV1().Pods(v1.NamespaceDefault) 221 | o.metricsPodClient = mclient.MetricsV1beta1().PodMetricses(v1.NamespaceDefault) 222 | } else { 223 | // targeted namespace (--namespace flag) 224 | o.podClient = client.CoreV1().Pods(*o.configFlags.Namespace) 225 | o.metricsPodClient = mclient.MetricsV1beta1().PodMetricses(*o.configFlags.Namespace) 226 | } 227 | } 228 | o.metricsNodeClient = mclient.MetricsV1beta1().NodeMetricses() 229 | 230 | // prepare table header 231 | o.prepareFreeTableHeader() 232 | o.prepareListTableHeader() 233 | 234 | return nil 235 | } 236 | 237 | // Validate ensures that all required arguments and flag values are provided 238 | func (o *FreeOptions) Validate() error { 239 | 240 | // validate threshold 241 | if err := util.ValidateThreshold(o.warnThreshold, o.critThreshold); err != nil { 242 | return err 243 | } 244 | 245 | return nil 246 | } 247 | 248 | // Run printing disk usage of images 249 | func (o *FreeOptions) Run(args []string) error { 250 | 251 | // get nodes 252 | nodes, err := util.GetNodes(o.nodeClient, args, o.labelSelector) 253 | if err != nil { 254 | return nil 255 | } 256 | 257 | // list pods and return 258 | if o.list { 259 | if err := o.showPodsOnNode(nodes); err != nil { 260 | return err 261 | } 262 | return nil 263 | } 264 | 265 | // print cpu/mem/pod resource usage 266 | if err := o.showFree(nodes); err != nil { 267 | return err 268 | } 269 | 270 | return nil 271 | } 272 | 273 | // prepareFreeTableHeader defines table headers for free usage 274 | func (o *FreeOptions) prepareFreeTableHeader() { 275 | 276 | hName := "NAME" 277 | hStatus := "STATUS" 278 | hCPUUse := "CPU/use" 279 | hCPUReq := "CPU/req" 280 | hCPULim := "CPU/lim" 281 | hCPUAlloc := "CPU/alloc" 282 | hCPUUseP := "CPU/use%" 283 | hCPUReqP := "CPU/req%" 284 | hCPULimP := "CPU/lim%" 285 | hMEMUse := "MEM/use" 286 | hMEMReq := "MEM/req" 287 | hMEMLim := "MEM/lim" 288 | hMEMAlloc := "MEM/alloc" 289 | hMEMUseP := "MEM/use%" 290 | hMEMReqP := "MEM/req%" 291 | hMEMLimP := "MEM/lim%" 292 | hPods := "PODS" 293 | hPodsAlloc := "PODS/alloc" 294 | hContainers := "CONTAINERS" 295 | 296 | if !o.nocolor { 297 | // hack: avoid breaking column by escape char 298 | util.DefaultColor(&hStatus) // STATUS 299 | util.DefaultColor(&hCPUUseP) // CPU/use% 300 | util.DefaultColor(&hCPUReqP) // CPU/req% 301 | util.DefaultColor(&hCPULimP) // CPU/lim% 302 | util.DefaultColor(&hMEMUseP) // MEM/use% 303 | util.DefaultColor(&hMEMReqP) // MEM/req% 304 | util.DefaultColor(&hMEMLimP) // MEM/lim% 305 | } 306 | 307 | baseHeader := []string{ 308 | hName, 309 | hStatus, 310 | } 311 | 312 | cpuHeader := []string{ 313 | hCPUReq, 314 | hCPULim, 315 | hCPUAlloc, 316 | } 317 | 318 | cpuPHeader := []string{ 319 | hCPUReqP, 320 | hCPULimP, 321 | } 322 | 323 | memHeader := []string{ 324 | hMEMReq, 325 | hMEMLim, 326 | hMEMAlloc, 327 | } 328 | 329 | memPHeader := []string{ 330 | hMEMReqP, 331 | hMEMLimP, 332 | } 333 | 334 | podHeader := []string{ 335 | hPods, 336 | hPodsAlloc, 337 | hContainers, 338 | } 339 | 340 | if !o.noMetrics { 341 | // insert metrics columns 342 | cpuHeader = append([]string{hCPUUse}, cpuHeader...) 343 | cpuPHeader = append([]string{hCPUUseP}, cpuPHeader...) 344 | memHeader = append([]string{hMEMUse}, memHeader...) 345 | memPHeader = append([]string{hMEMUseP}, memPHeader...) 346 | } 347 | 348 | // finally, join all columns 349 | fth := []string{} 350 | 351 | fth = append(fth, baseHeader...) 352 | fth = append(fth, cpuHeader...) 353 | fth = append(fth, cpuPHeader...) 354 | fth = append(fth, memHeader...) 355 | fth = append(fth, memPHeader...) 356 | 357 | if o.pod { 358 | fth = append(fth, podHeader...) 359 | } 360 | 361 | o.freeTableHeaders = fth 362 | } 363 | 364 | // prepareListTableHeader defines table headers for --list 365 | func (o *FreeOptions) prepareListTableHeader() { 366 | 367 | hNode := "NODE NAME" 368 | hNameSpace := "NAMESPACE" 369 | hPod := "POD NAME" 370 | hPodIP := "POD IP" 371 | hPodStatus := "POD STATUS" 372 | hPodAge := "POD AGE" 373 | hContainer := "CONTAINER" 374 | hCPUUse := "CPU/use" 375 | hCPUReq := "CPU/req" 376 | hCPULim := "CPU/lim" 377 | hMEMUse := "MEM/use" 378 | hMEMReq := "MEM/req" 379 | hMEMLim := "MEM/lim" 380 | hImage := "IMAGE" 381 | 382 | if !o.nocolor { 383 | // hack: avoid breaking column by escape char 384 | util.DefaultColor(&hPodStatus) // POD STATUS 385 | } 386 | 387 | baseHeader := []string{ 388 | hNode, 389 | hNameSpace, 390 | } 391 | 392 | podHeader := []string{ 393 | hPod, 394 | hPodAge, 395 | hPodIP, 396 | hPodStatus, 397 | } 398 | 399 | containerHeader := []string{ 400 | hContainer, 401 | } 402 | 403 | cpuHeader := []string{ 404 | hCPUReq, 405 | hCPULim, 406 | } 407 | 408 | memHeader := []string{ 409 | hMEMReq, 410 | hMEMLim, 411 | } 412 | 413 | imageHeader := []string{ 414 | hImage, 415 | } 416 | 417 | if !o.noMetrics { 418 | // insert metrics columns 419 | cpuHeader = append([]string{hCPUUse}, cpuHeader...) 420 | memHeader = append([]string{hMEMUse}, memHeader...) 421 | } 422 | 423 | // finally, join all columns 424 | lth := []string{} 425 | 426 | lth = append(lth, baseHeader...) 427 | lth = append(lth, podHeader...) 428 | lth = append(lth, containerHeader...) 429 | lth = append(lth, cpuHeader...) 430 | lth = append(lth, memHeader...) 431 | 432 | if o.listContainerImage { 433 | lth = append(lth, imageHeader...) 434 | } 435 | 436 | o.listTableHeaders = lth 437 | } 438 | 439 | // setMetricsClient sets metrics client 440 | func (o *FreeOptions) setMetricsClient(config *rest.Config) (*metrics.Clientset, error) { 441 | 442 | metricsClient, err := metrics.NewForConfig(config) 443 | if err != nil { 444 | return nil, err 445 | } 446 | 447 | return metricsClient, nil 448 | } 449 | 450 | // toUnit calculate and add unit for int64 451 | func (o *FreeOptions) toUnit(i int64) string { 452 | 453 | var unitbytes int64 454 | var unitstr string 455 | 456 | if o.binPrefix { 457 | unitbytes, unitstr = util.GetBinUnit(o.bytes, o.kByte, o.mByte, o.gByte) 458 | } else { 459 | unitbytes, unitstr = util.GetSiUnit(o.bytes, o.kByte, o.mByte, o.gByte) 460 | } 461 | 462 | // -H adds human readable unit 463 | unit := "" 464 | if !o.withoutUnit { 465 | unit = unitstr 466 | } 467 | 468 | return strconv.FormatInt(i/unitbytes, 10) + unit 469 | } 470 | 471 | // toUnitOrDash returns "-" if "i" is 0, otherwise returns toUnit() 472 | func (o *FreeOptions) toUnitOrDash(i int64) string { 473 | 474 | if i == 0 { 475 | return "-" 476 | } 477 | 478 | return o.toUnit(i) 479 | } 480 | 481 | // toMilliUnitOrDash returns "-" if "i" is 0, otherwise returns MilliQuantity 482 | func (o *FreeOptions) toMilliUnitOrDash(i int64) string { 483 | 484 | if i == 0 { 485 | return "-" 486 | } 487 | 488 | if o.withoutUnit { 489 | // return raw value 490 | return strconv.FormatInt(i, 10) 491 | } 492 | 493 | return resource.NewMilliQuantity(i, resource.DecimalSI).String() 494 | } 495 | 496 | // toColorPercent returns colored strings 497 | // percentage < warn : Green 498 | // warn < percentage < crit : Yellow 499 | // crit < percentage : Red 500 | func (o *FreeOptions) toColorPercent(i int64) string { 501 | p := strconv.FormatInt(i, 10) + "%" 502 | 503 | if o.nocolor { 504 | // nothing to do 505 | return p 506 | } 507 | 508 | switch { 509 | case i < o.warnThreshold: 510 | // percentage < warn : Green 511 | util.Green(&p) 512 | case i < o.critThreshold: 513 | // warn < percentage < crit : Yellow 514 | util.Yellow(&p) 515 | default: 516 | // crit < percentage : Red 517 | util.Red(&p) 518 | } 519 | 520 | return p 521 | } 522 | -------------------------------------------------------------------------------- /pkg/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | "testing" 7 | 8 | "k8s.io/apimachinery/pkg/api/resource" 9 | 10 | "github.com/makocchi-git/kubectl-free/pkg/constants" 11 | 12 | color "github.com/gookit/color" 13 | v1 "k8s.io/api/core/v1" 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | fake "k8s.io/client-go/kubernetes/fake" 16 | metricsapiv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" 17 | ) 18 | 19 | // test node object 20 | var testNodes = []v1.Node{ 21 | { 22 | ObjectMeta: metav1.ObjectMeta{ 23 | Name: "node1", 24 | Labels: map[string]string{"hostname": "node1"}, 25 | }, 26 | Status: v1.NodeStatus{ 27 | Capacity: v1.ResourceList{ 28 | v1.ResourceEphemeralStorage: *resource.NewQuantity(10*1000*1000*1000, resource.DecimalSI), 29 | }, 30 | Allocatable: v1.ResourceList{ 31 | v1.ResourceEphemeralStorage: *resource.NewQuantity(5*1000*1000*1000, resource.DecimalSI), 32 | }, 33 | Conditions: []v1.NodeCondition{ 34 | { 35 | Type: v1.NodeReady, 36 | Status: v1.ConditionTrue, 37 | }, 38 | }, 39 | }, 40 | }, 41 | { 42 | ObjectMeta: metav1.ObjectMeta{Name: "node2"}, 43 | Status: v1.NodeStatus{ 44 | Capacity: v1.ResourceList{ 45 | v1.ResourceEphemeralStorage: *resource.NewQuantity(10*1000*1000*1000, resource.DecimalSI), 46 | }, 47 | Allocatable: v1.ResourceList{ 48 | v1.ResourceEphemeralStorage: *resource.NewQuantity(5*1000*1000*1000, resource.DecimalSI), 49 | }, 50 | Conditions: []v1.NodeCondition{ 51 | { 52 | Type: v1.NodeReady, 53 | Status: v1.ConditionFalse, 54 | }, 55 | }, 56 | }, 57 | }, 58 | } 59 | 60 | // test pod object 61 | var testPods = []v1.Pod{ 62 | { 63 | ObjectMeta: metav1.ObjectMeta{ 64 | Name: "pod1", 65 | Namespace: "default", 66 | }, 67 | Spec: v1.PodSpec{ 68 | NodeName: "node1", 69 | Containers: []v1.Container{ 70 | { 71 | Name: "container1", 72 | Resources: v1.ResourceRequirements{ 73 | Limits: v1.ResourceList{ 74 | v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 75 | v1.ResourceMemory: *resource.NewQuantity(2000, resource.DecimalSI), 76 | }, 77 | Requests: v1.ResourceList{ 78 | v1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI), 79 | v1.ResourceMemory: *resource.NewQuantity(1000, resource.DecimalSI), 80 | }, 81 | }, 82 | }, 83 | }, 84 | }, 85 | Status: v1.PodStatus{ 86 | Phase: v1.PodRunning, 87 | }, 88 | }, 89 | { 90 | ObjectMeta: metav1.ObjectMeta{ 91 | Name: "pod2", 92 | Namespace: "default", 93 | }, 94 | Spec: v1.PodSpec{ 95 | NodeName: "node2", 96 | Containers: []v1.Container{ 97 | { 98 | Name: "container2a", 99 | Resources: v1.ResourceRequirements{ 100 | Limits: v1.ResourceList{ 101 | v1.ResourceCPU: *resource.NewMilliQuantity(500, resource.DecimalSI), 102 | v1.ResourceMemory: *resource.NewQuantity(1000, resource.DecimalSI), 103 | }, 104 | Requests: v1.ResourceList{ 105 | v1.ResourceCPU: *resource.NewMilliQuantity(500, resource.DecimalSI), 106 | v1.ResourceMemory: *resource.NewQuantity(1000, resource.DecimalSI), 107 | }, 108 | }, 109 | }, 110 | { 111 | Name: "container2b", 112 | Resources: v1.ResourceRequirements{ 113 | Limits: v1.ResourceList{ 114 | v1.ResourceCPU: *resource.NewMilliQuantity(50, resource.DecimalSI), 115 | v1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), 116 | }, 117 | Requests: v1.ResourceList{ 118 | v1.ResourceCPU: *resource.NewMilliQuantity(50, resource.DecimalSI), 119 | v1.ResourceMemory: *resource.NewQuantity(100, resource.DecimalSI), 120 | }, 121 | }, 122 | }, 123 | }, 124 | }, 125 | Status: v1.PodStatus{ 126 | Phase: v1.PodRunning, 127 | }, 128 | }, 129 | { 130 | ObjectMeta: metav1.ObjectMeta{ 131 | Name: "pod3", 132 | Namespace: "default", 133 | }, 134 | Spec: v1.PodSpec{ 135 | NodeName: "node3", 136 | Containers: []v1.Container{ 137 | { 138 | Name: "container3", 139 | Resources: v1.ResourceRequirements{ 140 | Limits: v1.ResourceList{ 141 | v1.ResourceCPU: *resource.NewMilliQuantity(300, resource.DecimalSI), 142 | v1.ResourceMemory: *resource.NewQuantity(300, resource.DecimalSI), 143 | }, 144 | Requests: v1.ResourceList{ 145 | v1.ResourceCPU: *resource.NewMilliQuantity(300, resource.DecimalSI), 146 | v1.ResourceMemory: *resource.NewQuantity(300, resource.DecimalSI), 147 | }, 148 | }, 149 | }, 150 | }, 151 | }, 152 | Status: v1.PodStatus{ 153 | Phase: v1.PodFailed, 154 | }, 155 | }, 156 | } 157 | 158 | var testMetrics = &metricsapiv1beta1.PodMetricsList{ 159 | Items: []metricsapiv1beta1.PodMetrics{ 160 | { 161 | ObjectMeta: metav1.ObjectMeta{ 162 | Name: "pod1", 163 | Namespace: "default", 164 | Labels: map[string]string{ 165 | "key": "value", 166 | }, 167 | }, 168 | Timestamp: metav1.Now(), 169 | Containers: []metricsapiv1beta1.ContainerMetrics{ 170 | { 171 | Name: "container1", 172 | Usage: v1.ResourceList{ 173 | v1.ResourceCPU: *resource.NewMilliQuantity(10, resource.DecimalSI), 174 | v1.ResourceMemory: *resource.NewQuantity(10, resource.DecimalSI), 175 | }, 176 | }, 177 | }, 178 | }, 179 | }, 180 | } 181 | 182 | func TestGetSiUnit(t *testing.T) { 183 | var tests = []struct { 184 | description string 185 | b bool 186 | k bool 187 | m bool 188 | g bool 189 | expectedInt int64 190 | expectedStr string 191 | }{ 192 | {"all false", false, false, false, false, constants.UnitKiloBytes, constants.UnitKiloBytesStr}, 193 | {"all true", true, true, true, true, constants.UnitGigaBytes, constants.UnitGigaBytesStr}, 194 | {"b only", true, false, false, false, constants.UnitBytes, constants.UnitBytesStr}, 195 | {"g only", false, false, false, true, constants.UnitGigaBytes, constants.UnitGigaBytesStr}, 196 | {"b and k", true, true, false, false, constants.UnitKiloBytes, constants.UnitKiloBytesStr}, 197 | {"k and m", false, true, true, false, constants.UnitMegaBytes, constants.UnitMegaBytesStr}, 198 | {"k and m and g", false, true, true, true, constants.UnitGigaBytes, constants.UnitGigaBytesStr}, 199 | } 200 | 201 | for _, test := range tests { 202 | t.Run(test.description, func(t *testing.T) { 203 | actualInt, actualStr := GetSiUnit(test.b, test.k, test.m, test.g) 204 | if actualInt != test.expectedInt || actualStr != test.expectedStr { 205 | t.Errorf( 206 | "[%s] expected(%d, %s) differ (got: %d, %s)", 207 | test.description, 208 | test.expectedInt, 209 | test.expectedStr, 210 | actualInt, 211 | actualStr, 212 | ) 213 | return 214 | } 215 | }) 216 | } 217 | } 218 | 219 | func TestGetBinUnit(t *testing.T) { 220 | var tests = []struct { 221 | description string 222 | b bool 223 | k bool 224 | m bool 225 | g bool 226 | expectedInt int64 227 | expectedStr string 228 | }{ 229 | {"all false", false, false, false, false, constants.UnitKibiBytes, constants.UnitKibiBytesStr}, 230 | {"all true", true, true, true, true, constants.UnitGibiBytes, constants.UnitGibiBytesStr}, 231 | {"b only", true, false, false, false, constants.UnitBytes, constants.UnitBytesStr}, 232 | {"g only", false, false, false, true, constants.UnitGibiBytes, constants.UnitGibiBytesStr}, 233 | {"b and k", true, true, false, false, constants.UnitKibiBytes, constants.UnitKibiBytesStr}, 234 | {"k and m", false, true, true, false, constants.UnitMibiBytes, constants.UnitMibiBytesStr}, 235 | {"k and m and g", false, true, true, true, constants.UnitGibiBytes, constants.UnitGibiBytesStr}, 236 | } 237 | 238 | for _, test := range tests { 239 | t.Run(test.description, func(t *testing.T) { 240 | actualInt, actualStr := GetBinUnit(test.b, test.k, test.m, test.g) 241 | if actualInt != test.expectedInt || actualStr != test.expectedStr { 242 | t.Errorf( 243 | "[%s] expected(%d, %s) differ (got: %d, %s)", 244 | test.description, 245 | test.expectedInt, 246 | test.expectedStr, 247 | actualInt, 248 | actualStr, 249 | ) 250 | return 251 | } 252 | }) 253 | } 254 | } 255 | 256 | func TestJoinTab(t *testing.T) { 257 | var tests = []struct { 258 | description string 259 | words []string 260 | expected string 261 | }{ 262 | {"1 string", []string{"foo"}, "foo"}, 263 | {"2 strings", []string{"foo", "bar"}, "foo\tbar"}, 264 | {"3 strings", []string{"foo", "bar", "baz"}, "foo\tbar\tbaz"}, 265 | } 266 | 267 | for _, test := range tests { 268 | t.Run(test.description, func(t *testing.T) { 269 | actual := JoinTab(test.words) 270 | if actual != test.expected { 271 | t.Errorf( 272 | "[%s] expected(%s) differ (got: %s)", 273 | test.description, 274 | test.expected, 275 | actual, 276 | ) 277 | return 278 | } 279 | }) 280 | } 281 | } 282 | 283 | func TestSetPercentageColor(t *testing.T) { 284 | 285 | var tests = []struct { 286 | description string 287 | percentage int64 288 | warn int64 289 | crit int64 290 | expected string 291 | }{ 292 | {"5 with warn 10 crit 30", 5, 10, 30, color.Green.Sprint("5%")}, 293 | {"15 with warn 10 crit 30", 15, 10, 30, color.Yellow.Sprint("15%")}, 294 | {"50 with warn 10 crit 30", 50, 10, 30, color.Red.Sprint("50%")}, 295 | } 296 | 297 | for _, test := range tests { 298 | t.Run(test.description, func(t *testing.T) { 299 | actual := strconv.FormatInt(test.percentage, 10) + "%" 300 | SetPercentageColor(&actual, test.percentage, test.warn, test.crit) 301 | 302 | if actual != test.expected { 303 | t.Errorf( 304 | "[%s] expected(%v) differ (got: %v)", 305 | test.description, 306 | actual, 307 | test.expected, 308 | ) 309 | return 310 | } 311 | }) 312 | } 313 | } 314 | 315 | func TestSetNodeStatusColor(t *testing.T) { 316 | 317 | var tests = []struct { 318 | description string 319 | status string 320 | nocolor bool 321 | expected string 322 | }{ 323 | {"green status", "Ready", false, color.Green.Sprint("Ready")}, 324 | {"red status", "NotReady", false, color.Red.Sprint("NotReady")}, 325 | {"green status but nocolor", "Ready", true, "Ready"}, 326 | } 327 | 328 | for _, test := range tests { 329 | t.Run(test.description, func(t *testing.T) { 330 | SetNodeStatusColor(&test.status, test.nocolor) 331 | actual := []byte(test.status) 332 | expectedB := []byte(test.expected) 333 | if !bytes.Equal(actual, expectedB) { 334 | t.Errorf( 335 | "[%s] expected(%v) differ (got: %v)", 336 | test.description, 337 | expectedB, 338 | actual, 339 | ) 340 | return 341 | } 342 | }) 343 | } 344 | } 345 | 346 | func TestGetPodStatus(t *testing.T) { 347 | 348 | var tests = []struct { 349 | description string 350 | podStatus string 351 | nocolor bool 352 | emoji bool 353 | expected string 354 | }{ 355 | {"pod running", string(v1.PodRunning), false, false, color.Green.Sprint("Running")}, 356 | {"pod succeeded", string(v1.PodSucceeded), false, false, color.Green.Sprint("Succeeded")}, 357 | {"pod pending", string(v1.PodPending), false, false, color.Yellow.Sprint("Pending")}, 358 | {"pod failed", string(v1.PodFailed), false, false, color.Red.Sprint("Failed")}, 359 | {"pod other status", "other", false, false, color.FgDefault.Render("Unknown")}, 360 | {"pod running but nocolor", string(v1.PodRunning), true, false, "Running"}, 361 | {"pod running and emoji", string(v1.PodRunning), false, true, color.Green.Sprint("✅")}, 362 | {"pod succeeded and emoji", string(v1.PodSucceeded), false, true, color.Green.Sprint("⭕")}, 363 | {"pod pending and emoji", string(v1.PodPending), false, true, color.Yellow.Sprint("🚫")}, 364 | {"pod failed and emoji", string(v1.PodFailed), false, true, color.Red.Sprint("❌")}, 365 | {"pod unknown and emoji", "other", false, true, color.FgDefault.Render("❓")}, 366 | } 367 | 368 | for _, test := range tests { 369 | t.Run(test.description, func(t *testing.T) { 370 | actual := GetPodStatus(test.podStatus, test.nocolor, test.emoji) 371 | actualB := []byte(actual) 372 | expectedB := []byte(test.expected) 373 | if !bytes.Equal(actualB, expectedB) { 374 | t.Errorf( 375 | "[%s] expected(%v) differ (got: %v)", 376 | test.description, 377 | expectedB, 378 | actualB, 379 | ) 380 | return 381 | } 382 | }) 383 | } 384 | } 385 | 386 | func TestGetNodes(t *testing.T) { 387 | fakeClient := fake.NewSimpleClientset(&testNodes[0], &testNodes[1]) 388 | fakenode := fakeClient.CoreV1().Nodes() 389 | 390 | // no args and no labels 391 | t.Run("no args and no labels", func(t *testing.T) { 392 | nodes, err := GetNodes(fakenode, []string{}, "") 393 | 394 | if err != nil { 395 | t.Errorf("unexpected error: %v", err) 396 | return 397 | } 398 | 399 | l := len(nodes) 400 | if l != 2 { 401 | t.Errorf("[no args and no labels] expected(2) differ (got: %d)", l) 402 | return 403 | } 404 | }) 405 | 406 | // no args with valid labels 407 | t.Run("no args with valid labels", func(t *testing.T) { 408 | nodes, err := GetNodes(fakenode, []string{}, "hostname=node1") 409 | 410 | if err != nil { 411 | t.Errorf("unexpected error: %v", err) 412 | return 413 | } 414 | 415 | l := len(nodes) 416 | if l != 1 { 417 | t.Errorf("[no args and no valid labels] expected(1) differ (got: %d)", l) 418 | return 419 | } 420 | 421 | if nodes[0].ObjectMeta.Name != "node1" { 422 | t.Errorf("[no args and no valid labels] expected(node1) differ (got: %s)", nodes[0].ObjectMeta.Name) 423 | } 424 | }) 425 | 426 | // no args with invalid labels 427 | t.Run("no args with invalid labels", func(t *testing.T) { 428 | nodes, err := GetNodes(fakenode, []string{}, "foo=bar") 429 | 430 | if err != nil { 431 | t.Errorf("unexpected error: %v", err) 432 | return 433 | } 434 | 435 | l := len(nodes) 436 | if l != 0 { 437 | t.Errorf("[no args and no invalid labels] expected(0) differ (got: %d)", l) 438 | return 439 | } 440 | }) 441 | 442 | // one arg 443 | t.Run("one arg", func(t *testing.T) { 444 | nodes, err := GetNodes(fakenode, []string{"node2"}, "") 445 | 446 | if err != nil { 447 | t.Errorf("unexpected error: %v", err) 448 | return 449 | } 450 | 451 | l := len(nodes) 452 | if l != 1 { 453 | t.Errorf("[one arg] expected(1) differ (got: %d)", l) 454 | return 455 | } 456 | 457 | if nodes[0].ObjectMeta.Name != "node2" { 458 | t.Errorf("[one arg] expected(node2) differ (got: %s)", nodes[0].ObjectMeta.Name) 459 | } 460 | }) 461 | 462 | // one arg but invalid node 463 | t.Run("one arg but invalid node", func(t *testing.T) { 464 | _, err := GetNodes(fakenode, []string{"foobar"}, "") 465 | 466 | if err == nil { 467 | t.Errorf("unexpected error: should return err") 468 | return 469 | } 470 | 471 | }) 472 | } 473 | 474 | func TestGetNodeStatus(t *testing.T) { 475 | 476 | var tests = []struct { 477 | description string 478 | node v1.Node 479 | emoji bool 480 | expected string 481 | }{ 482 | {"ready", testNodes[0], false, "Ready"}, 483 | {"notready", testNodes[1], false, "NotReady"}, 484 | {"ready emoji", testNodes[0], true, "😃"}, 485 | {"notready emoji", testNodes[1], true, "😭"}, 486 | } 487 | 488 | for _, test := range tests { 489 | t.Run(test.description, func(t *testing.T) { 490 | actual, err := GetNodeStatus(test.node, test.emoji) 491 | if err != nil { 492 | t.Errorf("unexpected error: %v", err) 493 | return 494 | } 495 | 496 | if actual != test.expected { 497 | t.Errorf( 498 | "[%s] expected(%s) differ (got: %s)", 499 | test.description, 500 | test.expected, 501 | actual, 502 | ) 503 | return 504 | } 505 | }) 506 | } 507 | } 508 | 509 | func TestGetPods(t *testing.T) { 510 | fakeClient := fake.NewSimpleClientset(&testPods[0], &testPods[1]) 511 | fakepod := fakeClient.CoreV1().Pods("") 512 | 513 | // get pods 514 | t.Run("get pods", func(t *testing.T) { 515 | 516 | // FieldSelector on fakeclient doesn't work well 517 | // https://github.com/kubernetes/client-go/issues/326 518 | pods, err := GetPods(fakepod, "dummy") 519 | 520 | if err != nil { 521 | t.Errorf("unexpected error: %v", err) 522 | return 523 | } 524 | 525 | l := len(pods.Items) 526 | if l != 2 { 527 | t.Errorf("[correct node name] expected(2) differ (got: %d)", l) 528 | return 529 | } 530 | }) 531 | } 532 | 533 | func TestGetPodResources(t *testing.T) { 534 | 535 | // cpu/ mem cpu/ mem 536 | // pod1 container1 : Running [requests 1000/1000] [limits 2000/2000] 537 | // pod2 container2a : Running [requests 500/1000] [limits 500/1000] 538 | // pod2 container2b : Running [requests 50/ 100] [limits 50/ 100] 539 | // pod3 container3 : Failed [requests 300/ 300] [limits 300/ 300] 540 | pods := v1.PodList{ 541 | Items: []v1.Pod{ 542 | testPods[0], 543 | testPods[1], 544 | testPods[2], 545 | }, 546 | } 547 | 548 | // get pods resource 549 | t.Run("get pods resource", func(t *testing.T) { 550 | 551 | rc, rm, lc, lm := GetPodResources(pods) 552 | 553 | // 1000 + 500 + 50 554 | if rc != 1550 { 555 | t.Errorf("[get pods resource Requests.Cpu] expected(1550) differ (got: %d)", rc) 556 | return 557 | } 558 | 559 | // 1000 + 1000 + 100 560 | if rm != 2100 { 561 | t.Errorf("[get pods resource Requests.Memory] expected(2100) differ (got: %d)", rm) 562 | return 563 | } 564 | 565 | // 2000 + 500 + 50 566 | if lc != 2550 { 567 | t.Errorf("[get pods resource Limits.Cpu] expected(2550) differ (got: %d)", lc) 568 | return 569 | } 570 | 571 | // 2000 + 1000 + 100 572 | if lm != 3100 { 573 | t.Errorf("[get pods resource Limits.Memeory] expected(3100) differ (got: %d)", lm) 574 | return 575 | } 576 | }) 577 | } 578 | 579 | func TestGetContainerMetrics(t *testing.T) { 580 | 581 | var tests = []struct { 582 | description string 583 | podName string 584 | containerName string 585 | expectedCPU int64 586 | expectedMEM int64 587 | }{ 588 | {"10 and 10", "pod1", "container1", 10, 10}, 589 | {"0 and 0", "pod999", "container999", 0, 0}, 590 | } 591 | 592 | for _, test := range tests { 593 | t.Run("[GetContainerMetrics] cpu and mem", func(t *testing.T) { 594 | actualCPU, actualMEM := GetContainerMetrics(testMetrics, test.podName, test.containerName) 595 | if actualCPU != test.expectedCPU { 596 | t.Errorf("[%s cpu] expected(%d) differ (got: %d)", test.description, test.expectedCPU, actualCPU) 597 | return 598 | } 599 | if actualMEM != test.expectedMEM { 600 | t.Errorf("[%s mem] expected(%d) differ (got: %d)", test.description, test.expectedMEM, actualMEM) 601 | return 602 | } 603 | }) 604 | } 605 | } 606 | 607 | func TestGetPodCount(t *testing.T) { 608 | 609 | var tests = []struct { 610 | description string 611 | pods v1.PodList 612 | expected int 613 | }{ 614 | {"2 pods", v1.PodList{Items: []v1.Pod{testPods[0], testPods[1]}}, 2}, 615 | {"1 pod ", v1.PodList{Items: []v1.Pod{testPods[0]}}, 1}, 616 | {"0 pods", v1.PodList{Items: []v1.Pod{}}, 0}, 617 | } 618 | 619 | for _, test := range tests { 620 | t.Run(test.description, func(t *testing.T) { 621 | actual := GetPodCount(test.pods) 622 | if actual != test.expected { 623 | t.Errorf( 624 | "[%s] expected(%d) differ (got: %d)", 625 | test.description, 626 | test.expected, 627 | actual, 628 | ) 629 | return 630 | } 631 | }) 632 | } 633 | } 634 | 635 | func TestGetContainerCount(t *testing.T) { 636 | 637 | var tests = []struct { 638 | description string 639 | pods v1.PodList 640 | expected int 641 | }{ 642 | {"2 pods (1 container + 2 container)", v1.PodList{Items: []v1.Pod{testPods[0], testPods[1]}}, 3}, 643 | {"1 pod (1 container)", v1.PodList{Items: []v1.Pod{testPods[0]}}, 1}, 644 | {"0 pods", v1.PodList{Items: []v1.Pod{}}, 0}, 645 | } 646 | 647 | for _, test := range tests { 648 | t.Run(test.description, func(t *testing.T) { 649 | actual := GetContainerCount(test.pods) 650 | if actual != test.expected { 651 | t.Errorf( 652 | "[%s] expected(%d) differ (got: %d)", 653 | test.description, 654 | test.expected, 655 | actual, 656 | ) 657 | return 658 | } 659 | }) 660 | } 661 | } 662 | 663 | func TestGetPercentage(t *testing.T) { 664 | 665 | var tests = []struct { 666 | description string 667 | a int64 668 | b int64 669 | expected int64 670 | }{ 671 | {"0/10", 0, 10, 0}, 672 | {"10/20", 10, 20, 50}, 673 | {"30/20", 30, 20, 150}, 674 | {"30/0", 30, 0, 0}, 675 | } 676 | 677 | for _, test := range tests { 678 | t.Run(test.description, func(t *testing.T) { 679 | actual := GetPercentage(test.a, test.b) 680 | if actual != test.expected { 681 | t.Errorf( 682 | "[%s] expected(%d) differ (got: %d)", 683 | test.description, 684 | test.expected, 685 | actual, 686 | ) 687 | return 688 | } 689 | }) 690 | } 691 | 692 | } 693 | 694 | func TestColor(t *testing.T) { 695 | 696 | t.Run("default color", func(t *testing.T) { 697 | s := "foo" 698 | expected := []byte(color.FgDefault.Render(s)) 699 | DefaultColor(&s) 700 | actual := []byte(s) 701 | if !bytes.Equal(actual, expected) { 702 | t.Errorf( 703 | "[default color] expected(%v) differ (got: %v)", 704 | expected, 705 | actual, 706 | ) 707 | return 708 | } 709 | }) 710 | 711 | t.Run("green color", func(t *testing.T) { 712 | s := "foo" 713 | expected := []byte(color.FgGreen.Render(s)) 714 | Green(&s) 715 | actual := []byte(s) 716 | if !bytes.Equal(actual, expected) { 717 | t.Errorf( 718 | "[green color] expected(%v) differ (got: %v)", 719 | expected, 720 | actual, 721 | ) 722 | return 723 | } 724 | }) 725 | 726 | t.Run("red color", func(t *testing.T) { 727 | s := "foo" 728 | expected := []byte(color.FgRed.Render(s)) 729 | Red(&s) 730 | actual := []byte(s) 731 | if !bytes.Equal(actual, expected) { 732 | t.Errorf( 733 | "[red color] expected(%v) differ (got: %v)", 734 | expected, 735 | actual, 736 | ) 737 | return 738 | } 739 | }) 740 | 741 | t.Run("yellow color", func(t *testing.T) { 742 | s := "foo" 743 | expected := []byte(color.FgYellow.Render(s)) 744 | Yellow(&s) 745 | actual := []byte(s) 746 | if !bytes.Equal(actual, expected) { 747 | t.Errorf( 748 | "[yellow color] expected(%v) differ (got: %v)", 749 | expected, 750 | actual, 751 | ) 752 | return 753 | } 754 | }) 755 | } 756 | -------------------------------------------------------------------------------- /pkg/cmd/root_test.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "os" 6 | "reflect" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/makocchi-git/kubectl-free/pkg/table" 11 | "github.com/makocchi-git/kubectl-free/pkg/util" 12 | 13 | color "github.com/gookit/color" 14 | "github.com/spf13/cobra" 15 | v1 "k8s.io/api/core/v1" 16 | "k8s.io/apimachinery/pkg/api/resource" 17 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 | "k8s.io/apimachinery/pkg/runtime" 19 | "k8s.io/cli-runtime/pkg/genericclioptions" 20 | fake "k8s.io/client-go/kubernetes/fake" 21 | core "k8s.io/client-go/testing" 22 | cmdtesting "k8s.io/kubernetes/pkg/kubectl/cmd/testing" 23 | cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" 24 | metricsapiv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" 25 | fakemetrics "k8s.io/metrics/pkg/client/clientset/versioned/fake" 26 | ) 27 | 28 | // test node object 29 | var testNodes = []v1.Node{ 30 | { 31 | ObjectMeta: metav1.ObjectMeta{ 32 | Name: "node1", 33 | Labels: map[string]string{"hostname": "node1"}, 34 | }, 35 | Status: v1.NodeStatus{ 36 | Allocatable: v1.ResourceList{ 37 | v1.ResourceCPU: *resource.NewMilliQuantity(4000, resource.DecimalSI), 38 | v1.ResourceMemory: *resource.NewQuantity(4000, resource.DecimalSI), 39 | v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 40 | }, 41 | Conditions: []v1.NodeCondition{ 42 | { 43 | Type: v1.NodeReady, 44 | Status: v1.ConditionTrue, 45 | }, 46 | }, 47 | }, 48 | }, 49 | { 50 | ObjectMeta: metav1.ObjectMeta{ 51 | Name: "node2", 52 | Labels: map[string]string{"hostname": "node1"}, 53 | }, 54 | Status: v1.NodeStatus{ 55 | Allocatable: v1.ResourceList{ 56 | v1.ResourceCPU: *resource.NewMilliQuantity(8000, resource.DecimalSI), 57 | v1.ResourceMemory: *resource.NewQuantity(8000, resource.DecimalSI), 58 | v1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), 59 | }, 60 | Conditions: []v1.NodeCondition{ 61 | { 62 | Type: v1.NodeReady, 63 | Status: v1.ConditionFalse, 64 | }, 65 | }, 66 | }, 67 | }, 68 | } 69 | 70 | // test pod object 71 | var testPods = []v1.Pod{ 72 | { 73 | ObjectMeta: metav1.ObjectMeta{ 74 | Name: "pod1", 75 | Namespace: "default", 76 | }, 77 | Status: v1.PodStatus{ 78 | PodIP: "1.2.3.4", 79 | Phase: v1.PodRunning, 80 | }, 81 | Spec: v1.PodSpec{ 82 | NodeName: "node1", 83 | Containers: []v1.Container{ 84 | { 85 | Name: "container1", 86 | Image: "alpine:latest", 87 | Resources: v1.ResourceRequirements{ 88 | Limits: v1.ResourceList{ 89 | v1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), 90 | v1.ResourceMemory: *resource.NewQuantity(2000, resource.DecimalSI), 91 | }, 92 | Requests: v1.ResourceList{ 93 | v1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI), 94 | v1.ResourceMemory: *resource.NewQuantity(1000, resource.DecimalSI), 95 | }, 96 | }, 97 | }, 98 | }, 99 | }, 100 | }, 101 | { 102 | ObjectMeta: metav1.ObjectMeta{ 103 | Name: "pod2", 104 | Namespace: "default", 105 | }, 106 | Status: v1.PodStatus{ 107 | PodIP: "2.3.4.5", 108 | Phase: v1.PodRunning, 109 | }, 110 | Spec: v1.PodSpec{ 111 | NodeName: "node1", 112 | Containers: []v1.Container{ 113 | { 114 | Name: "container2a", 115 | Image: "nginx:latest", 116 | Resources: v1.ResourceRequirements{ 117 | Limits: v1.ResourceList{ 118 | v1.ResourceCPU: *resource.NewMilliQuantity(500, resource.DecimalSI), 119 | v1.ResourceMemory: *resource.NewQuantity(1000, resource.DecimalSI), 120 | }, 121 | Requests: v1.ResourceList{ 122 | v1.ResourceCPU: *resource.NewMilliQuantity(500, resource.DecimalSI), 123 | v1.ResourceMemory: *resource.NewQuantity(1000, resource.DecimalSI), 124 | }, 125 | }, 126 | }, 127 | { 128 | Name: "container2b", 129 | Image: "busybox:latest", 130 | }, 131 | }, 132 | }, 133 | }, 134 | { 135 | ObjectMeta: metav1.ObjectMeta{ 136 | Name: "pod3", 137 | Namespace: "awesome-ns", 138 | }, 139 | Status: v1.PodStatus{ 140 | PodIP: "3.4.5.6", 141 | Phase: v1.PodRunning, 142 | }, 143 | Spec: v1.PodSpec{ 144 | NodeName: "node1", 145 | Containers: []v1.Container{ 146 | { 147 | Name: "container3a", 148 | Image: "centos:7", 149 | Resources: v1.ResourceRequirements{ 150 | Limits: v1.ResourceList{ 151 | v1.ResourceCPU: *resource.NewMilliQuantity(200, resource.DecimalSI), 152 | v1.ResourceMemory: *resource.NewQuantity(300, resource.DecimalSI), 153 | }, 154 | Requests: v1.ResourceList{ 155 | v1.ResourceCPU: *resource.NewMilliQuantity(200, resource.DecimalSI), 156 | v1.ResourceMemory: *resource.NewQuantity(300, resource.DecimalSI), 157 | }, 158 | }, 159 | }, 160 | { 161 | Name: "container3b", 162 | Image: "ubuntu:bionic", 163 | }, 164 | }, 165 | }, 166 | }, 167 | } 168 | 169 | var testPodMetrics = &metricsapiv1beta1.PodMetricsList{ 170 | Items: []metricsapiv1beta1.PodMetrics{ 171 | { 172 | ObjectMeta: metav1.ObjectMeta{ 173 | Name: "pod1", 174 | Namespace: "default", 175 | Labels: map[string]string{ 176 | "key": "value", 177 | }, 178 | }, 179 | Timestamp: metav1.Now(), 180 | Containers: []metricsapiv1beta1.ContainerMetrics{ 181 | { 182 | Name: "container1", 183 | Usage: v1.ResourceList{ 184 | v1.ResourceCPU: *resource.NewMilliQuantity(10, resource.DecimalSI), 185 | v1.ResourceMemory: *resource.NewQuantity(10, resource.DecimalSI), 186 | }, 187 | }, 188 | }, 189 | }, 190 | }, 191 | } 192 | 193 | var testNodeMetrics = &metricsapiv1beta1.NodeMetricsList{ 194 | Items: []metricsapiv1beta1.NodeMetrics{ 195 | { 196 | ObjectMeta: metav1.ObjectMeta{ 197 | Name: "node1", 198 | }, 199 | Usage: v1.ResourceList{ 200 | v1.ResourceCPU: *resource.NewMilliQuantity(100, resource.DecimalSI), 201 | v1.ResourceMemory: *resource.NewQuantity(1024, resource.DecimalSI), 202 | }, 203 | }, 204 | { 205 | ObjectMeta: metav1.ObjectMeta{ 206 | Name: "node2", 207 | }, 208 | Usage: v1.ResourceList{ 209 | v1.ResourceCPU: *resource.NewMilliQuantity(200, resource.DecimalSI), 210 | v1.ResourceMemory: *resource.NewQuantity(2048, resource.DecimalSI), 211 | }, 212 | }, 213 | }, 214 | } 215 | 216 | func TestNewFreeOptions(t *testing.T) { 217 | streams := genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} 218 | 219 | expected := &FreeOptions{ 220 | configFlags: genericclioptions.NewConfigFlags(true), 221 | bytes: false, 222 | kByte: false, 223 | mByte: false, 224 | gByte: false, 225 | withoutUnit: false, 226 | binPrefix: false, 227 | nocolor: false, 228 | warnThreshold: 25, 229 | critThreshold: 50, 230 | IOStreams: streams, 231 | labelSelector: "", 232 | list: false, 233 | listContainerImage: false, 234 | listAll: false, 235 | pod: false, 236 | emojiStatus: false, 237 | table: table.NewOutputTable(os.Stdout), 238 | noHeaders: false, 239 | noMetrics: false, 240 | } 241 | 242 | actual := NewFreeOptions(streams) 243 | 244 | if !reflect.DeepEqual(actual, expected) { 245 | t.Errorf("expected(%#v) differ (got: %#v)", expected, actual) 246 | } 247 | } 248 | 249 | func TestNewCmdFree(t *testing.T) { 250 | 251 | cmdtesting.InitTestErrorHandler(t) 252 | tf := cmdtesting.NewTestFactory() 253 | defer tf.Cleanup() 254 | tf.ClientConfigVal = cmdtesting.DefaultClientConfig() 255 | 256 | rootCmd := NewCmdFree( 257 | tf, 258 | genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}, 259 | "v0.0.1", 260 | "abcd123", 261 | "1234567890", 262 | ) 263 | 264 | // Version check 265 | t.Run("version", func(t *testing.T) { 266 | expected := "Version: v0.0.1, GitCommit: abcd123, BuildDate: 1234567890\n" 267 | actual, err := executeCommand(rootCmd, "--version") 268 | if err != nil { 269 | t.Errorf("unexpected error: %v", err) 270 | } 271 | 272 | if actual != expected { 273 | t.Errorf("expected(%s) differ (got: %s)", expected, actual) 274 | return 275 | } 276 | }) 277 | 278 | // Usage 279 | t.Run("usage", func(t *testing.T) { 280 | expected := "kubectl free [flags]" 281 | actual, err := executeCommand(rootCmd, "--help") 282 | if err != nil { 283 | t.Errorf("unexpected error: %v", err) 284 | } 285 | 286 | if !strings.Contains(actual, expected) { 287 | t.Errorf("expected(%s) differ (got: %s)", expected, actual) 288 | return 289 | } 290 | }) 291 | 292 | // Unknown option 293 | t.Run("unknown option", func(t *testing.T) { 294 | expected := "unknown flag: --very-very-bad-option" 295 | _, err := executeCommand(rootCmd, "--very-very-bad-option") 296 | if err == nil { 297 | t.Errorf("unexpected error: should return exit") 298 | return 299 | } 300 | 301 | if err.Error() != expected { 302 | t.Errorf("expected(%s) differ (got: %s)", expected, err.Error()) 303 | return 304 | } 305 | }) 306 | } 307 | 308 | func TestComplete(t *testing.T) { 309 | 310 | cmdtesting.InitTestErrorHandler(t) 311 | tf := cmdtesting.NewTestFactory() 312 | defer tf.Cleanup() 313 | tf.ClientConfigVal = cmdtesting.DefaultClientConfig() 314 | 315 | rootCmd := NewCmdFree( 316 | tf, 317 | genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}, 318 | "v0.0.1", 319 | "abcd123", 320 | "1234567890", 321 | ) 322 | 323 | kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() 324 | matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags) 325 | 326 | f := cmdutil.NewFactory(matchVersionKubeConfigFlags) 327 | 328 | t.Run("complete", func(t *testing.T) { 329 | 330 | o := &FreeOptions{ 331 | configFlags: genericclioptions.NewConfigFlags(true), 332 | } 333 | 334 | if err := o.Complete(f, rootCmd, []string{}); err != nil { 335 | t.Errorf("unexpected error: %v", err) 336 | return 337 | } 338 | }) 339 | 340 | t.Run("complete allnamespace", func(t *testing.T) { 341 | 342 | o := &FreeOptions{ 343 | configFlags: genericclioptions.NewConfigFlags(true), 344 | allNamespaces: true, 345 | } 346 | 347 | if err := o.Complete(f, rootCmd, []string{}); err != nil { 348 | t.Errorf("unexpected error: %v", err) 349 | return 350 | } 351 | }) 352 | 353 | t.Run("complete specific namespace", func(t *testing.T) { 354 | 355 | o := &FreeOptions{ 356 | configFlags: genericclioptions.NewConfigFlags(true), 357 | } 358 | *o.configFlags.Namespace = "awesome-ns" 359 | 360 | if err := o.Complete(f, rootCmd, []string{}); err != nil { 361 | t.Errorf("unexpected error: %v", err) 362 | return 363 | } 364 | }) 365 | } 366 | 367 | func TestValidate(t *testing.T) { 368 | 369 | t.Run("validate threshold", func(t *testing.T) { 370 | 371 | o := &FreeOptions{ 372 | critThreshold: 5, 373 | warnThreshold: 10, 374 | } 375 | 376 | err := o.Validate() 377 | expected := "can not set critical threshold less than warn threshold (warn:10 crit:5)" 378 | if err.Error() != expected { 379 | t.Errorf("unexpected error: %v", err) 380 | return 381 | } 382 | }) 383 | 384 | t.Run("validate success", func(t *testing.T) { 385 | 386 | o := &FreeOptions{ 387 | warnThreshold: 25, 388 | critThreshold: 50, 389 | } 390 | 391 | if err := o.Validate(); err != nil { 392 | t.Errorf("unexpected error: %v", err) 393 | return 394 | } 395 | }) 396 | } 397 | 398 | func TestRun(t *testing.T) { 399 | 400 | var tests = []struct { 401 | description string 402 | args []string 403 | listOption bool 404 | expected []string 405 | }{ 406 | { 407 | "default free", 408 | []string{}, 409 | false, 410 | []string{ 411 | "NAME STATUS CPU/use CPU/req CPU/lim CPU/alloc CPU/use% CPU/req% CPU/lim% MEM/use MEM/req MEM/lim MEM/alloc MEM/use% MEM/req% MEM/lim%", 412 | "node1 Ready 100m 1 2 4 2% 25% 50% 1K 1K 2K 4K 25% 25% 50%", 413 | "", 414 | }, 415 | }, 416 | { 417 | "defaul free --list", 418 | []string{}, 419 | true, 420 | []string{ 421 | "NODE NAME NAMESPACE POD NAME POD AGE POD IP POD STATUS CONTAINER CPU/use CPU/req CPU/lim MEM/use MEM/req MEM/lim", 422 | "node1 default pod1 1.2.3.4 Running container1 10m 1 2 0K 1K 2K", 423 | "", 424 | }, 425 | }, 426 | } 427 | 428 | for _, test := range tests { 429 | t.Run(test.description, func(t *testing.T) { 430 | 431 | fakeNodeClient := fake.NewSimpleClientset(&testNodes[0]) 432 | fakePodClient := fake.NewSimpleClientset(&testPods[0]) 433 | fakeMetricsNodeClient := prepareTestNodeMetricsClient() 434 | fakeMetricsPodClient := prepareTestPodMetricsClient() 435 | 436 | buffer := &bytes.Buffer{} 437 | o := &FreeOptions{ 438 | nocolor: true, 439 | table: table.NewOutputTable(buffer), 440 | list: test.listOption, 441 | nodeClient: fakeNodeClient.CoreV1().Nodes(), 442 | podClient: fakePodClient.CoreV1().Pods("default"), 443 | metricsPodClient: fakeMetricsPodClient.MetricsV1beta1().PodMetricses("default"), 444 | metricsNodeClient: fakeMetricsNodeClient.MetricsV1beta1().NodeMetricses(), 445 | } 446 | 447 | o.prepareFreeTableHeader() 448 | o.prepareListTableHeader() 449 | 450 | if err := o.Run(test.args); err != nil { 451 | t.Errorf("unexpected error: %v", err) 452 | return 453 | } 454 | 455 | e := strings.Join(test.expected, "\n") 456 | if buffer.String() != e { 457 | t.Errorf("expected(%s) differ (got: %s)", e, buffer.String()) 458 | return 459 | } 460 | }) 461 | } 462 | } 463 | 464 | func TestPrepareFreeTableHeader(t *testing.T) { 465 | 466 | colorStatus := "STATUS" 467 | colorCPUreqP := "CPU/req%" 468 | colorCPUlimP := "CPU/lim%" 469 | colorMEMreqP := "MEM/req%" 470 | colorMEMlimP := "MEM/lim%" 471 | util.DefaultColor(&colorStatus) 472 | util.DefaultColor(&colorCPUreqP) 473 | util.DefaultColor(&colorCPUlimP) 474 | util.DefaultColor(&colorMEMreqP) 475 | util.DefaultColor(&colorMEMlimP) 476 | 477 | var tests = []struct { 478 | description string 479 | listPod bool 480 | nocolor bool 481 | noheader bool 482 | nometrics bool 483 | expected []string 484 | }{ 485 | { 486 | "default header", 487 | false, 488 | true, 489 | false, 490 | true, 491 | []string{ 492 | "NAME", 493 | "STATUS", 494 | "CPU/req", 495 | "CPU/lim", 496 | "CPU/alloc", 497 | "CPU/req%", 498 | "CPU/lim%", 499 | "MEM/req", 500 | "MEM/lim", 501 | "MEM/alloc", 502 | "MEM/req%", 503 | "MEM/lim%", 504 | }, 505 | }, 506 | { 507 | "default header with metrics", 508 | false, 509 | true, 510 | false, 511 | false, 512 | []string{ 513 | "NAME", 514 | "STATUS", 515 | "CPU/use", 516 | "CPU/req", 517 | "CPU/lim", 518 | "CPU/alloc", 519 | "CPU/use%", 520 | "CPU/req%", 521 | "CPU/lim%", 522 | "MEM/use", 523 | "MEM/req", 524 | "MEM/lim", 525 | "MEM/alloc", 526 | "MEM/use%", 527 | "MEM/req%", 528 | "MEM/lim%", 529 | }, 530 | }, 531 | { 532 | "default header with --pod", 533 | true, 534 | true, 535 | false, 536 | true, 537 | []string{ 538 | "NAME", 539 | "STATUS", 540 | "CPU/req", 541 | "CPU/lim", 542 | "CPU/alloc", 543 | "CPU/req%", 544 | "CPU/lim%", 545 | "MEM/req", 546 | "MEM/lim", 547 | "MEM/alloc", 548 | "MEM/req%", 549 | "MEM/lim%", 550 | "PODS", 551 | "PODS/alloc", 552 | "CONTAINERS", 553 | }, 554 | }, 555 | { 556 | "default header with color", 557 | false, 558 | false, 559 | false, 560 | true, 561 | []string{ 562 | "NAME", 563 | colorStatus, 564 | "CPU/req", 565 | "CPU/lim", 566 | "CPU/alloc", 567 | colorCPUreqP, 568 | colorCPUlimP, 569 | "MEM/req", 570 | "MEM/lim", 571 | "MEM/alloc", 572 | colorMEMreqP, 573 | colorMEMlimP, 574 | }, 575 | }, 576 | } 577 | 578 | for _, test := range tests { 579 | t.Run(test.description, func(t *testing.T) { 580 | o := &FreeOptions{ 581 | pod: test.listPod, 582 | noHeaders: test.noheader, 583 | noMetrics: test.nometrics, 584 | nocolor: test.nocolor, 585 | } 586 | o.prepareFreeTableHeader() 587 | 588 | if !reflect.DeepEqual(o.freeTableHeaders, test.expected) { 589 | t.Errorf( 590 | "[%s] expected(%v) differ (got: %v)", 591 | test.description, 592 | test.expected, 593 | o.freeTableHeaders, 594 | ) 595 | return 596 | } 597 | }) 598 | } 599 | } 600 | 601 | func TestPrepareListTableHeader(t *testing.T) { 602 | 603 | colorStatus := "POD STATUS" 604 | util.DefaultColor(&colorStatus) 605 | 606 | var tests = []struct { 607 | description string 608 | listImage bool 609 | nocolor bool 610 | noheader bool 611 | nometrics bool 612 | expected []string 613 | }{ 614 | { 615 | "default header", 616 | false, 617 | true, 618 | false, 619 | true, 620 | []string{ 621 | "NODE NAME", 622 | "NAMESPACE", 623 | "POD NAME", 624 | "POD AGE", 625 | "POD IP", 626 | "POD STATUS", 627 | "CONTAINER", 628 | "CPU/req", 629 | "CPU/lim", 630 | "MEM/req", 631 | "MEM/lim", 632 | }, 633 | }, 634 | { 635 | "default header with metrics", 636 | false, 637 | true, 638 | false, 639 | false, 640 | []string{ 641 | "NODE NAME", 642 | "NAMESPACE", 643 | "POD NAME", 644 | "POD AGE", 645 | "POD IP", 646 | "POD STATUS", 647 | "CONTAINER", 648 | "CPU/use", 649 | "CPU/req", 650 | "CPU/lim", 651 | "MEM/use", 652 | "MEM/req", 653 | "MEM/lim", 654 | }, 655 | }, 656 | { 657 | "default header with --list-image", 658 | true, 659 | true, 660 | false, 661 | true, 662 | []string{ 663 | "NODE NAME", 664 | "NAMESPACE", 665 | "POD NAME", 666 | "POD AGE", 667 | "POD IP", 668 | "POD STATUS", 669 | "CONTAINER", 670 | "CPU/req", 671 | "CPU/lim", 672 | "MEM/req", 673 | "MEM/lim", 674 | "IMAGE", 675 | }, 676 | }, 677 | { 678 | "default header with color", 679 | false, 680 | false, 681 | false, 682 | true, 683 | []string{ 684 | "NODE NAME", 685 | "NAMESPACE", 686 | "POD NAME", 687 | "POD AGE", 688 | "POD IP", 689 | colorStatus, 690 | "CONTAINER", 691 | "CPU/req", 692 | "CPU/lim", 693 | "MEM/req", 694 | "MEM/lim", 695 | }, 696 | }, 697 | } 698 | 699 | for _, test := range tests { 700 | t.Run(test.description, func(t *testing.T) { 701 | o := &FreeOptions{ 702 | listContainerImage: test.listImage, 703 | noHeaders: test.noheader, 704 | noMetrics: test.nometrics, 705 | nocolor: test.nocolor, 706 | } 707 | o.prepareListTableHeader() 708 | 709 | if !reflect.DeepEqual(o.listTableHeaders, test.expected) { 710 | t.Errorf( 711 | "[%s] expected(%v) differ (got: %v)", 712 | test.description, 713 | test.expected, 714 | o.listTableHeaders, 715 | ) 716 | return 717 | } 718 | }) 719 | } 720 | 721 | } 722 | 723 | func TestToUnit(t *testing.T) { 724 | 725 | var tests = []struct { 726 | description string 727 | input int64 728 | binPrefix bool 729 | withoutunit bool 730 | expected string 731 | }{ 732 | {"si prefix without unit", 12345, false, true, "12"}, 733 | {"si prefix with unit", 6000, false, false, "6K"}, 734 | {"binary prefix without unit", 12345, true, true, "12"}, 735 | {"binary prefix with unit", 6000, true, false, "5Ki"}, 736 | {"0 case", 0, true, false, "0Ki"}, 737 | } 738 | 739 | o := &FreeOptions{ 740 | bytes: false, 741 | kByte: true, 742 | mByte: false, 743 | gByte: false, 744 | } 745 | 746 | for _, test := range tests { 747 | t.Run(test.description, func(t *testing.T) { 748 | o.withoutUnit = test.withoutunit 749 | o.binPrefix = test.binPrefix 750 | actual := o.toUnit(test.input) 751 | if actual != test.expected { 752 | t.Errorf( 753 | "[%s] expected(%s) differ (got: %s)", 754 | test.description, 755 | test.expected, 756 | actual, 757 | ) 758 | return 759 | } 760 | }) 761 | } 762 | } 763 | 764 | func TestToUnitOrDash(t *testing.T) { 765 | 766 | var tests = []struct { 767 | description string 768 | input int64 769 | binPrefix bool 770 | withoutunit bool 771 | expected string 772 | }{ 773 | {"si prefix without unit", 12345, false, true, "12"}, 774 | {"si prefix with unit", 6000, false, false, "6K"}, 775 | {"binary prefix without unit", 12345, true, true, "12"}, 776 | {"binary prefix with unit", 6000, true, false, "5Ki"}, 777 | {"0 case", 0, true, false, "-"}, 778 | } 779 | 780 | o := &FreeOptions{ 781 | bytes: false, 782 | kByte: true, 783 | mByte: false, 784 | gByte: false, 785 | } 786 | 787 | for _, test := range tests { 788 | t.Run(test.description, func(t *testing.T) { 789 | o.withoutUnit = test.withoutunit 790 | o.binPrefix = test.binPrefix 791 | actual := o.toUnitOrDash(test.input) 792 | if actual != test.expected { 793 | t.Errorf( 794 | "[%s] expected(%s) differ (got: %s)", 795 | test.description, 796 | test.expected, 797 | actual, 798 | ) 799 | return 800 | } 801 | }) 802 | } 803 | } 804 | 805 | func TestToMilliUnitOrDash(t *testing.T) { 806 | 807 | var tests = []struct { 808 | description string 809 | i int64 810 | withoutunit bool 811 | expected string 812 | }{ 813 | {"return dash", 0, false, "-"}, 814 | {"return unit", 1500, false, "1500m"}, 815 | {"return no unit", 1500, true, "1500"}, 816 | } 817 | 818 | for _, test := range tests { 819 | t.Run(test.description, func(t *testing.T) { 820 | o := &FreeOptions{ 821 | withoutUnit: test.withoutunit, 822 | } 823 | actual := o.toMilliUnitOrDash(test.i) 824 | if actual != test.expected { 825 | t.Errorf( 826 | "[%s] expected(%s) differ (got: %s)", 827 | test.description, 828 | test.expected, 829 | actual, 830 | ) 831 | return 832 | } 833 | }) 834 | } 835 | } 836 | 837 | func TestToColorPercent(t *testing.T) { 838 | 839 | var tests = []struct { 840 | description string 841 | p int64 842 | warn int64 843 | crit int64 844 | nocolor bool 845 | expected string 846 | }{ 847 | {"p < w", 10, 25, 50, false, color.Green.Sprint("10%")}, 848 | {"w < p < c", 30, 25, 50, false, color.Yellow.Sprint("30%")}, 849 | {"c < p", 80, 25, 50, false, color.Red.Sprint("80%")}, 850 | {"p < w with nocolor", 10, 25, 50, true, "10%"}, 851 | } 852 | 853 | for _, test := range tests { 854 | t.Run(test.description, func(t *testing.T) { 855 | o := &FreeOptions{ 856 | warnThreshold: test.warn, 857 | critThreshold: test.crit, 858 | nocolor: test.nocolor, 859 | } 860 | actual := o.toColorPercent(test.p) 861 | actualB := []byte(actual) 862 | expectedB := []byte(test.expected) 863 | if !bytes.Equal(actualB, expectedB) { 864 | t.Errorf( 865 | "[%s] expected(%v) differ (got: %v)", 866 | test.description, 867 | expectedB, 868 | actualB, 869 | ) 870 | return 871 | } 872 | }) 873 | } 874 | 875 | } 876 | 877 | // Test Helper 878 | func executeCommand(root *cobra.Command, args ...string) (output string, err error) { 879 | _, output, err = executeCommandC(root, args...) 880 | return output, err 881 | } 882 | 883 | func executeCommandC(root *cobra.Command, args ...string) (c *cobra.Command, output string, err error) { 884 | buf := new(bytes.Buffer) 885 | root.SetOutput(buf) 886 | root.SetArgs(args) 887 | 888 | c, err = root.ExecuteC() 889 | 890 | return c, buf.String(), err 891 | } 892 | 893 | func prepareTestNodeMetricsClient() *fakemetrics.Clientset { 894 | fakeMetricsClient := &fakemetrics.Clientset{} 895 | fakeMetricsClient.AddReactor("get", "nodes", func(action core.Action) (handled bool, ret runtime.Object, err error) { 896 | return true, &testNodeMetrics.Items[0], nil 897 | }) 898 | return fakeMetricsClient 899 | } 900 | 901 | func prepareTestPodMetricsClient() *fakemetrics.Clientset { 902 | fakeMetricsClient := &fakemetrics.Clientset{} 903 | fakeMetricsClient.AddReactor("list", "pods", func(action core.Action) (handled bool, ret runtime.Object, err error) { 904 | return true, testPodMetrics, nil 905 | }) 906 | return fakeMetricsClient 907 | } 908 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= 4 | github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= 5 | github.com/Azure/go-autorest v11.1.2+incompatible h1:viZ3tV5l4gE2Sw0xrasFHytCGtzYCrT+um/rrSQ1BfA= 6 | github.com/Azure/go-autorest v11.1.2+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 7 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 8 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 9 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 10 | github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e h1:eb0Pzkt15Bm7f2FFYv7sjY7NPFi3cPkS3tv1CcrFBWA= 11 | github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= 12 | github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= 13 | github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2 h1:HTOmFEEYrWi4MW5ZKUx6xfeyM10Sx3kQF65xiQJMPYA= 14 | github.com/OpenPeeDeeP/depguard v0.0.0-20180806142446-a69c782687b2/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o= 15 | github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 16 | github.com/PuerkitoBio/purell v1.1.0 h1:rmGxhojJlM0tuKtfdvliR84CFHljx9ag64t2xmVkjK4= 17 | github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 18 | github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= 19 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 20 | github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 21 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 22 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 23 | github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= 24 | github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 27 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda h1:NyywMz59neOoVRFDz+ccfKWxn784fiHMDnZSy6T+JXY= 29 | github.com/dgrijalva/jwt-go v0.0.0-20160705203006-01aeca54ebda/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 30 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96 h1:cenwrSVm+Z7QLSV/BsnenAOcDXdX4cMv4wP0B/5QbPg= 31 | github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= 32 | github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e h1:p1yVGRW3nmb85p1Sh1ZJSDm4A4iKLS5QNbvUHMgGu/M= 33 | github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= 34 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633 h1:H2pdYOb3KQ1/YsqVWoWNLQO+fusocsw354rqGTZtAgw= 35 | github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 36 | github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk= 37 | github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= 38 | github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550 h1:mV9jbLoSW/8m4VK16ZkHTozJa8sesK5u5kTMFysTYac= 39 | github.com/evanphx/json-patch v0.0.0-20190203023257-5858425f7550/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 40 | github.com/evanphx/json-patch v4.2.0+incompatible h1:fUDGZCv/7iAN7u0puUVhvKCcsR6vRfwrJatElLBEf0I= 41 | github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 42 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= 43 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= 44 | github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= 45 | github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= 46 | github.com/fatih/color v1.6.0 h1:66qjqZk8kalYAvDRtM1AdAJQI0tj4Wrue3Eq3B3pmFU= 47 | github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 48 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 49 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 50 | github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 51 | github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4 h1:bRzFpEzvausOAt4va+I/22BZ1vXDtERngp0BNYDKej0= 52 | github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 53 | github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540 h1:7CU1IXBpPvxpQ/NqJrpuMXMHAw+FB2vfqtRF8tgW9fw= 54 | github.com/go-critic/go-critic v0.0.0-20181204210945-1df300866540/go.mod h1:+sE8vrLDS2M0pZkBk0wy6+nLdKexVDrl/jBqQOTDThA= 55 | github.com/go-lintpack/lintpack v0.5.2 h1:DI5mA3+eKdWeJ40nU4d6Wc26qmdG8RCi/btYq0TuRN0= 56 | github.com/go-lintpack/lintpack v0.5.2/go.mod h1:NwZuYi2nUHho8XEIZ6SIxihrnPoqBTDqfpXvXAN0sXM= 57 | github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= 58 | github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= 59 | github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= 60 | github.com/go-openapi/jsonpointer v0.19.0 h1:FTUMcX77w5rQkClIzDtTxvn6Bsa894CcrzNj2MMfeg8= 61 | github.com/go-openapi/jsonpointer v0.19.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= 62 | github.com/go-openapi/jsonpointer v0.19.2 h1:A9+F4Dc/MCNB5jibxf6rRvOvR/iFgQdyNx9eIhnGqq0= 63 | github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= 64 | github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= 65 | github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= 66 | github.com/go-openapi/jsonreference v0.19.0 h1:BqWKpV1dFd+AuiKlgtddwVIFQsuMpxfBDBHGfM2yNpk= 67 | github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= 68 | github.com/go-openapi/jsonreference v0.19.2 h1:o20suLFB4Ri0tuzpWtyHlh7E7HnkqTNLq6aR6WVNS1w= 69 | github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= 70 | github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= 71 | github.com/go-openapi/spec v0.17.2 h1:eb2NbuCnoe8cWAxhtK6CfMWUYmiFEZJ9Hx3Z2WRwJ5M= 72 | github.com/go-openapi/spec v0.17.2/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= 73 | github.com/go-openapi/spec v0.19.2 h1:SStNd1jRcYtfKCN7R0laGNs80WYYvn5CbBjM2sOmCrE= 74 | github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY= 75 | github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= 76 | github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= 77 | github.com/go-openapi/swag v0.17.2 h1:K/ycE/XTUDFltNHSO32cGRUhrVGJD64o8WgAIZNyc3k= 78 | github.com/go-openapi/swag v0.17.2/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= 79 | github.com/go-openapi/swag v0.19.2 h1:jvO6bCMBEilGwMfHhrd61zIID4oIFdwb76V17SM88dE= 80 | github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 81 | github.com/go-toolsmith/astcast v1.0.0 h1:JojxlmI6STnFVG9yOImLeGREv8W2ocNUM+iOhR6jE7g= 82 | github.com/go-toolsmith/astcast v1.0.0/go.mod h1:mt2OdQTeAQcY4DQgPSArJjHCcOwlX+Wl/kwN+LbLGQ4= 83 | github.com/go-toolsmith/astcopy v1.0.0 h1:OMgl1b1MEpjFQ1m5ztEO06rz5CUd3oBv9RF7+DyvdG8= 84 | github.com/go-toolsmith/astcopy v1.0.0/go.mod h1:vrgyG+5Bxrnz4MZWPF+pI4R8h3qKRjjyvV/DSez4WVQ= 85 | github.com/go-toolsmith/astequal v0.0.0-20180903214952-dcb477bfacd6/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= 86 | github.com/go-toolsmith/astequal v1.0.0 h1:4zxD8j3JRFNyLN46lodQuqz3xdKSrur7U/sr0SDS/gQ= 87 | github.com/go-toolsmith/astequal v1.0.0/go.mod h1:H+xSiq0+LtiDC11+h1G32h7Of5O3CYFJ99GVbS5lDKY= 88 | github.com/go-toolsmith/astfmt v0.0.0-20180903215011-8f8ee99c3086/go.mod h1:mP93XdblcopXwlyN4X4uodxXQhldPGZbcEJIimQHrkg= 89 | github.com/go-toolsmith/astfmt v1.0.0 h1:A0vDDXt+vsvLEdbMFJAUBI/uTbRw1ffOPnxsILnFL6k= 90 | github.com/go-toolsmith/astfmt v1.0.0/go.mod h1:cnWmsOAuq4jJY6Ct5YWlVLmcmLMn1JUPuQIHCY7CJDw= 91 | github.com/go-toolsmith/astinfo v0.0.0-20180906194353-9809ff7efb21/go.mod h1:dDStQCHtmZpYOmjRP/8gHHnCCch3Zz3oEgCdZVdtweU= 92 | github.com/go-toolsmith/astp v0.0.0-20180903215135-0af7e3c24f30/go.mod h1:SV2ur98SGypH1UjcPpCatrV5hPazG6+IfNHbkDXBRrk= 93 | github.com/go-toolsmith/astp v1.0.0 h1:alXE75TXgcmupDsMK1fRAy0YUzLzqPVvBKoyWV+KPXg= 94 | github.com/go-toolsmith/astp v1.0.0/go.mod h1:RSyrtpVlfTFGDYRbrjyWP1pYu//tSFcvdYrA8meBmLI= 95 | github.com/go-toolsmith/pkgload v0.0.0-20181119091011-e9e65178eee8/go.mod h1:WoMrjiy4zvdS+Bg6z9jZH82QXwkcgCBX6nOfnmdaHks= 96 | github.com/go-toolsmith/pkgload v1.0.0/go.mod h1:5eFArkbO80v7Z0kdngIxsRXRMTaX4Ilcwuh3clNrQJc= 97 | github.com/go-toolsmith/strparse v1.0.0 h1:Vcw78DnpCAKlM20kSbAyO4mPfJn/lyYA4BJUDxe2Jb4= 98 | github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= 99 | github.com/go-toolsmith/typep v1.0.0 h1:zKymWyA1TRYvqYrYDrfEMZULyrhcnGY3x7LDKU2XQaA= 100 | github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU= 101 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 102 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 103 | github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415 h1:WSBJMqJbLxsn+bTCPyPYZfqHdJmc8MK4wrBjMft6BAM= 104 | github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 105 | github.com/gogo/protobuf v0.0.0-20190410021324-65acae22fc9/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= 106 | github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= 107 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 108 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 109 | github.com/golang/mock v1.0.0 h1:HzcpUG60pfl43n9d2qbdi/3l1uKpAmxlfWEPWtV/QxM= 110 | github.com/golang/mock v1.0.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 111 | github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 112 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 113 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 114 | github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 h1:23T5iq8rbUYlhpt5DB4XJkc6BU31uODLD1o1gKvZmD0= 115 | github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= 116 | github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a h1:w8hkcTqaFpzKqonE9uMCefW1WDie15eSP/4MssdenaM= 117 | github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a/go.mod h1:ryS0uhF+x9jgbj/N71xsEqODy9BN81/GonCZiOzirOk= 118 | github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6 h1:i2jIkQFb8RG45DuQs+ElyROY848cSJIoIkBM+7XXypA= 119 | github.com/golangci/errcheck v0.0.0-20181003203344-ef45e06d44b6/go.mod h1:DbHgvLiFKX1Sh2T1w8Q/h4NAI8MHIpzCdnBUDTXU3I0= 120 | github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 h1:9kfjN3AdxcbsZBf8NjltjWihK2QfBBBZuv91cMFfDHw= 121 | github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613/go.mod h1:SyvUF2NxV+sN8upjjeVYr5W7tyxaT1JVtvhKhOn2ii8= 122 | github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196 h1:9rtVlONXLF1rJZzvLt4tfOXtnAFUEhxCJ64Ibzj6ECo= 123 | github.com/golangci/go-tools v0.0.0-20180109140146-af6baa5dc196/go.mod h1:unzUULGw35sjyOYjUt0jMTXqHlZPpPc6e+xfO4cd6mM= 124 | github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3 h1:pe9JHs3cHHDQgOFXJJdYkK6fLz2PWyYtP4hthoCMvs8= 125 | github.com/golangci/goconst v0.0.0-20180610141641-041c5f2b40f3/go.mod h1:JXrF4TWy4tXYn62/9x8Wm/K/dm06p8tCKwFRDPZG/1o= 126 | github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee h1:J2XAy40+7yz70uaOiMbNnluTg7gyQhtGqLQncQh+4J8= 127 | github.com/golangci/gocyclo v0.0.0-20180528134321-2becd97e67ee/go.mod h1:ozx7R9SIwqmqf5pRP90DhR2Oay2UIjGuKheCBCNwAYU= 128 | github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98 h1:ir6/L2ZOJfFrJlOTsuf/hlzdPuUwXV/VzkSlgS6f1vs= 129 | github.com/golangci/gofmt v0.0.0-20181105071733-0b8337e80d98/go.mod h1:9qCChq59u/eW8im404Q2WWTrnBUQKjpNYKMbU4M7EFU= 130 | github.com/golangci/golangci-lint v1.17.1 h1:lc8Hf9GPCjIr0hg3S/xhvFT1+Hydass8F1xchr8jkME= 131 | github.com/golangci/golangci-lint v1.17.1/go.mod h1:+5sJSl2h3aly+fpmL2meSP8CaSKua2E4Twi9LPy7b1g= 132 | github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547 h1:qMomh8bv+kDazm1dSLZ9S3zZ2PJZMHL4ilfBjxFOlmI= 133 | github.com/golangci/gosec v0.0.0-20180901114220-66fb7fc33547/go.mod h1:0qUabqiIQgfmlAmulqxyiGkkyF6/tOGSnY2cnPVwrzU= 134 | github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc h1:XRFao922N8F3EcIXBSNX8Iywk+GI0dxD/8FicMX2D/c= 135 | github.com/golangci/ineffassign v0.0.0-20180808204949-42439a7714cc/go.mod h1:e5tpTHCfVze+7EpLEozzMB3eafxo2KT5veNg1k6byQU= 136 | github.com/golangci/lint-1 v0.0.0-20180610141402-ee948d087217 h1:r7vyX+SN24x6+5AnpnrRn/bdwBb7U+McZqCHOVtXDuk= 137 | github.com/golangci/lint-1 v0.0.0-20180610141402-ee948d087217/go.mod h1:66R6K6P6VWk9I95jvqGxkqJxVWGFy9XlDwLwVz1RCFg= 138 | github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca h1:kNY3/svz5T29MYHubXix4aDDuE3RWHkPvopM/EDv/MA= 139 | github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca/go.mod h1:tvlJhZqDe4LMs4ZHD0oMUlt9G2LWuDGoisJTBzLMV9o= 140 | github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770 h1:EL/O5HGrF7Jaq0yNhBLucz9hTuRzj2LdwGBOaENgxIk= 141 | github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlSfYbMQDtrpRMQU675gSDLDNa8sCPPChZ7PhiVA= 142 | github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 h1:leSNB7iYzLYSSx3J/s5sVf4Drkc68W2wm4Ixh/mr0us= 143 | github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI= 144 | github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0 h1:HVfrLniijszjS1aiNg8JbBMO2+E1WIQ+j/gL4SQqGPg= 145 | github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= 146 | github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys= 147 | github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= 148 | github.com/google/btree v0.0.0-20160524151835-7d79101e329e h1:JHB7F/4TJCrYBW8+GZO8VkWDj1jxcWuCl6uxKODiyi4= 149 | github.com/google/btree v0.0.0-20160524151835-7d79101e329e/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 150 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 151 | github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= 152 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 153 | github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 154 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck= 155 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 156 | github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= 157 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 158 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 159 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 160 | github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 161 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d h1:7XGaL1e6bYS1yIonGp9761ExpPPV1ui0SAC59Yube9k= 162 | github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 163 | github.com/gookit/color v1.1.7 h1:WR5I/mhSHzemW2DzG54hTsUb7OzaREvkcmUG4/WST4Q= 164 | github.com/gookit/color v1.1.7/go.mod h1:R3ogXq2B9rTbXoSHJ1HyUVAZ3poOJHpd9nQmyGZsfvQ= 165 | github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8 h1:L9JPKrtsHMQ4VCRQfHvbbHBfB2Urn8xf6QZeXZ+OrN4= 166 | github.com/gophercloud/gophercloud v0.0.0-20190126172459-c818fa66e4c8/go.mod h1:3WdhXV3rUYy9p6AUW8d94kr+HS62Y4VL9mBnFxsD8q4= 167 | github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3 h1:JVnpOZS+qxli+rgVl98ILOXVNbW+kb5wcxeGx8ShUIw= 168 | github.com/gostaticanalysis/analysisutil v0.0.0-20190318220348-4088753ea4d3/go.mod h1:eEOZF4jCKGi+aprrirO9e7WKB3beBRtWgqGunKl6pKE= 169 | github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7 h1:6TSoaYExHper8PYsJu23GWVNOyYRCSnIFyxKgLSZ54w= 170 | github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 171 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 172 | github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce h1:xdsDDbiBDQTKASoGEZ+pEmF1OnWuu8AQ9I8iNbHNeno= 173 | github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w= 174 | github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= 175 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 176 | github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q= 177 | github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 178 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 179 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 180 | github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 181 | github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be h1:AHimNtVIpiBjPUhEF5KNCkrUyqTSA5zWUl8sQ2bfGBE= 182 | github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 183 | github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= 184 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 185 | github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= 186 | github.com/kisielk/gotool v0.0.0-20161130080628-0de1eaf82fa3/go.mod h1:jxZFDH7ILpTPQTk+E2s+z4CUas9lVNjIuKR4c5/zKgM= 187 | github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= 188 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 189 | github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 190 | github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 191 | github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 192 | github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 193 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 194 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 195 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 196 | github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= 197 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 198 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 199 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= 200 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= 201 | github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 202 | github.com/magiconair/properties v1.7.6 h1:U+1DqNen04MdEPgFiIwdOUiqZ8qPa37xgogX/sd3+54= 203 | github.com/magiconair/properties v1.7.6/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 204 | github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 205 | github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic= 206 | github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 207 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63 h1:nTT4s92Dgz2HlrB2NaMgvlfqHH39OgMhA7z3PK7PGD4= 208 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 209 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 210 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 211 | github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= 212 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 213 | github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= 214 | github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= 215 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 216 | github.com/mitchellh/go-ps v0.0.0-20170309133038-4fdf99ab2936/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= 217 | github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= 218 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 219 | github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238 h1:+MZW2uvHgN8kYvksEN3f7eFL2wpzk0GxmlFsMybWc7E= 220 | github.com/mitchellh/mapstructure v0.0.0-20180220230111-00c29f56e238/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 221 | github.com/moby/moby v0.7.3-0.20190607191414-238f8eaa31aa h1:e8RR0vsoCbUqmmG1SPnP+pr10w59Gxvxg6W8rJ0oTVc= 222 | github.com/moby/moby v0.7.3-0.20190607191414-238f8eaa31aa/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= 223 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 224 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 225 | github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 226 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 227 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 228 | github.com/mozilla/tls-observatory v0.0.0-20180409132520-8791a200eb40/go.mod h1:SrKMQvPiws7F7iqYp8/TX+IhxCYhzr6N/1yb8cwHsGk= 229 | github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 230 | github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= 231 | github.com/nbutton23/zxcvbn-go v0.0.0-20160627004424-a22cb81b2ecd/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= 232 | github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663 h1:Ri1EhipkbhWsffPJ3IPlrb4SkTOPa2PfRXp3jchBczw= 233 | github.com/nbutton23/zxcvbn-go v0.0.0-20171102151520-eafdab6b0663/go.mod h1:o96djdrsSGy3AWPyBgZMAGfxZNfgntdJG+11KU4QvbU= 234 | github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 235 | github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= 236 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 237 | github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= 238 | github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 239 | github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= 240 | github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3 h1:EooPXg51Tn+xmWPXJUGCnJhJSpeuMlBmfJVcqIRmmv8= 241 | github.com/onsi/gomega v0.0.0-20190113212917-5533ce8a0da3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 242 | github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 243 | github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo= 244 | github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 245 | github.com/pelletier/go-toml v1.1.0 h1:cmiOvKzEunMsAxyhXSzpL5Q1CRKpVv0KQsnAIcSEVYM= 246 | github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 247 | github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= 248 | github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= 249 | github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= 250 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 251 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 252 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 253 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 254 | github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1:5STLWrekHfjyYwxBRVRXNOSewLJ3PWfDJd1VyTS21fI= 255 | github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= 256 | github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 257 | github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= 258 | github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= 259 | github.com/ryanuber/go-glob v0.0.0-20170128012129-256dc444b735/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 260 | github.com/shirou/gopsutil v0.0.0-20180427012116-c95755e4bcd7/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= 261 | github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= 262 | github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= 263 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= 264 | github.com/sirupsen/logrus v1.0.5 h1:8c8b5uO0zS4X6RPl/sd1ENwSkIc0/H2PaHxE3udaE8I= 265 | github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= 266 | github.com/sourcegraph/go-diff v0.5.1 h1:gO6i5zugwzo1RVTvgvfwCOSVegNuvnNi6bAD1QCmkHs= 267 | github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE= 268 | github.com/spf13/afero v1.1.0 h1:bopulORc2JeYaxfHLvJa5NzxviA9PoWhpiiJkru7Ji4= 269 | github.com/spf13/afero v1.1.0/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 270 | github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg= 271 | github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg= 272 | github.com/spf13/cobra v0.0.0-20180319062004-c439c4fa0937 h1:+ryWjMVzFAkEz5zT+Ms49aROZwxlJce3x3zLTFpkz3Y= 273 | github.com/spf13/cobra v0.0.0-20180319062004-c439c4fa0937/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 274 | github.com/spf13/cobra v0.0.2 h1:NfkwRbgViGoyjBKsLI0QMDcuMnhM+SBg3T0cGfpvKDE= 275 | github.com/spf13/cobra v0.0.2/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 276 | github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec h1:2ZXvIUGghLpdTVHR1UfvfrzoVlZaE/yOWC5LueIHZig= 277 | github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 278 | github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 279 | github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= 280 | github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 281 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 282 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 283 | github.com/spf13/viper v1.0.2 h1:Ncr3ZIuJn322w2k1qmzXDnkLAdQMlJqBa9kfAH+irso= 284 | github.com/spf13/viper v1.0.2/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= 285 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 286 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 287 | github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 288 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 289 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 290 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 291 | github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb h1:lI9ufgFfvuqRctP9Ny8lDDLbSWCMxBPletcSqrnyFYM= 292 | github.com/timakin/bodyclose v0.0.0-20190407043127-4a873e97b2bb/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk= 293 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 294 | github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s= 295 | github.com/valyala/quicktemplate v1.1.1/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4= 296 | github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= 297 | golang.org/x/crypto v0.0.0-20181025213731-e84da0312774 h1:a4tQYYYuK9QdeO/+kEvNYyuR21S+7ve5EANok6hABhI= 298 | golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 299 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 300 | golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 301 | golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 h1:1wopBVtVdWnn03fZelqdXTqk7U7zPQCb+T4rbU9ZEoU= 302 | golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 303 | golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 304 | golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 305 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 306 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 307 | golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 308 | golang.org/x/net v0.0.0-20170915142106-8351a756f30f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 309 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 310 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 311 | golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 312 | golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 313 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 314 | golang.org/x/net v0.0.0-20190206173232-65e2d4e15006 h1:bfLnR+k0tq5Lqt6dflRLcZiz6UaXCMt3vhYJ1l4FQ80= 315 | golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 316 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 317 | golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 318 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 319 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= 320 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 321 | golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA= 322 | golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 323 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 324 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= 325 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 326 | golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= 327 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 328 | golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 329 | golang.org/x/sys v0.0.0-20171026204733-164713f0dfce/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 330 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 331 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 332 | golang.org/x/sys v0.0.0-20190312061237-fead79001313 h1:pczuHS43Cp2ktBEEmLwScxgjWsBSzdaQiKzUyf3DTTc= 333 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 334 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 335 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f h1:25KHgbfyiSm6vwQLbM3zZIe1v9p/3ea4Rz+nnM5K/i4= 336 | golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 337 | golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 338 | golang.org/x/text v0.0.0-20170915090833-1cbadb444a80/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 339 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 340 | golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db h1:6/JqlYfC1CCaLnGceQTI+sDGhC9UBSPAsBqI0Gun6kU= 341 | golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 342 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 343 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 344 | golang.org/x/time v0.0.0-20161028155119-f51c12702a4d h1:TnM+PKb3ylGmZvyPXmo9m/wktg7Jn/a/fNmr33HSj8g= 345 | golang.org/x/time v0.0.0-20161028155119-f51c12702a4d/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 346 | golang.org/x/tools v0.0.0-20170915040203-e531a2a1c15f/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 347 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 348 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 349 | golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 350 | golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 351 | golang.org/x/tools v0.0.0-20181117154741-2ddaf7f79a09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 352 | golang.org/x/tools v0.0.0-20190110163146-51295c7ec13a/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 353 | golang.org/x/tools v0.0.0-20190121143147-24cd39ecf745/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 354 | golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 355 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 356 | golang.org/x/tools v0.0.0-20190311215038-5c2858a9cfe5/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 357 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 358 | golang.org/x/tools v0.0.0-20190322203728-c1a832b0ad89/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 359 | golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 360 | golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59 h1:QjA/9ArTfVTLfEhClDCG7SGrZkZixxWpwNCDiwJfh88= 361 | golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 362 | gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= 363 | gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= 364 | gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= 365 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 366 | google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c= 367 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 368 | gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= 369 | gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= 370 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 371 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 372 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 373 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 374 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 375 | gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= 376 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 377 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0= 378 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= 379 | gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o= 380 | gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 381 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 382 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 383 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 384 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 385 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 386 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 387 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 388 | gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= 389 | gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= 390 | k8s.io/api v0.0.0-20190531132109-d3f5f50bdd94 h1:cOORHfZy+Z+aXlYvVt8GAx2E6wY1YbQaXsw4y6b1Y4k= 391 | k8s.io/api v0.0.0-20190531132109-d3f5f50bdd94/go.mod h1:6MLSFN0Tl4sFDV6wvMXrHhny4RHx1A3U4l5/v3arnwE= 392 | k8s.io/api v0.0.0-20190726022912-69e1bce1dad5 h1:vSfC/FjyeuqXC/fjdNqZixNpeec4mEHJ68K3kzetm/M= 393 | k8s.io/api v0.0.0-20190726022912-69e1bce1dad5/go.mod h1:V6cpJ9D7WqSy0wqcE096gcbj+W//rshgQgmj1Shdwi8= 394 | k8s.io/apimachinery v0.0.0-20190531131812-859a0ba5e71a h1:DmmpwcLqiIGWqD+uqzL+JYqE5G1GNjvYWY2GVm378SA= 395 | k8s.io/apimachinery v0.0.0-20190531131812-859a0ba5e71a/go.mod h1:u/2VL7tgEMV0FFTV9q0JO+7cnTsV44LP8Pmx41R4AQ4= 396 | k8s.io/apimachinery v0.0.0-20190726022757-641a75999153 h1:Mg7TTs6b/jjTI6dMHGZM9/dbvHDAPCxnKyqK2ag9pAs= 397 | k8s.io/apimachinery v0.0.0-20190726022757-641a75999153/go.mod h1:eXR4ljjmbwK6Ng0PKsXRySPXnTUy/qBUa6kPDeckhQ0= 398 | k8s.io/cli-runtime v0.0.0-20190531135611-d60f41fb4dc3 h1:WPwjHYuZolUsVZPy0w3t6zbBqiYv5vswhRe+Fw0ItgM= 399 | k8s.io/cli-runtime v0.0.0-20190531135611-d60f41fb4dc3/go.mod h1:jcoOg0vH99jDkFZSsMpce5Y7withY5JXJgSi+HBgijk= 400 | k8s.io/client-go v0.0.0-20190531132438-d58e65e5f4b1 h1:P6ePeisThSJsRbzJ4ZetEGkE5dOrMZQzVkdBoB5GFcA= 401 | k8s.io/client-go v0.0.0-20190531132438-d58e65e5f4b1/go.mod h1:oBWDlQWEpK7nPTuJljTF4w6W/zjopIHAPOJu70zfOHE= 402 | k8s.io/code-generator v0.0.0-20190726022633-14ba7d03f06f/go.mod h1:kr7tMYxZEaP3mrijPwXnhxOvPyqdJw6TZH87KfFboQ0= 403 | k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= 404 | k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 405 | k8s.io/klog v0.3.1/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 406 | k8s.io/klog v0.3.2 h1:qvP/U6CcZ6qyi/qSHlJKdlAboCzo3mT0DAm0XAarpz4= 407 | k8s.io/klog v0.3.2/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= 408 | k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30 h1:TRb4wNWoBVrH9plmkp2q86FIDppkbrEXdXlxU3a3BMI= 409 | k8s.io/kube-openapi v0.0.0-20190228160746-b3a7cee44a30/go.mod h1:BXM9ceUBTj2QnfH2MK1odQs778ajze1RxcmP6S8RVVc= 410 | k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058 h1:di3XCwddOR9cWBNpfgXaskhh6cgJuwcK54rvtwUaC10= 411 | k8s.io/kube-openapi v0.0.0-20190709113604-33be087ad058/go.mod h1:nfDlWeOsu3pUf4yWGL+ERqohP4YsZcBJXWMK+gkzOA4= 412 | k8s.io/kubernetes v1.14.2 h1:Gdq2hPpttbaJBoClIanCE6WSu4IZReA54yhkZtvPUOo= 413 | k8s.io/kubernetes v1.14.2/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= 414 | k8s.io/metrics v0.0.0-20190726024513-9140f5fe6ab8 h1:s4Nt0kvkF66apJd6A7zLe313d2QWqW3OZtY+0yREW70= 415 | k8s.io/metrics v0.0.0-20190726024513-9140f5fe6ab8/go.mod h1:qKj9Rmzs1afkjgcziJ2/HzORbC8bXn457iAFxXUDykA= 416 | k8s.io/utils v0.0.0-20190221042446-c2654d5206da h1:ElyM7RPonbKnQqOcw7dG2IK5uvQQn3b/WPHqD5mBvP4= 417 | k8s.io/utils v0.0.0-20190221042446-c2654d5206da/go.mod h1:8k8uAuAQ0rXslZKaEWd0c3oVhZz7sSzSiPnVZayjIX0= 418 | modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= 419 | modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= 420 | modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= 421 | modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= 422 | modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= 423 | mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed h1:WX1yoOaKQfddO/mLzdV4wptyWgoH/6hwLs7QHTixo0I= 424 | mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= 425 | mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b h1:DxJ5nJdkhDlLok9K6qO+5290kphDJbHOQO1DFFFTeBo= 426 | mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= 427 | mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34 h1:B1LAOfRqg2QUyCdzfjf46quTSYUTAK5OCwbh6pljHbM= 428 | mvdan.cc/unparam v0.0.0-20190124213536-fbb59629db34/go.mod h1:H6SUd1XjIs+qQCyskXg5OFSrilMRUkD8ePJpHKDPaeY= 429 | sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0= 430 | sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= 431 | sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= 432 | sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= 433 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 434 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4 h1:JPJh2pk3+X4lXAkZIk2RuE/7/FoK9maXw+TNPJhVS/c= 435 | sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= 436 | --------------------------------------------------------------------------------