...

Source file src/github.com/instrumenta/kubeval/main.go

Documentation: github.com/instrumenta/kubeval

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/tls"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"os"
    12  	"path/filepath"
    13  	"regexp"
    14  	"runtime"
    15  	"strings"
    16  
    17  	"github.com/fatih/color"
    18  	multierror "github.com/hashicorp/go-multierror"
    19  	"github.com/spf13/cobra"
    20  	"github.com/spf13/viper"
    21  
    22  	"github.com/instrumenta/kubeval/kubeval"
    23  	"github.com/instrumenta/kubeval/log"
    24  )
    25  
    26  var (
    27  	version                 = "dev"
    28  	commit                  = "none"
    29  	date                    = "unknown"
    30  	directories             = []string{}
    31  	ignoredPathPatterns = []string{}
    32  
    33  	// forceColor tells kubeval to use colored output even if
    34  	// stdout is not a TTY
    35  	forceColor bool
    36  
    37  	config = kubeval.NewDefaultConfig()
    38  )
    39  
    40  // RootCmd represents the the command to run when kubeval is run
    41  var RootCmd = &cobra.Command{
    42  	Short:   "Validate a Kubernetes YAML file against the relevant schema",
    43  	Long:    `Validate a Kubernetes YAML file against the relevant schema`,
    44  	Version: fmt.Sprintf("Version: %s\nCommit: %s\nDate: %s\n", version, commit, date),
    45  	Run: func(cmd *cobra.Command, args []string) {
    46  		if config.IgnoreMissingSchemas && !config.Quiet {
    47  			log.Warn("Set to ignore missing schemas")
    48  		}
    49  
    50  		// This is not particularly secure but we highlight that with the name of
    51  		// the config item. It would be good to also support a configurable set of
    52  		// trusted certificate authorities as in the `--certificate-authority`
    53  		// kubectl option.
    54  		if config.InsecureSkipTLSVerify {
    55  			http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
    56  				InsecureSkipVerify: true,
    57  			}
    58  		}
    59  
    60  		success := true
    61  		windowsStdinIssue := false
    62  		outputManager := kubeval.GetOutputManager(config.OutputFormat)
    63  
    64  		stat, err := os.Stdin.Stat()
    65  		if err != nil {
    66  			// Stat() will return an error on Windows in both Powershell and
    67  			// console until go1.9 when nothing is passed on stdin.
    68  			// See https://github.com/golang/go/issues/14853.
    69  			if runtime.GOOS != "windows" {
    70  				log.Error(err)
    71  				os.Exit(1)
    72  			} else {
    73  				windowsStdinIssue = true
    74  			}
    75  		}
    76  		// Assert that colors will definitely be used if requested
    77  		if forceColor {
    78  			color.NoColor = false
    79  		}
    80  		// We detect whether we have anything on stdin to process if we have no arguments
    81  		// or if the argument is a -
    82  		notty := (stat.Mode() & os.ModeCharDevice) == 0
    83  		noFileOrDirArgs := (len(args) < 1 || args[0] == "-") && len(directories) < 1
    84  		if noFileOrDirArgs && !windowsStdinIssue && notty {
    85  			buffer := new(bytes.Buffer)
    86  			_, err := io.Copy(buffer, os.Stdin)
    87  			if err != nil {
    88  				log.Error(err)
    89  				os.Exit(1)
    90  			}
    91  			schemaCache := kubeval.NewSchemaCache()
    92  			config.FileName = viper.GetString("filename")
    93  			results, err := kubeval.ValidateWithCache(buffer.Bytes(), schemaCache, config)
    94  			if err != nil {
    95  				log.Error(err)
    96  				os.Exit(1)
    97  			}
    98  			success = !hasErrors(results)
    99  
   100  			for _, r := range results {
   101  				err = outputManager.Put(r)
   102  				if err != nil {
   103  					log.Error(err)
   104  					os.Exit(1)
   105  				}
   106  			}
   107  		} else {
   108  			if len(args) < 1 && len(directories) < 1 {
   109  				log.Error(errors.New("You must pass at least one file as an argument, or at least one directory to the directories flag"))
   110  				os.Exit(1)
   111  			}
   112  			schemaCache := kubeval.NewSchemaCache()
   113  			files, err := aggregateFiles(args)
   114  			if err != nil {
   115  				log.Error(err)
   116  				success = false
   117  			}
   118  
   119  			var aggResults []kubeval.ValidationResult
   120  			for _, fileName := range files {
   121  				filePath, _ := filepath.Abs(fileName)
   122  				fileContents, err := ioutil.ReadFile(filePath)
   123  				if err != nil {
   124  					log.Error(fmt.Errorf("Could not open file %v", fileName))
   125  					earlyExit()
   126  					success = false
   127  					continue
   128  				}
   129  				config.FileName = fileName
   130  				results, err := kubeval.ValidateWithCache(fileContents, schemaCache, config)
   131  				if err != nil {
   132  					log.Error(err)
   133  					earlyExit()
   134  					success = false
   135  					continue
   136  				}
   137  
   138  				for _, r := range results {
   139  					err := outputManager.Put(r)
   140  					if err != nil {
   141  						log.Error(err)
   142  						os.Exit(1)
   143  					}
   144  				}
   145  
   146  				aggResults = append(aggResults, results...)
   147  			}
   148  
   149  			// only use result of hasErrors check if `success` is currently truthy
   150  			success = success && !hasErrors(aggResults)
   151  		}
   152  
   153  		// flush any final logs which may be sitting in the buffer
   154  		err = outputManager.Flush()
   155  		if err != nil {
   156  			log.Error(err)
   157  			os.Exit(1)
   158  		}
   159  
   160  		if !success {
   161  			os.Exit(1)
   162  		}
   163  	},
   164  }
   165  
   166  // hasErrors returns truthy if any of the provided results
   167  // contain errors.
   168  func hasErrors(res []kubeval.ValidationResult) bool {
   169  	for _, r := range res {
   170  		if len(r.Errors) > 0 {
   171  			return true
   172  		}
   173  	}
   174  	return false
   175  }
   176  
   177  // isIgnored returns whether the specified filename should be ignored.
   178  func isIgnored(path string) (bool, error) {
   179  	for _, p := range ignoredPathPatterns {
   180  		m, err := regexp.MatchString(p, path)
   181  		if err != nil {
   182  			return false, err
   183  		}
   184  		if m {
   185  			return true, nil
   186  		}
   187  	}
   188  	return false, nil
   189  }
   190  
   191  func aggregateFiles(args []string) ([]string, error) {
   192  	files := make([]string, len(args))
   193  	copy(files, args)
   194  
   195  	var allErrors *multierror.Error
   196  	for _, directory := range directories {
   197  		err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
   198  			if err != nil {
   199  				return err
   200  			}
   201  			ignored, err := isIgnored(path)
   202  			if err != nil {
   203  				return err
   204  			}
   205  			if !info.IsDir() && (strings.HasSuffix(info.Name(), ".yaml") || strings.HasSuffix(info.Name(), ".yml")) && !ignored {
   206  				files = append(files, path)
   207  			}
   208  			return nil
   209  		})
   210  		if err != nil {
   211  			allErrors = multierror.Append(allErrors, err)
   212  		}
   213  	}
   214  
   215  	return files, allErrors.ErrorOrNil()
   216  }
   217  
   218  func earlyExit() {
   219  	if config.ExitOnError {
   220  		os.Exit(1)
   221  	}
   222  }
   223  
   224  // Execute adds all child commands to the root command sets flags appropriately.
   225  // This is called by main.main(). It only needs to happen once to the rootCmd.
   226  func Execute() {
   227  	if err := RootCmd.Execute(); err != nil {
   228  		log.Error(err)
   229  		os.Exit(-1)
   230  	}
   231  }
   232  
   233  func init() {
   234  	rootCmdName := filepath.Base(os.Args[0])
   235  	if strings.HasPrefix(rootCmdName, "kubectl-") {
   236  		rootCmdName = strings.Replace(rootCmdName, "-", " ", 1)
   237  	}
   238  	RootCmd.Use = fmt.Sprintf("%s <file> [file...]", rootCmdName)
   239  	kubeval.AddKubevalFlags(RootCmd, config)
   240  	RootCmd.Flags().BoolVarP(&forceColor, "force-color", "", false, "Force colored output even if stdout is not a TTY")
   241  	RootCmd.SetVersionTemplate(`{{.Version}}`)
   242  	RootCmd.Flags().StringSliceVarP(&directories, "directories", "d", []string{}, "A comma-separated list of directories to recursively search for YAML documents")
   243  	RootCmd.Flags().StringSliceVarP(&ignoredPathPatterns, "ignored-path-patterns", "i", []string{}, "A comma-separated list of regular expressions specifying paths to ignore")
   244  	RootCmd.Flags().StringSliceVarP(&ignoredPathPatterns, "ignored-filename-patterns", "", []string{}, "An alias for ignored-path-patterns")
   245  	
   246  	viper.SetEnvPrefix("KUBEVAL")
   247  	viper.AutomaticEnv()
   248  	viper.BindPFlag("schema_location", RootCmd.Flags().Lookup("schema-location"))
   249  	viper.BindPFlag("filename", RootCmd.Flags().Lookup("filename"))
   250  }
   251  
   252  func main() {
   253  	Execute()
   254  }
   255  

View as plain text