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