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