github.com/jenkins-x/jx/v2@v2.1.155/pkg/cmd/step/verify/step_verify_values.go (about)

     1  package verify
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/jenkins-x/jx-logging/pkg/log"
    12  	"github.com/jenkins-x/jx/v2/pkg/cmd/helper"
    13  	"github.com/jenkins-x/jx/v2/pkg/cmd/opts"
    14  	"github.com/jenkins-x/jx/v2/pkg/cmd/opts/step"
    15  	"github.com/jenkins-x/jx/v2/pkg/config"
    16  	"github.com/jenkins-x/jx/v2/pkg/io/secrets"
    17  	"github.com/jenkins-x/jx/v2/pkg/secreturl"
    18  	"github.com/jenkins-x/jx/v2/pkg/surveyutils"
    19  	"github.com/jenkins-x/jx/v2/pkg/util"
    20  	"github.com/pkg/errors"
    21  	"github.com/spf13/cobra"
    22  	"github.com/xeipuuv/gojsonschema"
    23  	"gopkg.in/yaml.v2"
    24  )
    25  
    26  // StepVerifyValuesOptions contains the command line options
    27  type StepVerifyValuesOptions struct {
    28  	step.StepOptions
    29  
    30  	SchemaFile      string
    31  	RequirementsDir string
    32  	ValuesFile      string
    33  
    34  	// SecretClient secrets URL client (added as a field to be able to easy mock it)
    35  	SecretClient secreturl.Client
    36  }
    37  
    38  const (
    39  	schemaFileOption      = "schema-file"
    40  	requirementsDirOption = "requirements-dir"
    41  	valuesFileOption      = "values-file"
    42  )
    43  
    44  // NewCmdStepVerifyValues constructs the command
    45  func NewCmdStepVerifyValues(commonOpts *opts.CommonOptions) *cobra.Command {
    46  	options := &StepVerifyValuesOptions{
    47  		StepOptions: step.StepOptions{
    48  			CommonOptions: commonOpts,
    49  		},
    50  	}
    51  
    52  	cmd := &cobra.Command{
    53  		Use: "values",
    54  		Run: func(cmd *cobra.Command, args []string) {
    55  			options.Cmd = cmd
    56  			options.Args = args
    57  			err := options.Run()
    58  			helper.CheckErr(err)
    59  		},
    60  	}
    61  
    62  	cmd.Flags().StringVarP(&options.SchemaFile, schemaFileOption, "s", "", "the path to the JSON schema file")
    63  	cmd.Flags().StringVarP(&options.RequirementsDir, requirementsDirOption, "r", "",
    64  		fmt.Sprintf("the path to the dir which contains the %s file, if omitted looks in the current directory",
    65  			config.RequirementsConfigFileName))
    66  	cmd.Flags().StringVarP(&options.ValuesFile, valuesFileOption, "v", "", "the path to the values YAML file")
    67  
    68  	return cmd
    69  }
    70  
    71  func (o *StepVerifyValuesOptions) checkFile(file string) error {
    72  	if exists, err := util.FileExists(file); !exists || err != nil {
    73  		return fmt.Errorf("provided file %q does not exists", file)
    74  	}
    75  	return nil
    76  }
    77  
    78  func (o *StepVerifyValuesOptions) checkOptions() error {
    79  	if o.SchemaFile == "" {
    80  		return util.MissingOption(schemaFileOption)
    81  	}
    82  
    83  	if err := o.checkFile(o.SchemaFile); err != nil {
    84  		return err
    85  	}
    86  
    87  	if o.RequirementsDir == "" {
    88  		dir, err := os.Getwd()
    89  		if err != nil {
    90  			return errors.Wrapf(err, "get current working directory to lookup for %s",
    91  				config.RequirementsConfigFileName)
    92  		}
    93  		o.RequirementsDir = dir
    94  	}
    95  
    96  	if exists, err := util.DirExists(o.RequirementsDir); !exists || err != nil {
    97  		return fmt.Errorf("provided dir for requirements does not exist")
    98  	}
    99  
   100  	if o.ValuesFile == "" {
   101  		return util.MissingOption(valuesFileOption)
   102  	}
   103  
   104  	if err := o.checkFile(o.ValuesFile); err != nil {
   105  		return err
   106  	}
   107  
   108  	return nil
   109  }
   110  
   111  // Run implements this command
   112  func (o *StepVerifyValuesOptions) Run() error {
   113  	if err := o.checkOptions(); err != nil {
   114  		return err
   115  	}
   116  
   117  	requirements, reqFile, err := config.LoadRequirementsConfig(o.RequirementsDir, config.DefaultFailOnValidationError)
   118  	if err != nil {
   119  		return errors.Wrapf(err, "loading requirements from %q", o.RequirementsDir)
   120  	}
   121  	if err := o.checkFile(reqFile); err != nil {
   122  		return err
   123  	}
   124  
   125  	schema, err := surveyutils.ReadSchemaTemplate(o.SchemaFile, requirements)
   126  	if err != nil {
   127  		return errors.Wrapf(err, "rendering the schema template %q", o.SchemaFile)
   128  	}
   129  
   130  	values, err := ioutil.ReadFile(o.ValuesFile)
   131  	if err != nil {
   132  		return errors.Wrapf(err, "reading the values from file %q", o.ValuesFile)
   133  	}
   134  
   135  	values, err = o.resolveSecrets(requirements, values)
   136  	if err != nil {
   137  		return errors.Wrapf(err, "resolve the secrets URIs")
   138  	}
   139  
   140  	values, err = convertYamlToJson(values)
   141  	if err != nil {
   142  		return errors.Wrap(err, "converting values data from YAML to JSON")
   143  	}
   144  
   145  	if err := o.verifySchema(schema, values); err != nil {
   146  		name := filepath.Base(o.ValuesFile)
   147  		name = strings.TrimSuffix(name, filepath.Ext(name))
   148  		log.Logger().Infof(`
   149  The %q values file needs to be updated. You can regenerate the values file from schema %q with command:
   150  
   151  jx step create values --name %s
   152  		`, o.ValuesFile, o.SchemaFile, name)
   153  		return errors.Wrap(err, "verifying provided values file against schema file")
   154  	}
   155  
   156  	return nil
   157  }
   158  
   159  func (o *StepVerifyValuesOptions) verifySchema(schema []byte, values []byte) error {
   160  	schemaLoader := gojsonschema.NewBytesLoader(schema)
   161  	valuesLoader := gojsonschema.NewBytesLoader(values)
   162  	result, err := gojsonschema.Validate(schemaLoader, valuesLoader)
   163  	if err != nil {
   164  		return errors.Wrap(err, "validating the JSON schema against the values")
   165  	}
   166  
   167  	if result.Valid() {
   168  		return nil
   169  	}
   170  
   171  	for _, err := range result.Errors() {
   172  		log.Logger().Errorf("%s", err)
   173  	}
   174  
   175  	return errors.New("invalid values")
   176  }
   177  
   178  func (o *StepVerifyValuesOptions) resolveSecrets(requirements *config.RequirementsConfig, values []byte) ([]byte, error) {
   179  	client, err := o.secretClient(requirements.SecretStorage)
   180  	if err != nil {
   181  		return nil, errors.Wrap(err, "creating secret client")
   182  	}
   183  	result, err := client.ReplaceURIs(string(values))
   184  	if err != nil {
   185  		return nil, errors.Wrap(err, "replacing secrets URIs")
   186  	}
   187  	return []byte(result), nil
   188  }
   189  
   190  func (o *StepVerifyValuesOptions) secretClient(secretStorage config.SecretStorageType) (secreturl.Client, error) {
   191  	if o.SecretClient != nil {
   192  		return o.SecretClient, nil
   193  	}
   194  
   195  	location := secrets.ToSecretsLocation(string(secretStorage))
   196  	return o.GetSecretURLClient(location)
   197  }
   198  
   199  func convertYamlToJson(yml []byte) ([]byte, error) {
   200  	var data interface{}
   201  	if err := yaml.Unmarshal(yml, &data); err != nil {
   202  		return nil, errors.Wrap(err, "unmarshaling yaml data")
   203  	}
   204  
   205  	data = convertType(data)
   206  
   207  	result, err := json.Marshal(data)
   208  	if err != nil {
   209  		return nil, errors.Wrap(err, "marshaling data to json")
   210  	}
   211  	return result, nil
   212  }
   213  
   214  func convertType(t interface{}) interface{} {
   215  	switch x := t.(type) {
   216  	case map[interface{}]interface{}:
   217  		m := map[string]interface{}{}
   218  		for k, v := range x {
   219  			m[k.(string)] = convertType(v)
   220  		}
   221  		return m
   222  	case []interface{}:
   223  		for k, v := range x {
   224  			x[k] = convertType(v)
   225  		}
   226  	}
   227  	return t
   228  }