github.com/umeshredd/helm@v3.0.0-alpha.1+incompatible/pkg/chartutil/values.go (about) 1 /* 2 Copyright The Helm Authors. 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 chartutil 18 19 import ( 20 "bytes" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "log" 25 "strings" 26 27 "github.com/ghodss/yaml" 28 "github.com/pkg/errors" 29 "github.com/xeipuuv/gojsonschema" 30 31 "helm.sh/helm/pkg/chart" 32 ) 33 34 // ErrNoTable indicates that a chart does not have a matching table. 35 type ErrNoTable string 36 37 func (e ErrNoTable) Error() string { return fmt.Sprintf("%q is not a table", e) } 38 39 // ErrNoValue indicates that Values does not contain a key with a value 40 type ErrNoValue string 41 42 func (e ErrNoValue) Error() string { return fmt.Sprintf("%q is not a value", e) } 43 44 // GlobalKey is the name of the Values key that is used for storing global vars. 45 const GlobalKey = "global" 46 47 // Values represents a collection of chart values. 48 type Values map[string]interface{} 49 50 // YAML encodes the Values into a YAML string. 51 func (v Values) YAML() (string, error) { 52 b, err := yaml.Marshal(v) 53 return string(b), err 54 } 55 56 // Table gets a table (YAML subsection) from a Values object. 57 // 58 // The table is returned as a Values. 59 // 60 // Compound table names may be specified with dots: 61 // 62 // foo.bar 63 // 64 // The above will be evaluated as "The table bar inside the table 65 // foo". 66 // 67 // An ErrNoTable is returned if the table does not exist. 68 func (v Values) Table(name string) (Values, error) { 69 table := v 70 var err error 71 72 for _, n := range parsePath(name) { 73 if table, err = tableLookup(table, n); err != nil { 74 break 75 } 76 } 77 return table, err 78 } 79 80 // AsMap is a utility function for converting Values to a map[string]interface{}. 81 // 82 // It protects against nil map panics. 83 func (v Values) AsMap() map[string]interface{} { 84 if v == nil || len(v) == 0 { 85 return map[string]interface{}{} 86 } 87 return v 88 } 89 90 // Encode writes serialized Values information to the given io.Writer. 91 func (v Values) Encode(w io.Writer) error { 92 //return yaml.NewEncoder(w).Encode(v) 93 out, err := yaml.Marshal(v) 94 if err != nil { 95 return err 96 } 97 _, err = w.Write(out) 98 return err 99 } 100 101 func tableLookup(v Values, simple string) (Values, error) { 102 v2, ok := v[simple] 103 if !ok { 104 return v, ErrNoTable(simple) 105 } 106 if vv, ok := v2.(map[string]interface{}); ok { 107 return vv, nil 108 } 109 110 // This catches a case where a value is of type Values, but doesn't (for some 111 // reason) match the map[string]interface{}. This has been observed in the 112 // wild, and might be a result of a nil map of type Values. 113 if vv, ok := v2.(Values); ok { 114 return vv, nil 115 } 116 117 return Values{}, ErrNoTable(simple) 118 } 119 120 // ReadValues will parse YAML byte data into a Values. 121 func ReadValues(data []byte) (vals Values, err error) { 122 err = yaml.Unmarshal(data, &vals) 123 if len(vals) == 0 { 124 vals = Values{} 125 } 126 return vals, err 127 } 128 129 // ReadValuesFile will parse a YAML file into a map of values. 130 func ReadValuesFile(filename string) (Values, error) { 131 data, err := ioutil.ReadFile(filename) 132 if err != nil { 133 return map[string]interface{}{}, err 134 } 135 return ReadValues(data) 136 } 137 138 // ValidateAgainstSchema checks that values does not violate the structure laid out in schema 139 func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) error { 140 var sb strings.Builder 141 if chrt.Schema != nil { 142 err := ValidateAgainstSingleSchema(values, chrt.Schema) 143 if err != nil { 144 sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name())) 145 sb.WriteString(err.Error()) 146 } 147 } 148 149 // For each dependency, recurively call this function with the coalesced values 150 for _, subchrt := range chrt.Dependencies() { 151 subchrtValues := values[subchrt.Name()].(map[string]interface{}) 152 if err := ValidateAgainstSchema(subchrt, subchrtValues); err != nil { 153 sb.WriteString(err.Error()) 154 } 155 } 156 157 if sb.Len() > 0 { 158 return errors.New(sb.String()) 159 } 160 161 return nil 162 } 163 164 // ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema 165 func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) error { 166 valuesData, err := yaml.Marshal(values) 167 if err != nil { 168 return err 169 } 170 valuesJSON, err := yaml.YAMLToJSON(valuesData) 171 if err != nil { 172 return err 173 } 174 if bytes.Equal(valuesJSON, []byte("null")) { 175 valuesJSON = []byte("{}") 176 } 177 schemaLoader := gojsonschema.NewBytesLoader(schemaJSON) 178 valuesLoader := gojsonschema.NewBytesLoader(valuesJSON) 179 180 result, err := gojsonschema.Validate(schemaLoader, valuesLoader) 181 if err != nil { 182 return err 183 } 184 185 if !result.Valid() { 186 var sb strings.Builder 187 for _, desc := range result.Errors() { 188 sb.WriteString(fmt.Sprintf("- %s\n", desc)) 189 } 190 return errors.New(sb.String()) 191 } 192 193 return nil 194 } 195 196 // CoalesceValues coalesces all of the values in a chart (and its subcharts). 197 // 198 // Values are coalesced together using the following rules: 199 // 200 // - Values in a higher level chart always override values in a lower-level 201 // dependency chart 202 // - Scalar values and arrays are replaced, maps are merged 203 // - A chart has access to all of the variables for it, as well as all of 204 // the values destined for its dependencies. 205 func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) { 206 if vals == nil { 207 vals = make(map[string]interface{}) 208 } 209 if _, err := coalesce(chrt, vals); err != nil { 210 return vals, err 211 } 212 return coalesceDeps(chrt, vals) 213 } 214 215 // coalesce coalesces the dest values and the chart values, giving priority to the dest values. 216 // 217 // This is a helper function for CoalesceValues. 218 func coalesce(ch *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) { 219 coalesceValues(ch, dest) 220 return coalesceDeps(ch, dest) 221 } 222 223 // coalesceDeps coalesces the dependencies of the given chart. 224 func coalesceDeps(chrt *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) { 225 for _, subchart := range chrt.Dependencies() { 226 if c, ok := dest[subchart.Name()]; !ok { 227 // If dest doesn't already have the key, create it. 228 dest[subchart.Name()] = make(map[string]interface{}) 229 } else if !istable(c) { 230 return dest, errors.Errorf("type mismatch on %s: %t", subchart.Name(), c) 231 } 232 if dv, ok := dest[subchart.Name()]; ok { 233 dvmap := dv.(map[string]interface{}) 234 235 // Get globals out of dest and merge them into dvmap. 236 coalesceGlobals(dvmap, dest) 237 238 // Now coalesce the rest of the values. 239 var err error 240 dest[subchart.Name()], err = coalesce(subchart, dvmap) 241 if err != nil { 242 return dest, err 243 } 244 } 245 } 246 return dest, nil 247 } 248 249 // coalesceGlobals copies the globals out of src and merges them into dest. 250 // 251 // For convenience, returns dest. 252 func coalesceGlobals(dest, src map[string]interface{}) { 253 var dg, sg map[string]interface{} 254 255 if destglob, ok := dest[GlobalKey]; !ok { 256 dg = make(map[string]interface{}) 257 } else if dg, ok = destglob.(map[string]interface{}); !ok { 258 log.Printf("warning: skipping globals because destination %s is not a table.", GlobalKey) 259 return 260 } 261 262 if srcglob, ok := src[GlobalKey]; !ok { 263 sg = make(map[string]interface{}) 264 } else if sg, ok = srcglob.(map[string]interface{}); !ok { 265 log.Printf("warning: skipping globals because source %s is not a table.", GlobalKey) 266 return 267 } 268 269 // EXPERIMENTAL: In the past, we have disallowed globals to test tables. This 270 // reverses that decision. It may somehow be possible to introduce a loop 271 // here, but I haven't found a way. So for the time being, let's allow 272 // tables in globals. 273 for key, val := range sg { 274 if istable(val) { 275 vv := copyMap(val.(map[string]interface{})) 276 if destv, ok := dg[key]; !ok { 277 // Here there is no merge. We're just adding. 278 dg[key] = vv 279 } else { 280 if destvmap, ok := destv.(map[string]interface{}); !ok { 281 log.Printf("Conflict: cannot merge map onto non-map for %q. Skipping.", key) 282 } else { 283 // Basically, we reverse order of coalesce here to merge 284 // top-down. 285 CoalesceTables(vv, destvmap) 286 dg[key] = vv 287 continue 288 } 289 } 290 } else if dv, ok := dg[key]; ok && istable(dv) { 291 // It's not clear if this condition can actually ever trigger. 292 log.Printf("key %s is table. Skipping", key) 293 continue 294 } 295 // TODO: Do we need to do any additional checking on the value? 296 dg[key] = val 297 } 298 dest[GlobalKey] = dg 299 } 300 301 func copyMap(src map[string]interface{}) map[string]interface{} { 302 m := make(map[string]interface{}, len(src)) 303 for k, v := range src { 304 m[k] = v 305 } 306 return m 307 } 308 309 // coalesceValues builds up a values map for a particular chart. 310 // 311 // Values in v will override the values in the chart. 312 func coalesceValues(c *chart.Chart, v map[string]interface{}) { 313 for key, val := range c.Values { 314 if value, ok := v[key]; ok { 315 if value == nil { 316 // When the YAML value is null, we remove the value's key. 317 // This allows Helm's various sources of values (value files or --set) to 318 // remove incompatible keys from any previous chart, file, or set values. 319 delete(v, key) 320 } else if dest, ok := value.(map[string]interface{}); ok { 321 // if v[key] is a table, merge nv's val table into v[key]. 322 src, ok := val.(map[string]interface{}) 323 if !ok { 324 log.Printf("warning: skipped value for %s: Not a table.", key) 325 continue 326 } 327 // Because v has higher precedence than nv, dest values override src 328 // values. 329 CoalesceTables(dest, src) 330 } 331 } else { 332 // If the key is not in v, copy it from nv. 333 v[key] = val 334 } 335 } 336 } 337 338 // CoalesceTables merges a source map into a destination map. 339 // 340 // dest is considered authoritative. 341 func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} { 342 if dst == nil || src == nil { 343 return src 344 } 345 // Because dest has higher precedence than src, dest values override src 346 // values. 347 for key, val := range src { 348 if istable(val) { 349 switch innerdst, ok := dst[key]; { 350 case !ok: 351 dst[key] = val 352 case istable(innerdst): 353 CoalesceTables(innerdst.(map[string]interface{}), val.(map[string]interface{})) 354 default: 355 log.Printf("warning: cannot overwrite table with non table for %s (%v)", key, val) 356 } 357 } else if dv, ok := dst[key]; ok && istable(dv) { 358 log.Printf("warning: destination for %s is a table. Ignoring non-table value %v", key, val) 359 } else if !ok { // <- ok is still in scope from preceding conditional. 360 dst[key] = val 361 } 362 } 363 return dst 364 } 365 366 // ReleaseOptions represents the additional release options needed 367 // for the composition of the final values struct 368 type ReleaseOptions struct { 369 Name string 370 Namespace string 371 IsUpgrade bool 372 IsInstall bool 373 } 374 375 // ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files 376 // 377 // This takes both ReleaseOptions and Capabilities to merge into the render values. 378 func ToRenderValues(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities) (Values, error) { 379 if caps == nil { 380 caps = DefaultCapabilities 381 } 382 top := map[string]interface{}{ 383 "Chart": chrt.Metadata, 384 "Capabilities": caps, 385 "Release": map[string]interface{}{ 386 "Name": options.Name, 387 "Namespace": options.Namespace, 388 "IsUpgrade": options.IsUpgrade, 389 "IsInstall": options.IsInstall, 390 "Service": "Helm", 391 }, 392 } 393 394 vals, err := CoalesceValues(chrt, chrtVals) 395 if err != nil { 396 return top, err 397 } 398 399 if err := ValidateAgainstSchema(chrt, vals); err != nil { 400 errFmt := "values don't meet the specifications of the schema(s) in the following chart(s):\n%s" 401 return top, fmt.Errorf(errFmt, err.Error()) 402 } 403 404 top["Values"] = vals 405 return top, nil 406 } 407 408 // istable is a special-purpose function to see if the present thing matches the definition of a YAML table. 409 func istable(v interface{}) bool { 410 _, ok := v.(map[string]interface{}) 411 return ok 412 } 413 414 // PathValue takes a path that traverses a YAML structure and returns the value at the end of that path. 415 // The path starts at the root of the YAML structure and is comprised of YAML keys separated by periods. 416 // Given the following YAML data the value at path "chapter.one.title" is "Loomings". 417 // 418 // chapter: 419 // one: 420 // title: "Loomings" 421 func (v Values) PathValue(path string) (interface{}, error) { 422 if path == "" { 423 return nil, errors.New("YAML path cannot be empty") 424 } 425 return v.pathValue(parsePath(path)) 426 } 427 428 func (v Values) pathValue(path []string) (interface{}, error) { 429 if len(path) == 1 { 430 // if exists must be root key not table 431 if _, ok := v[path[0]]; ok && !istable(v[path[0]]) { 432 return v[path[0]], nil 433 } 434 return nil, ErrNoValue(path[0]) 435 } 436 437 key, path := path[len(path)-1], path[:len(path)-1] 438 // get our table for table path 439 t, err := v.Table(joinPath(path...)) 440 if err != nil { 441 return nil, ErrNoValue(key) 442 } 443 // check table for key and ensure value is not a table 444 if k, ok := t[key]; ok && !istable(k) { 445 return k, nil 446 } 447 return nil, ErrNoValue(key) 448 } 449 450 func parsePath(key string) []string { return strings.Split(key, ".") } 451 452 func joinPath(path ...string) string { return strings.Join(path, ".") }