Skip to content

Commit

Permalink
feat(helm): add conditions and tags
Browse files Browse the repository at this point in the history
This feature adds the ability to selectively control the loading of charts using entries in top chart's values.
When 'helm install --set tags.mytag=true', charts with that tag will be enabled unless disabled in parent by condition.
When 'helm install --set mychart.enabled=true', charts with that yaml path specified will be enabled.

Closes #1837
  • Loading branch information
jascott1 committed Feb 11, 2017
1 parent 1fda4e8 commit 8ef733c
Show file tree
Hide file tree
Showing 32 changed files with 748 additions and 22 deletions.
6 changes: 6 additions & 0 deletions _proto/hapi/chart/metadata.proto
Expand Up @@ -64,4 +64,10 @@ message Metadata {

// The API Version of this chart.
string apiVersion = 10;

// The condition to check to enable chart
string condition = 11;

// The tags to check to enable chart
string tags = 12;
}
147 changes: 146 additions & 1 deletion pkg/chartutil/requirements.go
Expand Up @@ -17,10 +17,11 @@ package chartutil

import (
"errors"
"log"
"strings"
"time"

"github.com/ghodss/yaml"

"k8s.io/helm/pkg/proto/hapi/chart"
)

Expand Down Expand Up @@ -55,8 +56,17 @@ type Dependency struct {
// Appending `index.yaml` to this string should result in a URL that can be
// used to fetch the repository index.
Repository string `json:"repository"`
// A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled )
Condition string `json:"condition"`
// Tags can be used to group charts for enabling/disabling together
Tags []string `json:"tags"`
// Enabled bool determines if chart should be loaded
Enabled bool `json:"enabled"`
}

// ErrNoRequirementsFile to detect error condition
type ErrNoRequirementsFile error

// Requirements is a list of requirements for a chart.
//
// Requirements are charts upon which this chart depends. This expresses
Expand Down Expand Up @@ -106,3 +116,138 @@ func LoadRequirementsLock(c *chart.Chart) (*RequirementsLock, error) {
r := &RequirementsLock{}
return r, yaml.Unmarshal(data, r)
}

// ProcessRequirementsConditions disables charts based on condition path value in values
func ProcessRequirementsConditions(reqs *Requirements, cvals Values) {
var cond string
var conds []string

if reqs != nil && len(reqs.Dependencies) > 0 {
for _, r := range reqs.Dependencies {
var hasTrue, hasFalse bool
cond = string(r.Condition)
// check for list
if len(cond) > 0 {
if strings.Contains(cond, ",") {
conds = strings.Split(strings.TrimSpace(cond), ",")
} else {
conds = []string{strings.TrimSpace(cond)}
}
for _, c := range conds {
if len(c) > 0 {
// retrieve value
vv, err := cvals.PathValue(c)
if err == nil {
if vv.(bool) {
hasTrue = true
}
if !vv.(bool) {
hasFalse = true
}
} else {
if _, ok := err.(ErrNoValue); !ok {
// this is a real error
log.Printf("Warning: PathValue returned error %v", err)
}
}
if vv != nil {
// got first value, break loop
break
}
}
}
if !hasTrue && hasFalse {
r.Enabled = false
} else {
if hasTrue {
r.Enabled = true
}
}
}

}
}
}

// ProcessRequirementsTags disables charts based on tags in values
func ProcessRequirementsTags(reqs *Requirements, cvals Values) {
vt, err := cvals.Table("tags")
if err != nil {
return

}
if reqs != nil && len(reqs.Dependencies) > 0 {
for _, r := range reqs.Dependencies {
if len(r.Tags) > 0 {
tags := r.Tags

var hasTrue, hasFalse bool
for _, k := range tags {
if b, ok := vt[k]; ok {
if b.(bool) {
hasTrue = true
}
if !b.(bool) {
hasFalse = true
}
}
}
if !hasTrue && hasFalse {
r.Enabled = false
} else {
if hasTrue || !hasTrue && !hasFalse {
r.Enabled = true
}
}

}
}
}
}

// ProcessRequirementsEnabled removes disabled charts from dependencies
func ProcessRequirementsEnabled(c *chart.Chart, v *chart.Config) error {
reqs, err := LoadRequirements(c)
if err != nil {
return ErrRequirementsNotFound
}
// set all to true
for _, lr := range reqs.Dependencies {
lr.Enabled = true
}
cvals, err := CoalesceValues(c, v)
if err != nil {
return err
}
// flag dependencies as enabled/disabled
ProcessRequirementsTags(reqs, cvals)
ProcessRequirementsConditions(reqs, cvals)

// make a map of charts to keep
rm := map[string]bool{}
for _, r := range reqs.Dependencies {
if !r.Enabled {
// remove disabled chart
rm[r.Name] = true
}
}
// don't keep disabled charts in new slice
cd := c.Dependencies[:0]
for _, n := range c.Dependencies {
if _, ok := rm[n.Metadata.Name]; !ok {
cd = append(cd, n)
}

}
// recursively call self to process sub dependencies
for _, t := range cd {
err := ProcessRequirementsEnabled(t, v)
// if its not just missing requirements file, return error
if nerr, ok := err.(ErrNoRequirementsFile); !ok && err != nil {
return nerr
}
}
c.Dependencies = cd

return nil
}
173 changes: 173 additions & 0 deletions pkg/chartutil/requirements_test.go
Expand Up @@ -15,7 +15,10 @@ limitations under the License.
package chartutil

import (
"sort"
"testing"

"k8s.io/helm/pkg/proto/hapi/chart"
)

func TestLoadRequirements(t *testing.T) {
Expand All @@ -33,3 +36,173 @@ func TestLoadRequirementsLock(t *testing.T) {
}
verifyRequirementsLock(t, c)
}
func TestRequirementsTagsNonValue(t *testing.T) {
c, err := Load("testdata/subpop")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
// tags with no effect
v := &chart.Config{Raw: string("tags:\n nothinguseful: false\n\n")}
// expected charts including duplicates in alphanumeric order
e := []string{"parentchart", "subchart1", "subcharta", "subchartb"}

verifyRequirementsEnabled(t, c, v, e)
}
func TestRequirementsTagsDisabledL1(t *testing.T) {
c, err := Load("testdata/subpop")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
// tags disabling a group
v := &chart.Config{Raw: string("tags:\n front-end: false\n\n")}
// expected charts including duplicates in alphanumeric order
e := []string{"parentchart"}

verifyRequirementsEnabled(t, c, v, e)
}
func TestRequirementsTagsEnabledL1(t *testing.T) {
c, err := Load("testdata/subpop")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
// tags disabling a group and enabling a different group
v := &chart.Config{Raw: string("tags:\n front-end: false\n\n back-end: true\n")}
// expected charts including duplicates in alphanumeric order
e := []string{"parentchart", "subchart2", "subchartb", "subchartc"}

verifyRequirementsEnabled(t, c, v, e)
}
func TestRequirementsTagsDisabledL2(t *testing.T) {
c, err := Load("testdata/subpop")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
// tags disabling only children
v := &chart.Config{Raw: string("tags:\n subcharta: false\n\n subchartb: false\n")}
// expected charts including duplicates in alphanumeric order
e := []string{"parentchart", "subchart1"}

verifyRequirementsEnabled(t, c, v, e)
}
func TestRequirementsTagsDisabledL1Mixed(t *testing.T) {
c, err := Load("testdata/subpop")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
// tags disabling all parents/children with additional tag re-enabling a parent
v := &chart.Config{Raw: string("tags:\n front-end: false\n\n subchart1: true\n\n back-end: false\n")}
// expected charts including duplicates in alphanumeric order
e := []string{"parentchart", "subchart1"}

verifyRequirementsEnabled(t, c, v, e)
}
func TestRequirementsConditionsNonValue(t *testing.T) {
c, err := Load("testdata/subpop")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
// tags with no effect
v := &chart.Config{Raw: string("subchart1:\n nothinguseful: false\n\n")}
// expected charts including duplicates in alphanumeric order
e := []string{"parentchart", "subchart1", "subcharta", "subchartb"}

verifyRequirementsEnabled(t, c, v, e)
}
func TestRequirementsConditionsEnabledL1Both(t *testing.T) {
c, err := Load("testdata/subpop")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
// conditions enabling the parent charts, effectively enabling children
v := &chart.Config{Raw: string("subchart1:\n enabled: true\nsubchart2:\n enabled: true\n")}
// expected charts including duplicates in alphanumeric order
e := []string{"parentchart", "subchart1", "subchart2", "subcharta", "subchartb", "subchartb", "subchartc"}

verifyRequirementsEnabled(t, c, v, e)
}
func TestRequirementsConditionsDisabledL1Both(t *testing.T) {
c, err := Load("testdata/subpop")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
// conditions disabling the parent charts, effectively disabling children
v := &chart.Config{Raw: string("subchart1:\n enabled: false\nsubchart2:\n enabled: false\n")}
// expected charts including duplicates in alphanumeric order
e := []string{"parentchart"}

verifyRequirementsEnabled(t, c, v, e)
}

func TestRequirementsConditionsSecond(t *testing.T) {
c, err := Load("testdata/subpop")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
// conditions a child using the second condition path of child's condition
v := &chart.Config{Raw: string("subchart1:\n subcharta:\n enabled: false\n")}
// expected charts including duplicates in alphanumeric order
e := []string{"parentchart", "subchart1", "subchartb"}

verifyRequirementsEnabled(t, c, v, e)
}
func TestRequirementsCombinedDisabledL2(t *testing.T) {
c, err := Load("testdata/subpop")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
// tags enabling a parent/child group with condition disabling one child
v := &chart.Config{Raw: string("subchartc:\n enabled: false\ntags:\n back-end: true\n")}
// expected charts including duplicates in alphanumeric order
e := []string{"parentchart", "subchart1", "subchart2", "subcharta", "subchartb", "subchartb"}

verifyRequirementsEnabled(t, c, v, e)
}
func TestRequirementsCombinedDisabledL1(t *testing.T) {
c, err := Load("testdata/subpop")
if err != nil {
t.Fatalf("Failed to load testdata: %s", err)
}
// tags will not enable a child if parent is explicitly disabled with condition
v := &chart.Config{Raw: string("subchart1:\n enabled: false\ntags:\n front-end: true\n")}
// expected charts including duplicates in alphanumeric order
e := []string{"parentchart"}

verifyRequirementsEnabled(t, c, v, e)
}

func verifyRequirementsEnabled(t *testing.T, c *chart.Chart, v *chart.Config, e []string) {
out := []*chart.Chart{}
err := ProcessRequirementsEnabled(c, v)
if err != nil {
t.Errorf("Error processing enabled requirements %v", err)
}
out = extractCharts(c, out)
// build list of chart names
p := []string{}
for _, r := range out {
p = append(p, r.Metadata.Name)
}
//sort alphanumeric and compare to expectations
sort.Strings(p)
if len(p) != len(e) {
t.Errorf("Error slice lengths do not match got %v, expected %v", len(p), len(e))
return
}
for i := range p {
if p[i] != e[i] {
t.Errorf("Error slice values do not match got %v, expected %v", p[i], e[i])
}
}
}

// extractCharts recursively searches chart dependencies returning all charts found
func extractCharts(c *chart.Chart, out []*chart.Chart) []*chart.Chart {

if len(c.Metadata.Name) > 0 {
out = append(out, c)
}
for _, d := range c.Dependencies {
out = extractCharts(d, out)
}
return out
}
4 changes: 4 additions & 0 deletions pkg/chartutil/testdata/subpop/Chart.yaml
@@ -0,0 +1,4 @@
apiVersion: v1
description: A Helm chart for Kubernetes
name: parentchart
version: 0.1.0
18 changes: 18 additions & 0 deletions pkg/chartutil/testdata/subpop/README.md
@@ -0,0 +1,18 @@
## Subpop

This chart is for testing the processing of enabled/disabled charts
via conditions and tags.

Currently there are three levels:

````
parent
-1 tags: front-end, subchart1
--A tags: front-end, subchartA
--B tags: front-end, subchartB
-2 tags: back-end, subchart2
--B tags: back-end, subchartB
--C tags: back-end, subchartC
````

Tags and conditions are currently in requirements.yaml files.
4 changes: 4 additions & 0 deletions pkg/chartutil/testdata/subpop/charts/subchart1/Chart.yaml
@@ -0,0 +1,4 @@
apiVersion: v1
description: A Helm chart for Kubernetes
name: subchart1
version: 0.1.0

0 comments on commit 8ef733c

Please sign in to comment.