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 }