github.com/splunk/dan1-qbec@v0.7.3/internal/commands/validate.go (about)

     1  /*
     2     Copyright 2019 Splunk Inc.
     3  
     4     Licensed under the Apache License, Version 2.0 (the "License");
     5     you may not use this file except in compliance with the License.
     6     You may obtain a copy of the License at
     7  
     8         http://www.apache.org/licenses/LICENSE-2.0
     9  
    10     Unless required by applicable law or agreed to in writing, software
    11     distributed under the License is distributed on an "AS IS" BASIS,
    12     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13     See the License for the specific language governing permissions and
    14     limitations under the License.
    15  */
    16  
    17  package commands
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"strings"
    23  	"sync"
    24  
    25  	"github.com/spf13/cobra"
    26  	"github.com/splunk/qbec/internal/model"
    27  	"github.com/splunk/qbec/internal/remote/k8smeta"
    28  )
    29  
    30  const (
    31  	escGreen = "\x1b[32m"
    32  	escRed   = "\x1b[31m"
    33  	escDim   = "\x1b[2m"
    34  	escReset = "\x1b[0m"
    35  
    36  	unicodeCheck    = "\u2714"
    37  	unicodeX        = "\u2718"
    38  	unicodeQuestion = "\u003f"
    39  )
    40  
    41  type validatorStats struct {
    42  	l          sync.Mutex
    43  	ValidCount int      `json:"valid,omitempty"`
    44  	Unknown    []string `json:"unknown,omitempty"`
    45  	Invalid    []string `json:"invalid,omitempty"`
    46  	Errors     []string `json:"errors,omitempty"`
    47  }
    48  
    49  func (v *validatorStats) valid(s string) {
    50  	v.l.Lock()
    51  	defer v.l.Unlock()
    52  	v.ValidCount++
    53  }
    54  
    55  func (v *validatorStats) invalid(s string) {
    56  	v.l.Lock()
    57  	defer v.l.Unlock()
    58  	v.Invalid = append(v.Invalid, s)
    59  }
    60  
    61  func (v *validatorStats) unknown(s string) {
    62  	v.l.Lock()
    63  	defer v.l.Unlock()
    64  	v.Unknown = append(v.Unknown, s)
    65  }
    66  
    67  func (v *validatorStats) errors(s string) {
    68  	v.l.Lock()
    69  	defer v.l.Unlock()
    70  	v.Errors = append(v.Errors, s)
    71  }
    72  
    73  type validator struct {
    74  	w                      io.Writer
    75  	client                 Client
    76  	stats                  validatorStats
    77  	red, green, dim, reset string
    78  	silent                 bool
    79  }
    80  
    81  func (v *validator) validate(obj model.K8sLocalObject) error {
    82  	name := v.client.DisplayName(obj)
    83  	schema, err := v.client.ValidatorFor(obj.GroupVersionKind())
    84  	if err != nil {
    85  		if err == k8smeta.ErrSchemaNotFound {
    86  			if !v.silent {
    87  				fmt.Fprintf(v.w, "%s%s %s: no schema found, cannot validate%s\n", v.dim, unicodeQuestion, name, v.reset)
    88  			}
    89  			v.stats.unknown(name)
    90  			return nil
    91  		}
    92  		fmt.Fprintf(v.w, "%s%s %s: schema fetch error %v%s\n", v.red, unicodeX, name, err, v.reset)
    93  		v.stats.errors(name)
    94  		return err
    95  	}
    96  	errs := schema.Validate(obj.ToUnstructured())
    97  	if len(errs) == 0 {
    98  		if !v.silent {
    99  			fmt.Fprintf(v.w, "%s%s %s is valid%s\n", v.green, unicodeCheck, name, v.reset)
   100  		}
   101  		v.stats.valid(name)
   102  		return nil
   103  	}
   104  	var lines []string
   105  	for _, e := range errs {
   106  		lines = append(lines, e.Error())
   107  	}
   108  	fmt.Fprintf(v.w, "%s%s %s is invalid\n\t- %s%s\n", v.red, unicodeX, name, strings.Join(lines, "\n\t- "), v.reset)
   109  	v.stats.invalid(name)
   110  	return nil
   111  }
   112  
   113  func validateObjects(objs []model.K8sLocalObject, client Client, parallel int, colors bool, out io.Writer, silent bool) error {
   114  	v := &validator{
   115  		w:      &lockWriter{Writer: out},
   116  		client: client,
   117  		silent: silent,
   118  	}
   119  	if colors {
   120  		v.green = escGreen
   121  		v.red = escRed
   122  		v.dim = escDim
   123  		v.reset = escReset
   124  	}
   125  
   126  	vErr := runInParallel(objs, v.validate, parallel)
   127  	printStats(v.w, &v.stats)
   128  
   129  	switch {
   130  	case vErr != nil:
   131  		return vErr
   132  	case len(v.stats.Invalid) > 0:
   133  		return fmt.Errorf("%d invalid objects found", len(v.stats.Invalid))
   134  	default:
   135  		return nil
   136  	}
   137  }
   138  
   139  type validateCommandConfig struct {
   140  	*Config
   141  	parallel   int
   142  	silent     bool
   143  	filterFunc func() (filterParams, error)
   144  }
   145  
   146  func doValidate(args []string, config validateCommandConfig) error {
   147  	if len(args) != 1 {
   148  		return newUsageError("exactly one environment required")
   149  	}
   150  	env := args[0]
   151  	if env == model.Baseline {
   152  		return newUsageError("cannot validate baseline environment, use a real environment")
   153  	}
   154  	fp, err := config.filterFunc()
   155  	if err != nil {
   156  		return err
   157  	}
   158  	client, err := config.Client(env)
   159  	if err != nil {
   160  		return err
   161  	}
   162  	objects, err := filteredObjects(config.Config, env, client.ObjectKey, fp)
   163  	if err != nil {
   164  		return err
   165  	}
   166  	return validateObjects(objects, client, config.parallel, config.Colorize(), config.Stdout(), config.silent)
   167  
   168  }
   169  
   170  func newValidateCommand(cp ConfigProvider) *cobra.Command {
   171  	cmd := &cobra.Command{
   172  		Use:     "validate <environment>",
   173  		Short:   "validate one or more components against the spec of a kubernetes cluster",
   174  		Example: validateExamples(),
   175  	}
   176  
   177  	config := validateCommandConfig{
   178  		filterFunc: addFilterParams(cmd, true),
   179  	}
   180  
   181  	cmd.Flags().IntVar(&config.parallel, "parallel", 5, "number of parallel routines to run")
   182  	cmd.Flags().BoolVar(&config.silent, "silent", false, "do not print success messages for every object")
   183  	cmd.RunE = func(c *cobra.Command, args []string) error {
   184  		config.Config = cp()
   185  		return wrapError(doValidate(args, config))
   186  	}
   187  	return cmd
   188  }