github.com/caicloud/helm@v2.5.0+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 // Alias usable alias to be used for the chart 69 Alias string `json:"alias"` 70 } 71 72 // ErrNoRequirementsFile to detect error condition 73 type ErrNoRequirementsFile error 74 75 // Requirements is a list of requirements for a chart. 76 // 77 // Requirements are charts upon which this chart depends. This expresses 78 // developer intent. 79 type Requirements struct { 80 Dependencies []*Dependency `json:"dependencies"` 81 } 82 83 // RequirementsLock is a lock file for requirements. 84 // 85 // It represents the state that the dependencies should be in. 86 type RequirementsLock struct { 87 // Genderated is the date the lock file was last generated. 88 Generated time.Time `json:"generated"` 89 // Digest is a hash of the requirements file used to generate it. 90 Digest string `json:"digest"` 91 // Dependencies is the list of dependencies that this lock file has locked. 92 Dependencies []*Dependency `json:"dependencies"` 93 } 94 95 // LoadRequirements loads a requirements file from an in-memory chart. 96 func LoadRequirements(c *chart.Chart) (*Requirements, error) { 97 var data []byte 98 for _, f := range c.Files { 99 if f.TypeUrl == requirementsName { 100 data = f.Value 101 } 102 } 103 if len(data) == 0 { 104 return nil, ErrRequirementsNotFound 105 } 106 r := &Requirements{} 107 return r, yaml.Unmarshal(data, r) 108 } 109 110 // LoadRequirementsLock loads a requirements lock file. 111 func LoadRequirementsLock(c *chart.Chart) (*RequirementsLock, error) { 112 var data []byte 113 for _, f := range c.Files { 114 if f.TypeUrl == lockfileName { 115 data = f.Value 116 } 117 } 118 if len(data) == 0 { 119 return nil, ErrLockfileNotFound 120 } 121 r := &RequirementsLock{} 122 return r, yaml.Unmarshal(data, r) 123 } 124 125 // ProcessRequirementsConditions disables charts based on condition path value in values 126 func ProcessRequirementsConditions(reqs *Requirements, cvals Values) { 127 var cond string 128 var conds []string 129 if reqs == nil || len(reqs.Dependencies) == 0 { 130 return 131 } 132 for _, r := range reqs.Dependencies { 133 var hasTrue, hasFalse bool 134 cond = string(r.Condition) 135 // check for list 136 if len(cond) > 0 { 137 if strings.Contains(cond, ",") { 138 conds = strings.Split(strings.TrimSpace(cond), ",") 139 } else { 140 conds = []string{strings.TrimSpace(cond)} 141 } 142 for _, c := range conds { 143 if len(c) > 0 { 144 // retrieve value 145 vv, err := cvals.PathValue(c) 146 if err == nil { 147 // if not bool, warn 148 if bv, ok := vv.(bool); ok { 149 if bv { 150 hasTrue = true 151 } else { 152 hasFalse = true 153 } 154 } else { 155 log.Printf("Warning: Condition path '%s' for chart %s returned non-bool value", c, r.Name) 156 } 157 } else if _, ok := err.(ErrNoValue); !ok { 158 // this is a real error 159 log.Printf("Warning: PathValue returned error %v", err) 160 161 } 162 if vv != nil { 163 // got first value, break loop 164 break 165 } 166 } 167 } 168 if !hasTrue && hasFalse { 169 r.Enabled = false 170 } else if hasTrue { 171 r.Enabled = true 172 173 } 174 } 175 176 } 177 178 } 179 180 // ProcessRequirementsTags disables charts based on tags in values 181 func ProcessRequirementsTags(reqs *Requirements, cvals Values) { 182 vt, err := cvals.Table("tags") 183 if err != nil { 184 return 185 186 } 187 if reqs == nil || len(reqs.Dependencies) == 0 { 188 return 189 } 190 for _, r := range reqs.Dependencies { 191 if len(r.Tags) > 0 { 192 tags := r.Tags 193 194 var hasTrue, hasFalse bool 195 for _, k := range tags { 196 if b, ok := vt[k]; ok { 197 // if not bool, warn 198 if bv, ok := b.(bool); ok { 199 if bv { 200 hasTrue = true 201 } else { 202 hasFalse = true 203 } 204 } else { 205 log.Printf("Warning: Tag '%s' for chart %s returned non-bool value", k, r.Name) 206 } 207 } 208 } 209 if !hasTrue && hasFalse { 210 r.Enabled = false 211 } else if hasTrue || !hasTrue && !hasFalse { 212 r.Enabled = true 213 214 } 215 216 } 217 } 218 219 } 220 221 func getAliasDependency(charts []*chart.Chart, aliasChart *Dependency) *chart.Chart { 222 var chartFound chart.Chart 223 for _, existingChart := range charts { 224 if existingChart == nil { 225 continue 226 } 227 if existingChart.Metadata == nil { 228 continue 229 } 230 if existingChart.Metadata.Name != aliasChart.Name { 231 continue 232 } 233 if existingChart.Metadata.Version != aliasChart.Version { 234 continue 235 } 236 chartFound = *existingChart 237 newMetadata := *existingChart.Metadata 238 if aliasChart.Alias != "" { 239 newMetadata.Name = aliasChart.Alias 240 } 241 chartFound.Metadata = &newMetadata 242 return &chartFound 243 } 244 return nil 245 } 246 247 // ProcessRequirementsEnabled removes disabled charts from dependencies 248 func ProcessRequirementsEnabled(c *chart.Chart, v *chart.Config) error { 249 reqs, err := LoadRequirements(c) 250 if err != nil { 251 // if not just missing requirements file, return error 252 if nerr, ok := err.(ErrNoRequirementsFile); !ok { 253 return nerr 254 } 255 256 // no requirements to process 257 return nil 258 } 259 260 var chartDependencies []*chart.Chart 261 for _, req := range reqs.Dependencies { 262 if chartDependency := getAliasDependency(c.Dependencies, req); chartDependency != nil { 263 chartDependencies = append(chartDependencies, chartDependency) 264 } 265 if req.Alias != "" { 266 req.Name = req.Alias 267 } 268 } 269 c.Dependencies = chartDependencies 270 271 // set all to true 272 for _, lr := range reqs.Dependencies { 273 lr.Enabled = true 274 } 275 cvals, err := CoalesceValues(c, v) 276 if err != nil { 277 return err 278 } 279 // convert our values back into config 280 yvals, err := cvals.YAML() 281 if err != nil { 282 return err 283 } 284 cc := chart.Config{Raw: yvals} 285 // flag dependencies as enabled/disabled 286 ProcessRequirementsTags(reqs, cvals) 287 ProcessRequirementsConditions(reqs, cvals) 288 // make a map of charts to remove 289 rm := map[string]bool{} 290 for _, r := range reqs.Dependencies { 291 if !r.Enabled { 292 // remove disabled chart 293 rm[r.Name] = true 294 } 295 } 296 // don't keep disabled charts in new slice 297 cd := []*chart.Chart{} 298 copy(cd, c.Dependencies[:0]) 299 for _, n := range c.Dependencies { 300 if _, ok := rm[n.Metadata.Name]; !ok { 301 cd = append(cd, n) 302 } 303 304 } 305 // recursively call self to process sub dependencies 306 for _, t := range cd { 307 err := ProcessRequirementsEnabled(t, &cc) 308 // if its not just missing requirements file, return error 309 if nerr, ok := err.(ErrNoRequirementsFile); !ok && err != nil { 310 return nerr 311 } 312 } 313 c.Dependencies = cd 314 315 return nil 316 } 317 318 // pathToMap creates a nested map given a YAML path in dot notation. 319 func pathToMap(path string, data map[string]interface{}) map[string]interface{} { 320 if path == "." { 321 return data 322 } 323 ap := strings.Split(path, ".") 324 if len(ap) == 0 { 325 return nil 326 } 327 n := []map[string]interface{}{} 328 // created nested map for each key, adding to slice 329 for _, v := range ap { 330 nm := make(map[string]interface{}) 331 nm[v] = make(map[string]interface{}) 332 n = append(n, nm) 333 } 334 // find the last key (map) and set our data 335 for i, d := range n { 336 for k := range d { 337 z := i + 1 338 if z == len(n) { 339 n[i][k] = data 340 break 341 } 342 n[i][k] = n[z] 343 } 344 } 345 346 return n[0] 347 } 348 349 // getParents returns a slice of parent charts in reverse order. 350 func getParents(c *chart.Chart, out []*chart.Chart) []*chart.Chart { 351 if len(out) == 0 { 352 out = []*chart.Chart{c} 353 } 354 for _, ch := range c.Dependencies { 355 if len(ch.Dependencies) > 0 { 356 out = append(out, ch) 357 out = getParents(ch, out) 358 } 359 } 360 361 return out 362 } 363 364 // processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field. 365 func processImportValues(c *chart.Chart) error { 366 reqs, err := LoadRequirements(c) 367 if err != nil { 368 return err 369 } 370 // combine chart values and empty config to get Values 371 cvals, err := CoalesceValues(c, &chart.Config{}) 372 if err != nil { 373 return err 374 } 375 b := make(map[string]interface{}, 0) 376 // import values from each dependency if specified in import-values 377 for _, r := range reqs.Dependencies { 378 if len(r.ImportValues) > 0 { 379 var outiv []interface{} 380 for _, riv := range r.ImportValues { 381 switch iv := riv.(type) { 382 case map[string]interface{}: 383 nm := map[string]string{ 384 "child": iv["child"].(string), 385 "parent": iv["parent"].(string), 386 } 387 outiv = append(outiv, nm) 388 s := r.Name + "." + nm["child"] 389 // get child table 390 vv, err := cvals.Table(s) 391 if err != nil { 392 log.Printf("Warning: ImportValues missing table: %v", err) 393 continue 394 } 395 // create value map from child to be merged into parent 396 vm := pathToMap(nm["parent"], vv.AsMap()) 397 b = coalesceTables(cvals, vm) 398 case string: 399 nm := map[string]string{ 400 "child": "exports." + iv, 401 "parent": ".", 402 } 403 outiv = append(outiv, nm) 404 s := r.Name + "." + nm["child"] 405 vm, err := cvals.Table(s) 406 if err != nil { 407 log.Printf("Warning: ImportValues missing table: %v", err) 408 continue 409 } 410 b = coalesceTables(b, vm.AsMap()) 411 } 412 } 413 // set our formatted import values 414 r.ImportValues = outiv 415 } 416 } 417 b = coalesceTables(b, cvals) 418 y, err := yaml.Marshal(b) 419 if err != nil { 420 return err 421 } 422 423 // set the new values 424 c.Values = &chart.Config{Raw: string(y)} 425 426 return nil 427 } 428 429 // ProcessRequirementsImportValues imports specified chart values from child to parent. 430 func ProcessRequirementsImportValues(c *chart.Chart) error { 431 pc := getParents(c, nil) 432 for i := len(pc) - 1; i >= 0; i-- { 433 processImportValues(pc[i]) 434 } 435 436 return nil 437 }