github.com/koderover/helm@v2.17.0+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 "errors" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "log" 25 "strings" 26 27 "github.com/ghodss/yaml" 28 "github.com/golang/protobuf/ptypes/timestamp" 29 "k8s.io/helm/pkg/proto/hapi/chart" 30 ) 31 32 // ErrNoTable indicates that a chart does not have a matching table. 33 type ErrNoTable error 34 35 // ErrNoValue indicates that Values does not contain a key with a value 36 type ErrNoValue error 37 38 // GlobalKey is the name of the Values key that is used for storing global vars. 39 const GlobalKey = "global" 40 41 // Values represents a collection of chart values. 42 type Values map[string]interface{} 43 44 // YAML encodes the Values into a YAML string. 45 func (v Values) YAML() (string, error) { 46 b, err := yaml.Marshal(v) 47 return string(b), err 48 } 49 50 // Table gets a table (YAML subsection) from a Values object. 51 // 52 // The table is returned as a Values. 53 // 54 // Compound table names may be specified with dots: 55 // 56 // foo.bar 57 // 58 // The above will be evaluated as "The table bar inside the table 59 // foo". 60 // 61 // An ErrNoTable is returned if the table does not exist. 62 func (v Values) Table(name string) (Values, error) { 63 names := strings.Split(name, ".") 64 table := v 65 var err error 66 67 for _, n := range names { 68 table, err = tableLookup(table, n) 69 if err != nil { 70 return table, err 71 } 72 } 73 return table, err 74 } 75 76 // AsMap is a utility function for converting Values to a map[string]interface{}. 77 // 78 // It protects against nil map panics. 79 func (v Values) AsMap() map[string]interface{} { 80 if v == nil || len(v) == 0 { 81 return map[string]interface{}{} 82 } 83 return v 84 } 85 86 // Encode writes serialized Values information to the given io.Writer. 87 func (v Values) Encode(w io.Writer) error { 88 //return yaml.NewEncoder(w).Encode(v) 89 out, err := yaml.Marshal(v) 90 if err != nil { 91 return err 92 } 93 _, err = w.Write(out) 94 return err 95 } 96 97 // MergeInto takes the properties in src and merges them into Values. Maps 98 // are merged while values and arrays are replaced. 99 func (v Values) MergeInto(src Values) { 100 for key, srcVal := range src { 101 destVal, found := v[key] 102 103 if found && istable(srcVal) && istable(destVal) { 104 destMap := destVal.(map[string]interface{}) 105 srcMap := srcVal.(map[string]interface{}) 106 Values(destMap).MergeInto(Values(srcMap)) 107 } else { 108 v[key] = srcVal 109 } 110 } 111 } 112 113 func tableLookup(v Values, simple string) (Values, error) { 114 v2, ok := v[simple] 115 if !ok { 116 return v, ErrNoTable(fmt.Errorf("no table named %q (%v)", simple, v)) 117 } 118 if vv, ok := v2.(map[string]interface{}); ok { 119 return vv, nil 120 } 121 122 // This catches a case where a value is of type Values, but doesn't (for some 123 // reason) match the map[string]interface{}. This has been observed in the 124 // wild, and might be a result of a nil map of type Values. 125 if vv, ok := v2.(Values); ok { 126 return vv, nil 127 } 128 129 var e ErrNoTable = fmt.Errorf("no table named %q", simple) 130 return map[string]interface{}{}, e 131 } 132 133 // ReadValues will parse YAML byte data into a Values. 134 func ReadValues(data []byte) (vals Values, err error) { 135 err = yaml.Unmarshal(data, &vals) 136 if len(vals) == 0 { 137 vals = Values{} 138 } 139 return 140 } 141 142 // ReadValuesFile will parse a YAML file into a map of values. 143 func ReadValuesFile(filename string) (Values, error) { 144 data, err := ioutil.ReadFile(filename) 145 if err != nil { 146 return map[string]interface{}{}, err 147 } 148 return ReadValues(data) 149 } 150 151 // CoalesceValues coalesces all of the values in a chart (and its subcharts). 152 // 153 // Values are coalesced together using the following rules: 154 // 155 // - Values in a higher level chart always override values in a lower-level 156 // dependency chart 157 // - Scalar values and arrays are replaced, maps are merged 158 // - A chart has access to all of the variables for it, as well as all of 159 // the values destined for its dependencies. 160 func CoalesceValues(chrt *chart.Chart, vals *chart.Config) (Values, error) { 161 cvals := Values{} 162 // Parse values if not nil. We merge these at the top level because 163 // the passed-in values are in the same namespace as the parent chart. 164 if vals != nil { 165 evals, err := ReadValues([]byte(vals.Raw)) 166 if err != nil { 167 return cvals, err 168 } 169 return coalesce(chrt, evals) 170 } 171 172 return coalesceDeps(chrt, cvals) 173 } 174 175 // coalesce coalesces the dest values and the chart values, giving priority to the dest values. 176 // 177 // This is a helper function for CoalesceValues. 178 func coalesce(ch *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) { 179 var err error 180 dest, err = coalesceValues(ch, dest) 181 if err != nil { 182 return dest, err 183 } 184 return coalesceDeps(ch, dest) 185 } 186 187 // coalesceDeps coalesces the dependencies of the given chart. 188 func coalesceDeps(chrt *chart.Chart, dest map[string]interface{}) (map[string]interface{}, error) { 189 for _, subchart := range chrt.Dependencies { 190 if c, ok := dest[subchart.Metadata.Name]; !ok { 191 // If dest doesn't already have the key, create it. 192 dest[subchart.Metadata.Name] = map[string]interface{}{} 193 } else if !istable(c) { 194 return dest, fmt.Errorf("type mismatch on %s: %t", subchart.Metadata.Name, c) 195 } 196 if dv, ok := dest[subchart.Metadata.Name]; ok { 197 dvmap := dv.(map[string]interface{}) 198 199 // Get globals out of dest and merge them into dvmap. 200 dvmap = coalesceGlobals(dvmap, dest, chrt.Metadata.Name) 201 202 var err error 203 // Now coalesce the rest of the values. 204 dest[subchart.Metadata.Name], err = coalesce(subchart, dvmap) 205 if err != nil { 206 return dest, err 207 } 208 } 209 } 210 return dest, nil 211 } 212 213 // coalesceGlobals copies the globals out of src and merges them into dest. 214 // 215 // For convenience, returns dest. 216 func coalesceGlobals(dest, src map[string]interface{}, chartName string) map[string]interface{} { 217 var dg, sg map[string]interface{} 218 219 if destglob, ok := dest[GlobalKey]; !ok { 220 dg = map[string]interface{}{} 221 } else if dg, ok = destglob.(map[string]interface{}); !ok { 222 log.Printf("Warning: Skipping globals for chart '%s' because destination '%s' is not a table.", chartName, GlobalKey) 223 return dg 224 } 225 226 if srcglob, ok := src[GlobalKey]; !ok { 227 sg = map[string]interface{}{} 228 } else if sg, ok = srcglob.(map[string]interface{}); !ok { 229 log.Printf("Warning: skipping globals for chart '%s' because source '%s' is not a table.", chartName, GlobalKey) 230 return dg 231 } 232 233 rv := make(map[string]interface{}) 234 for k, v := range dest { 235 rv[k] = v 236 } 237 238 // EXPERIMENTAL: In the past, we have disallowed globals to test tables. This 239 // reverses that decision. It may somehow be possible to introduce a loop 240 // here, but I haven't found a way. So for the time being, let's allow 241 // tables in globals. 242 243 // Basically, we reverse order of coalesce here to merge 244 // top-down. 245 rv[GlobalKey] = coalesceTables(sg, dg, chartName) 246 return rv 247 } 248 249 // coalesceValues builds up a values map for a particular chart. 250 // 251 // Values in v will override the values in the chart. 252 func coalesceValues(c *chart.Chart, v map[string]interface{}) (map[string]interface{}, error) { 253 // If there are no values in the chart, we just return the given values 254 if c.Values == nil || c.Values.Raw == "" { 255 return v, nil 256 } 257 258 nv, err := ReadValues([]byte(c.Values.Raw)) 259 if err != nil { 260 // On error, we return just the overridden values. 261 // FIXME: We should log this error. It indicates that the YAML data 262 // did not parse. 263 return v, fmt.Errorf("Error: Reading chart '%s' default values (%s): %s", c.Metadata.Name, c.Values.Raw, err) 264 } 265 266 return coalesceTables(v, nv.AsMap(), c.Metadata.Name), nil 267 } 268 269 // coalesceTables merges a source map into a destination map. 270 // 271 // dest is considered authoritative. 272 func coalesceTables(dst, src map[string]interface{}, chartName string) map[string]interface{} { 273 // Because dest has higher precedence than src, dest values override src 274 // values. 275 276 rv := make(map[string]interface{}) 277 for key, val := range src { 278 dv, ok := dst[key] 279 if !ok { // if not in dst, then copy from src 280 rv[key] = val 281 continue 282 } 283 if dv == nil { // if set to nil in dst, then ignore 284 // When the YAML value is null, we skip the value's key. 285 // This allows Helm's various sources of values (value files or --set) to 286 // remove incompatible keys from any previous chart, file, or set values. 287 continue 288 } 289 290 srcTable, srcIsTable := val.(map[string]interface{}) 291 dstTable, dstIsTable := dv.(map[string]interface{}) 292 switch { 293 case srcIsTable && dstIsTable: // both tables, we coalesce 294 rv[key] = coalesceTables(dstTable, srcTable, chartName) 295 case srcIsTable && !dstIsTable: 296 log.Printf("Warning: Merging destination map for chart '%s'. Overwriting table item '%s', with non table value: %v", chartName, key, dv) 297 rv[key] = dv 298 case !srcIsTable && dstIsTable: 299 log.Printf("Warning: Merging destination map for chart '%s'. The destination item '%s' is a table and ignoring the source '%s' as it has a non-table value of: %v", chartName, key, key, val) 300 rv[key] = dv 301 default: // neither are tables, simply take the dst value 302 rv[key] = dv 303 } 304 } 305 306 // do we have anything in dst that wasn't processed already that we need to copy across? 307 for key, val := range dst { 308 if val == nil { 309 continue 310 } 311 _, ok := rv[key] 312 if !ok { 313 rv[key] = val 314 } 315 } 316 317 return rv 318 } 319 320 // ReleaseOptions represents the additional release options needed 321 // for the composition of the final values struct 322 type ReleaseOptions struct { 323 Name string 324 Time *timestamp.Timestamp 325 Namespace string 326 IsUpgrade bool 327 IsInstall bool 328 Revision int 329 } 330 331 // ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files 332 // 333 // WARNING: This function is deprecated for Helm > 2.1.99 Use ToRenderValuesCaps() instead. It will 334 // remain in the codebase to stay SemVer compliant. 335 // 336 // In Helm 3.0, this will be changed to accept Capabilities as a fourth parameter. 337 func ToRenderValues(chrt *chart.Chart, chrtVals *chart.Config, options ReleaseOptions) (Values, error) { 338 caps := &Capabilities{APIVersions: DefaultVersionSet} 339 return ToRenderValuesCaps(chrt, chrtVals, options, caps) 340 } 341 342 // ToRenderValuesCaps composes the struct from the data coming from the Releases, Charts and Values files 343 // 344 // This takes both ReleaseOptions and Capabilities to merge into the render values. 345 func ToRenderValuesCaps(chrt *chart.Chart, chrtVals *chart.Config, options ReleaseOptions, caps *Capabilities) (Values, error) { 346 347 top := map[string]interface{}{ 348 "Release": map[string]interface{}{ 349 "Name": options.Name, 350 "Time": options.Time, 351 "Namespace": options.Namespace, 352 "IsUpgrade": options.IsUpgrade, 353 "IsInstall": options.IsInstall, 354 "Revision": options.Revision, 355 "Service": "Tiller", 356 }, 357 "Chart": chrt.Metadata, 358 "Files": NewFiles(chrt.Files), 359 "Capabilities": caps, 360 } 361 362 vals, err := CoalesceValues(chrt, chrtVals) 363 if err != nil { 364 return top, err 365 } 366 367 top["Values"] = vals 368 return top, nil 369 } 370 371 // istable is a special-purpose function to see if the present thing matches the definition of a YAML table. 372 func istable(v interface{}) bool { 373 _, ok := v.(map[string]interface{}) 374 return ok 375 } 376 377 // PathValue takes a path that traverses a YAML structure and returns the value at the end of that path. 378 // The path starts at the root of the YAML structure and is comprised of YAML keys separated by periods. 379 // Given the following YAML data the value at path "chapter.one.title" is "Loomings". 380 // 381 // chapter: 382 // one: 383 // title: "Loomings" 384 func (v Values) PathValue(ypath string) (interface{}, error) { 385 if len(ypath) == 0 { 386 return nil, errors.New("YAML path string cannot be zero length") 387 } 388 yps := strings.Split(ypath, ".") 389 if len(yps) == 1 { 390 // if exists must be root key not table 391 vals := v.AsMap() 392 k := yps[0] 393 if _, ok := vals[k]; ok && !istable(vals[k]) { 394 // key found 395 return vals[yps[0]], nil 396 } 397 // key not found 398 return nil, ErrNoValue(fmt.Errorf("%v is not a value", k)) 399 } 400 // join all elements of YAML path except last to get string table path 401 ypsLen := len(yps) 402 table := yps[:ypsLen-1] 403 st := strings.Join(table, ".") 404 // get the last element as a string key 405 key := yps[ypsLen-1:] 406 sk := string(key[0]) 407 // get our table for table path 408 t, err := v.Table(st) 409 if err != nil { 410 //no table 411 return nil, ErrNoValue(fmt.Errorf("%v is not a value", sk)) 412 } 413 // check table for key and ensure value is not a table 414 if k, ok := t[sk]; ok && !istable(k) { 415 // key found 416 return k, nil 417 } 418 419 // key not found 420 return nil, ErrNoValue(fmt.Errorf("key not found: %s", sk)) 421 }