github.com/sgoings/helm@v2.0.0-alpha.2.0.20170406211108-734e92851ac3+incompatible/pkg/chartutil/requirements.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors All rights reserved. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package chartutil 17 18 import ( 19 "errors" 20 "log" 21 "strings" 22 "time" 23 24 "github.com/ghodss/yaml" 25 "k8s.io/helm/pkg/proto/hapi/chart" 26 ) 27 28 const ( 29 requirementsName = "requirements.yaml" 30 lockfileName = "requirements.lock" 31 ) 32 33 var ( 34 // ErrRequirementsNotFound indicates that a requirements.yaml is not found. 35 ErrRequirementsNotFound = errors.New(requirementsName + " not found") 36 // ErrLockfileNotFound indicates that a requirements.lock is not found. 37 ErrLockfileNotFound = errors.New(lockfileName + " not found") 38 ) 39 40 // Dependency describes a chart upon which another chart depends. 41 // 42 // Dependencies can be used to express developer intent, or to capture the state 43 // of a chart. 44 type Dependency struct { 45 // Name is the name of the dependency. 46 // 47 // This must mach the name in the dependency's Chart.yaml. 48 Name string `json:"name"` 49 // Version is the version (range) of this chart. 50 // 51 // A lock file will always produce a single version, while a dependency 52 // may contain a semantic version range. 53 Version string `json:"version,omitempty"` 54 // The URL to the repository. 55 // 56 // Appending `index.yaml` to this string should result in a URL that can be 57 // used to fetch the repository index. 58 Repository string `json:"repository"` 59 // A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled ) 60 Condition string `json:"condition"` 61 // Tags can be used to group charts for enabling/disabling together 62 Tags []string `json:"tags"` 63 // Enabled bool determines if chart should be loaded 64 Enabled bool `json:"enabled"` 65 // ImportValues holds the mapping of source values to parent key to be imported. Each item can be a 66 // string or pair of child/parent sublist items. 67 ImportValues []interface{} `json:"import-values"` 68 } 69 70 // ErrNoRequirementsFile to detect error condition 71 type ErrNoRequirementsFile error 72 73 // Requirements is a list of requirements for a chart. 74 // 75 // Requirements are charts upon which this chart depends. This expresses 76 // developer intent. 77 type Requirements struct { 78 Dependencies []*Dependency `json:"dependencies"` 79 } 80 81 // RequirementsLock is a lock file for requirements. 82 // 83 // It represents the state that the dependencies should be in. 84 type RequirementsLock struct { 85 // Genderated is the date the lock file was last generated. 86 Generated time.Time `json:"generated"` 87 // Digest is a hash of the requirements file used to generate it. 88 Digest string `json:"digest"` 89 // Dependencies is the list of dependencies that this lock file has locked. 90 Dependencies []*Dependency `json:"dependencies"` 91 } 92 93 // LoadRequirements loads a requirements file from an in-memory chart. 94 func LoadRequirements(c *chart.Chart) (*Requirements, error) { 95 var data []byte 96 for _, f := range c.Files { 97 if f.TypeUrl == requirementsName { 98 data = f.Value 99 } 100 } 101 if len(data) == 0 { 102 return nil, ErrRequirementsNotFound 103 } 104 r := &Requirements{} 105 return r, yaml.Unmarshal(data, r) 106 } 107 108 // LoadRequirementsLock loads a requirements lock file. 109 func LoadRequirementsLock(c *chart.Chart) (*RequirementsLock, error) { 110 var data []byte 111 for _, f := range c.Files { 112 if f.TypeUrl == lockfileName { 113 data = f.Value 114 } 115 } 116 if len(data) == 0 { 117 return nil, ErrLockfileNotFound 118 } 119 r := &RequirementsLock{} 120 return r, yaml.Unmarshal(data, r) 121 } 122 123 // ProcessRequirementsConditions disables charts based on condition path value in values 124 func ProcessRequirementsConditions(reqs *Requirements, cvals Values) { 125 var cond string 126 var conds []string 127 if reqs == nil || len(reqs.Dependencies) == 0 { 128 return 129 } 130 for _, r := range reqs.Dependencies { 131 var hasTrue, hasFalse bool 132 cond = string(r.Condition) 133 // check for list 134 if len(cond) > 0 { 135 if strings.Contains(cond, ",") { 136 conds = strings.Split(strings.TrimSpace(cond), ",") 137 } else { 138 conds = []string{strings.TrimSpace(cond)} 139 } 140 for _, c := range conds { 141 if len(c) > 0 { 142 // retrieve value 143 vv, err := cvals.PathValue(c) 144 if err == nil { 145 // if not bool, warn 146 if bv, ok := vv.(bool); ok { 147 if bv { 148 hasTrue = true 149 } else { 150 hasFalse = true 151 } 152 } else { 153 log.Printf("Warning: Condition path '%s' for chart %s returned non-bool value", c, r.Name) 154 } 155 } else if _, ok := err.(ErrNoValue); !ok { 156 // this is a real error 157 log.Printf("Warning: PathValue returned error %v", err) 158 159 } 160 if vv != nil { 161 // got first value, break loop 162 break 163 } 164 } 165 } 166 if !hasTrue && hasFalse { 167 r.Enabled = false 168 } else if hasTrue { 169 r.Enabled = true 170 171 } 172 } 173 174 } 175 176 } 177 178 // ProcessRequirementsTags disables charts based on tags in values 179 func ProcessRequirementsTags(reqs *Requirements, cvals Values) { 180 vt, err := cvals.Table("tags") 181 if err != nil { 182 return 183 184 } 185 if reqs == nil || len(reqs.Dependencies) == 0 { 186 return 187 } 188 for _, r := range reqs.Dependencies { 189 if len(r.Tags) > 0 { 190 tags := r.Tags 191 192 var hasTrue, hasFalse bool 193 for _, k := range tags { 194 if b, ok := vt[k]; ok { 195 // if not bool, warn 196 if bv, ok := b.(bool); ok { 197 if bv { 198 hasTrue = true 199 } else { 200 hasFalse = true 201 } 202 } else { 203 log.Printf("Warning: Tag '%s' for chart %s returned non-bool value", k, r.Name) 204 } 205 } 206 } 207 if !hasTrue && hasFalse { 208 r.Enabled = false 209 } else if hasTrue || !hasTrue && !hasFalse { 210 r.Enabled = true 211 212 } 213 214 } 215 } 216 217 } 218 219 // ProcessRequirementsEnabled removes disabled charts from dependencies 220 func ProcessRequirementsEnabled(c *chart.Chart, v *chart.Config) error { 221 reqs, err := LoadRequirements(c) 222 if err != nil { 223 // if not just missing requirements file, return error 224 if nerr, ok := err.(ErrNoRequirementsFile); !ok { 225 return nerr 226 } 227 228 // no requirements to process 229 return nil 230 } 231 // set all to true 232 for _, lr := range reqs.Dependencies { 233 lr.Enabled = true 234 } 235 cvals, err := CoalesceValues(c, v) 236 if err != nil { 237 return err 238 } 239 // convert our values back into config 240 yvals, err := cvals.YAML() 241 if err != nil { 242 return err 243 } 244 cc := chart.Config{Raw: yvals} 245 // flag dependencies as enabled/disabled 246 ProcessRequirementsTags(reqs, cvals) 247 ProcessRequirementsConditions(reqs, cvals) 248 // make a map of charts to remove 249 rm := map[string]bool{} 250 for _, r := range reqs.Dependencies { 251 if !r.Enabled { 252 // remove disabled chart 253 rm[r.Name] = true 254 } 255 } 256 // don't keep disabled charts in new slice 257 cd := []*chart.Chart{} 258 copy(cd, c.Dependencies[:0]) 259 for _, n := range c.Dependencies { 260 if _, ok := rm[n.Metadata.Name]; !ok { 261 cd = append(cd, n) 262 } 263 264 } 265 // recursively call self to process sub dependencies 266 for _, t := range cd { 267 err := ProcessRequirementsEnabled(t, &cc) 268 // if its not just missing requirements file, return error 269 if nerr, ok := err.(ErrNoRequirementsFile); !ok && err != nil { 270 return nerr 271 } 272 } 273 c.Dependencies = cd 274 275 return nil 276 } 277 278 // pathToMap creates a nested map given a YAML path in dot notation. 279 func pathToMap(path string, data map[string]interface{}) map[string]interface{} { 280 if path == "." { 281 return data 282 } 283 ap := strings.Split(path, ".") 284 if len(ap) == 0 { 285 return nil 286 } 287 n := []map[string]interface{}{} 288 // created nested map for each key, adding to slice 289 for _, v := range ap { 290 nm := make(map[string]interface{}) 291 nm[v] = make(map[string]interface{}) 292 n = append(n, nm) 293 } 294 // find the last key (map) and set our data 295 for i, d := range n { 296 for k := range d { 297 z := i + 1 298 if z == len(n) { 299 n[i][k] = data 300 break 301 } 302 n[i][k] = n[z] 303 } 304 } 305 306 return n[0] 307 } 308 309 // getParents returns a slice of parent charts in reverse order. 310 func getParents(c *chart.Chart, out []*chart.Chart) []*chart.Chart { 311 if len(out) == 0 { 312 out = []*chart.Chart{c} 313 } 314 for _, ch := range c.Dependencies { 315 if len(ch.Dependencies) > 0 { 316 out = append(out, ch) 317 out = getParents(ch, out) 318 } 319 } 320 321 return out 322 } 323 324 // processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field. 325 func processImportValues(c *chart.Chart, v *chart.Config) error { 326 reqs, err := LoadRequirements(c) 327 if err != nil { 328 return err 329 } 330 // combine chart values and its dependencies' values 331 cvals, err := CoalesceValues(c, v) 332 if err != nil { 333 return err 334 } 335 nv := v.GetValues() 336 b := make(map[string]interface{}, len(nv)) 337 // convert values to map 338 for kk, vvv := range nv { 339 b[kk] = vvv 340 } 341 // import values from each dependency if specified in import-values 342 for _, r := range reqs.Dependencies { 343 if len(r.ImportValues) > 0 { 344 var outiv []interface{} 345 for _, riv := range r.ImportValues { 346 switch iv := riv.(type) { 347 case map[string]interface{}: 348 nm := map[string]string{ 349 "child": iv["child"].(string), 350 "parent": iv["parent"].(string), 351 } 352 outiv = append(outiv, nm) 353 s := r.Name + "." + nm["child"] 354 // get child table 355 vv, err := cvals.Table(s) 356 if err != nil { 357 log.Printf("Warning: ImportValues missing table: %v", err) 358 continue 359 } 360 // create value map from child to be merged into parent 361 vm := pathToMap(nm["parent"], vv.AsMap()) 362 b = coalesceTables(cvals, vm) 363 case string: 364 nm := map[string]string{ 365 "child": "exports." + iv, 366 "parent": ".", 367 } 368 outiv = append(outiv, nm) 369 s := r.Name + "." + nm["child"] 370 vm, err := cvals.Table(s) 371 if err != nil { 372 log.Printf("Warning: ImportValues missing table: %v", err) 373 continue 374 } 375 b = coalesceTables(b, vm.AsMap()) 376 } 377 } 378 // set our formatted import values 379 r.ImportValues = outiv 380 } 381 } 382 b = coalesceTables(b, cvals) 383 y, err := yaml.Marshal(b) 384 if err != nil { 385 return err 386 } 387 // set the new values 388 c.Values.Raw = string(y) 389 390 return nil 391 } 392 393 // ProcessRequirementsImportValues imports specified chart values from child to parent. 394 func ProcessRequirementsImportValues(c *chart.Chart, v *chart.Config) error { 395 pc := getParents(c, nil) 396 for i := len(pc) - 1; i >= 0; i-- { 397 processImportValues(pc[i], v) 398 } 399 400 return nil 401 }