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