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