From 998344bf1041867dc690c339e123cffc899fa616 Mon Sep 17 00:00:00 2001 From: Andrew Suderman Date: Thu, 26 Mar 2020 16:46:10 -0600 Subject: [PATCH 1/4] Testing output. Rename to DisplayOutput. Fix #9. Handle more cases --- cmd/root.go | 55 +--------------------------- go.mod | 1 + go.sum | 1 + pkg/api/output.go | 73 +++++++++++++++++++++++++++++++++++++ pkg/api/output_test.go | 83 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 160 insertions(+), 53 deletions(-) create mode 100644 pkg/api/output.go create mode 100644 pkg/api/output_test.go diff --git a/cmd/root.go b/cmd/root.go index 9232fe96..a65e0bfc 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -15,13 +15,9 @@ package cmd import ( - "encoding/json" "flag" "fmt" "os" - "text/tabwriter" - - "gopkg.in/yaml.v3" "github.com/fairwindsops/pluto/pkg/api" "github.com/fairwindsops/pluto/pkg/finder" @@ -91,7 +87,7 @@ var detectFilesCmd = &cobra.Command{ os.Exit(0) } - err = parseOutput(dir.Outputs) + err = api.DisplayOutput(dir.Outputs, outputFormat, showNonDeprecated) if err != nil { fmt.Println("Error Parsing Output:", err) os.Exit(1) @@ -110,7 +106,7 @@ var detectHelmCmd = &cobra.Command{ fmt.Println("Error running helm-detect:", err) os.Exit(1) } - err = parseOutput(h.Outputs) + err = api.DisplayOutput(h.Outputs, outputFormat, showNonDeprecated) if err != nil { fmt.Println("Error Parsing Output:", err) os.Exit(1) @@ -127,50 +123,3 @@ func Execute(VERSION string, COMMIT string) { os.Exit(1) } } - -func parseOutput(outputs []*api.Output) error { - var err error - var outData []byte - switch outputFormat { - case "tabular": - w := new(tabwriter.Writer) - w.Init(os.Stdout, 0, 8, 2, ' ', 0) - _, err = fmt.Fprintln(w, "KIND\t VERSION\t DEPRECATED\t RESOURCE NAME") - if err != nil { - return err - } - for _, output := range outputs { - // Don't show non-deprecated apis if we have them disabled - if !showNonDeprecated { - if !output.APIVersion.Deprecated { - continue - } - } - kind := output.APIVersion.Kind - deprecated := fmt.Sprintf("%t", output.APIVersion.Deprecated) - version := output.APIVersion.Name - fileName := output.Name - - _, err = fmt.Fprintf(w, "%s\t %s\t %s\t %s\t\n", kind, version, deprecated, fileName) - if err != nil { - return err - } - } - err = w.Flush() - if err != nil { - return err - } - case "json": - outData, err = json.Marshal(outputs) - if err != nil { - return err - } - case "yaml": - outData, err = yaml.Marshal(outputs) - if err != nil { - return err - } - } - fmt.Println(string(outData)) - return nil -} diff --git a/go.mod b/go.mod index d77fbbc8..009250aa 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/spf13/cobra v0.0.6 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.4.0 + gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c k8s.io/apimachinery v0.17.4 k8s.io/client-go v0.17.4 diff --git a/go.sum b/go.sum index 8c3a1223..60e382eb 100644 --- a/go.sum +++ b/go.sum @@ -265,6 +265,7 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/pkg/api/output.go b/pkg/api/output.go new file mode 100644 index 00000000..6595ab94 --- /dev/null +++ b/pkg/api/output.go @@ -0,0 +1,73 @@ +package api + +import ( + "encoding/json" + "fmt" + "os" + "text/tabwriter" + + "gopkg.in/yaml.v2" +) + +// DisplayOutput prints the output based on desired variables +func DisplayOutput(outputs []*Output, outputFormat string, showNonDeprecated bool) error { + if len(outputs) == 0 { + fmt.Println("There were no apiVersions found that match our records.") + return nil + } + var err error + var outData []byte + var usableOutputs []*Output + switch outputFormat { + case "tabular": + if showNonDeprecated { + usableOutputs = outputs + } else { + for _, output := range outputs { + if output.APIVersion.Deprecated { + usableOutputs = append(usableOutputs, output) + } + } + } + if len(usableOutputs) == 0 { + fmt.Println("APIVersions were found, but none were deprecated. Try --show-non-deprecated.") + return nil + } + w := new(tabwriter.Writer) + w.Init(os.Stdout, 0, 8, 2, ' ', 0) + _, err = fmt.Fprintln(w, "KIND\t VERSION\t DEPRECATED\t RESOURCE NAME") + if err != nil { + return err + } + for _, output := range usableOutputs { + kind := output.APIVersion.Kind + deprecated := fmt.Sprintf("%t", output.APIVersion.Deprecated) + version := output.APIVersion.Name + fileName := output.Name + + _, err = fmt.Fprintf(w, "%s\t %s\t %s\t %s\t\n", kind, version, deprecated, fileName) + if err != nil { + return err + } + } + err = w.Flush() + if err != nil { + return err + } + case "json": + outData, err = json.Marshal(outputs) + if err != nil { + return err + } + case "yaml": + outData, err = yaml.Marshal(outputs) + if err != nil { + return err + } + default: + fmt.Println("output format should be one of (json,yaml,tabular)") + } + + fmt.Println(string(outData)) + return nil +} diff --git a/pkg/api/output_test.go b/pkg/api/output_test.go new file mode 100644 index 00000000..80a776d6 --- /dev/null +++ b/pkg/api/output_test.go @@ -0,0 +1,83 @@ +// Copyright 2020 Fairwinds +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License + +package api + +var testOutput1 = &Output{ + Name: "some name one", + APIVersion: &Version{ + Name: "apps/v1", + Kind: "Deployment", + Deprecated: false, + }, +} +var testOutput2 = &Output{ + Name: "some name two", + APIVersion: &Version{ + Name: "extensions/v1beta1", + Kind: "Deployment", + Deprecated: true, + }, +} + +func ExampleDisplayOutput_showNonDeprecated() { + _ = DisplayOutput([]*Output{testOutput1}, "tabular", true) + + // Output: + // KIND VERSION DEPRECATED RESOURCE NAME + // Deployment apps/v1 false some name one +} + +func ExampleDisplayOutput() { + _ = DisplayOutput([]*Output{testOutput1, testOutput2}, "tabular", false) + + // Output: + // KIND VERSION DEPRECATED RESOURCE NAME + // Deployment extensions/v1beta1 true some name two +} + +func ExampleDisplayOutput_json() { + _ = DisplayOutput([]*Output{testOutput1}, "json", true) + + // Output: + // [{"file":"some name one","api":{"version":"apps/v1","kind":"Deployment"}}] +} + +func ExampleDisplayOutput_yaml() { + _ = DisplayOutput([]*Output{testOutput1}, "yaml", true) + + // Output: + // - file: some name one + // api: + // version: apps/v1 + // kind: Deployment +} + +func ExampleDisplayOutput_noOutput() { + _ = DisplayOutput([]*Output{testOutput1}, "tabular", false) + + // Output: APIVersions were found, but none were deprecated. Try --show-non-deprecated. +} + +func ExampleDisplayOutput_badFormat() { + _ = DisplayOutput([]*Output{testOutput1}, "foo", true) + + // Output: output format should be one of (json,yaml,tabular) +} + +func ExampleDisplayOutput_zeroLength() { + _ = DisplayOutput([]*Output{}, "tabular", false) + + // Output: There were no apiVersions found that match our records. +} From fba2e2ed401bf08608d28b85704263f465ca7f9c Mon Sep 17 00:00:00 2001 From: Andrew Suderman Date: Thu, 26 Mar 2020 15:53:19 -0600 Subject: [PATCH 2/4] Allow checking a single file or stdin --- cmd/root.go | 67 +++++++++++++++++++++++++++++++++++++-- pkg/finder/finder.go | 6 ++-- pkg/finder/finder_test.go | 2 +- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index a65e0bfc..2bb7ffce 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,6 +17,7 @@ package cmd import ( "flag" "fmt" + "io/ioutil" "os" "github.com/fairwindsops/pluto/pkg/api" @@ -39,19 +40,22 @@ var ( func init() { rootCmd.AddCommand(detectFilesCmd) + rootCmd.PersistentFlags().BoolVar(&showNonDeprecated, "show-non-deprecated", false, "If enabled, will show files that have non-deprecated apiVersion. Only applies to tabular output.") + detectFilesCmd.PersistentFlags().StringVarP(&directory, "directory", "d", "", "The directory to scan. If blank, defaults to current workding dir.") detectFilesCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "tabular", "The output format to use. (tabular|json|yaml)") - detectFilesCmd.PersistentFlags().BoolVar(&showNonDeprecated, "show-non-deprecated", false, "If enabled, will show files that have non-deprecated apiVersion. Only applies to tabular output.") rootCmd.AddCommand(detectHelmCmd) detectHelmCmd.PersistentFlags().StringVar(&helmVersion, "helm-version", "3", "Helm version in current cluster (2|3)") - detectHelmCmd.PersistentFlags().BoolVar(&showNonDeprecated, "show-non-deprecated", false, "If enabled, will show files that have non-deprecated apiVersion. Only applies to tabular output.") + + rootCmd.AddCommand(detectCmd) klog.InitFlags(nil) flag.Parse() pflag.CommandLine.AddGoFlagSet(flag.CommandLine) } +// Checker is an interface to find versions type Checker interface { FindVersions() error } @@ -114,6 +118,54 @@ var detectHelmCmd = &cobra.Command{ }, } +var detectCmd = &cobra.Command{ + Use: "detect [file to check or -]", + Short: "Checks a single file or stdin for deprecated apiVersions.", + Long: `Detect deprecated apiVersion in a specific file or other input. Accepts multi-document yaml files and/or - for stdin. Useful for helm testing.`, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) < 1 { + return fmt.Errorf("requires a file argument") + } + if isFileOrStdin(args[0]) { + return nil + } + return fmt.Errorf("invalid file specified: %s", args[0]) + }, + Run: func(cmd *cobra.Command, args []string) { + klog.V(3).Infof("arg0: %s", args[0]) + + if args[0] == "-" { + //stdin + fileData, err := ioutil.ReadAll(os.Stdin) + if err != nil { + fmt.Println("Error reading stdin:", err) + os.Exit(1) + } + output, err := api.IsVersioned(fileData) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + err = api.DisplayOutput(output, outputFormat, showNonDeprecated) + if err != nil { + fmt.Println("Error parsing output:", err) + os.Exit(1) + } + os.Exit(0) + } + output, err := finder.CheckForAPIVersion(args[0]) + if err != nil { + fmt.Println("Error reading file:", err) + os.Exit(1) + } + err = api.DisplayOutput(output, outputFormat, showNonDeprecated) + if err != nil { + fmt.Println("Error parsing output:", err) + os.Exit(1) + } + }, +} + // Execute the stuff func Execute(VERSION string, COMMIT string) { version = VERSION @@ -123,3 +175,14 @@ func Execute(VERSION string, COMMIT string) { os.Exit(1) } } + +func isFileOrStdin(name string) bool { + if name == "-" { + return true + } + info, err := os.Stat(name) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} diff --git a/pkg/finder/finder.go b/pkg/finder/finder.go index a24c98f3..9e934b38 100644 --- a/pkg/finder/finder.go +++ b/pkg/finder/finder.go @@ -91,7 +91,7 @@ func (dir *Dir) listFiles() error { func (dir *Dir) scanFiles() error { for _, file := range dir.FileList { klog.V(8).Infof("processing file: %s", file) - apiFile, err := checkForAPIVersion(file) + apiFile, err := CheckForAPIVersion(file) if err != nil { klog.V(2).Infof("error scanning file %s: %s", file, err.Error()) } @@ -102,10 +102,10 @@ func (dir *Dir) scanFiles() error { return nil } -// checkForAPIVersion checks a filename to see if +// CheckForAPIVersion checks a filename to see if // it is an api-versioned Kubernetes object. // Returns the File object if it is. -func checkForAPIVersion(file string) ([]*api.Output, error) { +func CheckForAPIVersion(file string) ([]*api.Output, error) { data, err := ioutil.ReadFile(file) if err != nil { return nil, err diff --git a/pkg/finder/finder_test.go b/pkg/finder/finder_test.go index 23be9e8a..728a2c41 100644 --- a/pkg/finder/finder_test.go +++ b/pkg/finder/finder_test.go @@ -146,7 +146,7 @@ func Test_checkForAPIVersion(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := checkForAPIVersion(tt.file) + got, err := CheckForAPIVersion(tt.file) if tt.wantErr { assert.Error(t, err) } else { From aa756e2e248d17c87805d339e2b69a4dbeea813b Mon Sep 17 00:00:00 2001 From: Andrew Suderman Date: Thu, 26 Mar 2020 16:01:49 -0600 Subject: [PATCH 3/4] Removed usage from readme. It was annoying. Just expand quickstart --- README.md | 68 +++++++------------------------------------------------ 1 file changed, 8 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 5d51702c..9c9e33ec 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ This is a very simple utility to help users find deprecated Kubernetes apiVersio Install the binary from our [releases](https://github.com/FairwindsOps/pluto/releases) page. -### File Detection +### File Detection in a Directory Run `pluto detect-files -d ` @@ -25,7 +25,7 @@ Deployment extensions/v1beta1 true pkg/finder/testdata/deployment-ex This indicates that we have two files in our directory that have deprecated apiVersions. This will need to be fixed prior to a 1.16 upgrade. -### Helm Detection +### Helm Detection (in-cluster) ``` $ pluto detect-helm --helm-version 3 @@ -35,66 +35,14 @@ StatefulSet apps/v1beta1 true audit-dashboard-prod-rabbitmq-ha This indicates that the StatefulSet audit-dashboard-prod-rabbitmq-ha was deployed with apps/v1beta1 which is deprecated in 1.16 -## Usage +### Helm Chart Checking (local files) -``` -A tool to detect Kubernetes apiVersions - -Usage: - pluto [flags] - pluto [command] - -Available Commands: - detect-files detect-files - detect-helm detect-helm - help Help about any command - version Prints the current version of the tool. - -Flags: - --add_dir_header If true, adds the file directory to the header - --alsologtostderr log to standard error as well as files - -h, --help help for pluto - --kubeconfig string Paths to a kubeconfig. Only required if out-of-cluster. - --log_backtrace_at traceLocation when logging hits line file:N, emit a stack trace (default :0) - --log_dir string If non-empty, write log files in this directory - --log_file string If non-empty, use this log file - --log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800) - --logtostderr log to standard error instead of files (default true) - --master --kubeconfig (Deprecated: switch to --kubeconfig) The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster. - --skip_headers If true, avoid header prefixes in the log messages - --skip_log_headers If true, avoid headers when opening log files - --stderrthreshold severity logs at or above this threshold go to stderr (default 2) - -v, --v Level number for the log level verbosity - --vmodule moduleSpec comma-separated list of pattern=N settings for file-filtered logging - -Use "pluto [command] --help" for more information about a command. -``` +You can run `helm template | pluto detect --show-non-deprecated -` -## Detect Files Options +This will output something like so: ``` -Usage: - pluto detect-files [flags] - -Flags: - -d, --directory string The directory to scan. If blank, defaults to current workding dir. - -h, --help help for detect-files - -o, --output string The output format to use. (tabular|json|yaml) (default "tabular") - --show-non-deprecated If enabled, will show files that have non-deprecated apiVersion. Only applies to tabular output. -``` - -## Detect Helm Options - -NOTE: Only helm 3 is currently supported - -``` -Detect Kubernetes apiVersions in a helm release (in cluster) - -Usage: - pluto detect-helm [flags] - -Flags: - --helm-version string Helm version in current cluster (2|3) (default "3") - -h, --help help for detect-helm - --show-non-deprecated If enabled, will show files that have non-deprecated apiVersion. Only applies to tabular output. +KIND VERSION DEPRECATED RESOURCE NAME +Deployment apps/v1 false RELEASE-NAME-goldilocks-controller +Deployment apps/v1 false RELEASE-NAME-goldilocks-dashboard ``` From 319256249da798041bfcea48be8ee3412295bfcd Mon Sep 17 00:00:00 2001 From: Andrew Suderman Date: Fri, 27 Mar 2020 09:13:24 -0600 Subject: [PATCH 4/4] Review comment. Fixing an extra line we didn't need --- cmd/root.go | 2 +- pkg/api/output.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 2bb7ffce..36130ce7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -143,7 +143,7 @@ var detectCmd = &cobra.Command{ } output, err := api.IsVersioned(fileData) if err != nil { - fmt.Println(err) + fmt.Println("Error checking for versions:", err) os.Exit(1) } err = api.DisplayOutput(output, outputFormat, showNonDeprecated) diff --git a/pkg/api/output.go b/pkg/api/output.go index 6595ab94..e0c43e74 100644 --- a/pkg/api/output.go +++ b/pkg/api/output.go @@ -59,15 +59,15 @@ func DisplayOutput(outputs []*Output, outputFormat string, showNonDeprecated boo if err != nil { return err } + fmt.Println(string(outData)) case "yaml": outData, err = yaml.Marshal(outputs) if err != nil { return err } + fmt.Println(string(outData)) default: fmt.Println("output format should be one of (json,yaml,tabular)") } - - fmt.Println(string(outData)) return nil }