github.com/migueleliasweb/helm@v2.6.1+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,omitempty"` 61 // Tags can be used to group charts for enabling/disabling together 62 Tags []string `json:"tags,omitempty"` 63 // Enabled bool determines if chart should be loaded 64 Enabled bool `json:"enabled,omitempty"` 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,omitempty"` 68 // Alias usable alias to be used for the chart 69 Alias string `json:"alias,omitempty"` 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 // If any dependency is not a part of requirements.yaml 262 // then this should be added to chartDependencies. 263 // However, if the dependency is already specified in requirements.yaml 264 // we should not add it, as it would be anyways processed from requirements.yaml 265 266 for _, existingDependency := range c.Dependencies { 267 var dependencyFound bool 268 for _, req := range reqs.Dependencies { 269 if existingDependency.Metadata.Name == req.Name && existingDependency.Metadata.Version == req.Version { 270 dependencyFound = true 271 break 272 } 273 } 274 if !dependencyFound { 275 chartDependencies = append(chartDependencies, existingDependency) 276 } 277 } 278 279 for _, req := range reqs.Dependencies { 280 if chartDependency := getAliasDependency(c.Dependencies, req); chartDependency != nil { 281 chartDependencies = append(chartDependencies, chartDependency) 282 } 283 if req.Alias != "" { 284 req.Name = req.Alias 285 } 286 } 287 c.Dependencies = chartDependencies 288 289 // set all to true 290 for _, lr := range reqs.Dependencies { 291 lr.Enabled = true 292 } 293 cvals, err := CoalesceValues(c, v) 294 if err != nil { 295 return err 296 } 297 // convert our values back into config 298 yvals, err := cvals.YAML() 299 if err != nil { 300 return err 301 } 302 cc := chart.Config{Raw: yvals} 303 // flag dependencies as enabled/disabled 304 ProcessRequirementsTags(reqs, cvals) 305 ProcessRequirementsConditions(reqs, cvals) 306 // make a map of charts to remove 307 rm := map[string]bool{} 308 for _, r := range reqs.Dependencies { 309 if !r.Enabled { 310 // remove disabled chart 311 rm[r.Name] = true 312 } 313 } 314 // don't keep disabled charts in new slice 315 cd := []*chart.Chart{} 316 copy(cd, c.Dependencies[:0]) 317 for _, n := range c.Dependencies { 318 if _, ok := rm[n.Metadata.Name]; !ok { 319 cd = append(cd, n) 320 } 321 322 } 323 // recursively call self to process sub dependencies 324 for _, t := range cd { 325 err := ProcessRequirementsEnabled(t, &cc) 326 // if its not just missing requirements file, return error 327 if nerr, ok := err.(ErrNoRequirementsFile); !ok && err != nil { 328 return nerr 329 } 330 } 331 c.Dependencies = cd 332 333 return nil 334 } 335 336 // pathToMap creates a nested map given a YAML path in dot notation. 337 func pathToMap(path string, data map[string]interface{}) map[string]interface{} { 338 if path == "." { 339 return data 340 } 341 ap := strings.Split(path, ".") 342 if len(ap) == 0 { 343 return nil 344 } 345 n := []map[string]interface{}{} 346 // created nested map for each key, adding to slice 347 for _, v := range ap { 348 nm := make(map[string]interface{}) 349 nm[v] = make(map[string]interface{}) 350 n = append(n, nm) 351 } 352 // find the last key (map) and set our data 353 for i, d := range n { 354 for k := range d { 355 z := i + 1 356 if z == len(n) { 357 n[i][k] = data 358 break 359 } 360 n[i][k] = n[z] 361 } 362 } 363 364 return n[0] 365 } 366 367 // getParents returns a slice of parent charts in reverse order. 368 func getParents(c *chart.Chart, out []*chart.Chart) []*chart.Chart { 369 if len(out) == 0 { 370 out = []*chart.Chart{c} 371 } 372 for _, ch := range c.Dependencies { 373 if len(ch.Dependencies) > 0 { 374 out = append(out, ch) 375 out = getParents(ch, out) 376 } 377 } 378 379 return out 380 } 381 382 // processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field. 383 func processImportValues(c *chart.Chart) error { 384 reqs, err := LoadRequirements(c) 385 if err != nil { 386 return err 387 } 388 // combine chart values and empty config to get Values 389 cvals, err := CoalesceValues(c, &chart.Config{}) 390 if err != nil { 391 return err 392 } 393 b := make(map[string]interface{}, 0) 394 // import values from each dependency if specified in import-values 395 for _, r := range reqs.Dependencies { 396 if len(r.ImportValues) > 0 { 397 var outiv []interface{} 398 for _, riv := range r.ImportValues { 399 switch iv := riv.(type) { 400 case map[string]interface{}: 401 nm := map[string]string{ 402 "child": iv["child"].(string), 403 "parent": iv["parent"].(string), 404 } 405 outiv = append(outiv, nm) 406 s := r.Name + "." + nm["child"] 407 // get child table 408 vv, err := cvals.Table(s) 409 if err != nil { 410 log.Printf("Warning: ImportValues missing table: %v", err) 411 continue 412 } 413 // create value map from child to be merged into parent 414 vm := pathToMap(nm["parent"], vv.AsMap()) 415 b = coalesceTables(cvals, vm) 416 case string: 417 nm := map[string]string{ 418 "child": "exports." + iv, 419 "parent": ".", 420 } 421 outiv = append(outiv, nm) 422 s := r.Name + "." + nm["child"] 423 vm, err := cvals.Table(s) 424 if err != nil { 425 log.Printf("Warning: ImportValues missing table: %v", err) 426 continue 427 } 428 b = coalesceTables(b, vm.AsMap()) 429 } 430 } 431 // set our formatted import values 432 r.ImportValues = outiv 433 } 434 } 435 b = coalesceTables(b, cvals) 436 y, err := yaml.Marshal(b) 437 if err != nil { 438 return err 439 } 440 441 // set the new values 442 c.Values = &chart.Config{Raw: string(y)} 443 444 return nil 445 } 446 447 // ProcessRequirementsImportValues imports specified chart values from child to parent. 448 func ProcessRequirementsImportValues(c *chart.Chart) error { 449 pc := getParents(c, nil) 450 for i := len(pc) - 1; i >= 0; i-- { 451 processImportValues(pc[i]) 452 } 453 454 return nil 455 }