Commit 8ef733ca authored by Justin Scott's avatar Justin Scott
Browse files

feat(helm): add conditions and tags

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
parent 1fda4e8a
main Release add-codeql dependabot/go_modules/github.com/docker/distribution-2.8.2incompatible dependabot/go_modules/github.com/lib/pq-1.10.9 dependabot/go_modules/github.com/rubenv/sql-migrate-1.4.0 dependabot/go_modules/golang.org/x/crypto-0.9.0 dependabot/go_modules/golang.org/x/term-0.8.0 dependabot/go_modules/k8s.io/klog/v2-2.100.1 dev-v2 feat-v3/event-emitter-lua release-2.10 release-2.11 release-2.12 release-2.13 release-2.14 release-2.15 release-2.16 release-2.17 release-2.2 release-2.3 release-2.4 release-2.5 release-2.6 release-2.7 release-2.8 release-2.9 release-3.0 release-3.1 release-3.10 release-3.11 release-3.12 release-3.2 release-3.3 release-3.4 release-3.5 release-3.6 release-3.6.1 release-3.6.2 release-3.7 release-3.8 release-3.9 release-v3.0.0-beta.4 v3.12.0 v3.12.0-rc.1 v3.12.0-dev.1 v3.11.3 v3.11.2 v3.11.1 v3.11.0 v3.11.0-rc.2 v3.11.0-rc.1 v3.10.3 v3.10.2 v3.10.1 v3.10.0 v3.10.0-rc.1 v3.9.4 v3.9.3 v3.9.2 v3.9.1 v3.9.0 v3.9.0-rc.1 v3.8.2 v3.8.1 v3.8.0 v3.8.0-rc.2 v3.8.0-rc.1 v3.7.2 v3.7.1 v3.7.0 v3.7.0-rc.3 v3.7.0-rc.2 v3.7.0-rc.1 v3.6.3 v3.6.2 v3.6.1 v3.6.0 v3.6.0-rc.1 v3.5.4 v3.5.3 v3.5.2 v3.5.1 v3.5.0 v3.5.0-rc.2 v3.5.0-rc.1 v3.4.2 v3.4.1 v3.4.0 v3.4.0-rc.1 v3.3.4 v3.3.3 v3.3.2 v3.3.1 v3.3.0 v3.3.0-rc.2 v3.3.0-rc.1 v3.2.4 v3.2.3 v3.2.2 v3.2.1 v3.2.0 v3.2.0-rc.1 v3.1.3 v3.1.2 v3.1.1 v3.1.0 v3.1.0-rc.3 v3.1.0-rc.2 v3.1.0-rc.1 v3.0.3 v3.0.2 v3.0.1 v3.0.0 v3.0.0-rc.4 v3.0.0-rc.3 v3.0.0-rc.2 v3.0.0-rc.1 v3.0.0-beta.5 v3.0.0-beta.4 v3.0.0-beta.3 v3.0.0-beta.2 v3.0.0-beta.1 v3.0.0-alpha.2 v3.0.0-alpha.1 v2.17.0 v2.17.0-rc.1 v2.16.12 v2.16.11 v2.16.10 v2.16.9 v2.16.8 v2.16.7 v2.16.6 v2.16.5 v2.16.4 v2.16.3 v2.16.2 v2.16.1 v2.16.0 v2.16.0-rc.2 v2.16.0-rc.1 v2.15.2 v2.15.1 v2.15.0 v2.15.0-rc.2 v2.15.0-rc.1 v2.14.3 v2.14.2 v2.14.1 v2.14.0 v2.14.0-rc.2 v2.14.0-rc.1 v2.13.1 v2.13.1-rc.1 v2.13.0 v2.13.0-rc.2 v2.13.0-rc.1 v2.12.3 v2.12.2 v2.12.1 v2.12.0 v2.12.0-rc.2 v2.12.0-rc.1 v2.11.0 v2.11.0-rc.4 v2.11.0-rc.3 v2.11.0-rc.2 v2.11.0-rc.1 v2.10.0 v2.10.0-rc.3 v2.10.0-rc.2 v2.10.0-rc.1 v2.9.1 v2.9.0 v2.9.0-rc5 v2.9.0-rc4 v2.9.0-rc3 v2.9.0-rc2 v2.9.0-rc1 v2.8.2 v2.8.2-rc1 v2.8.1 v2.8.0 v2.8.0-rc.1 v2.7.2 v2.7.1 v2.7.0 v2.7.0-rc1 v2.6.2 v2.6.1 v2.6.0 v2.5.1 v2.5.0 v2.4.2 v2.4.1 v2.4.0 v2.3.1 v2.3.0 v2.2.3 v2.2.2 v2.2.1 v2.2.0
No related merge requests found
Showing with 530 additions and 1 deletion
+530 -1
......@@ -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;
}
......@@ -17,10 +17,11 @@ package chartutil
import (
"errors"
"log"
"strings"
"time"
"github.com/ghodss/yaml"
"k8s.io/helm/pkg/proto/hapi/chart"
)
......@@ -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
......@@ -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
}
......@@ -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) {
......@@ -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
}
apiVersion: v1
description: A Helm chart for Kubernetes
name: parentchart
version: 0.1.0
\ No newline at end of file
## 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.
\ No newline at end of file
apiVersion: v1
description: A Helm chart for Kubernetes
name: subchart1
version: 0.1.0
apiVersion: v1
description: A Helm chart for Kubernetes
name: subcharta
version: 0.1.0
apiVersion: v1
kind: Service
metadata:
name: {{ .Chart.Name }}
labels:
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.externalPort }}
targetPort: {{ .Values.service.internalPort }}
protocol: TCP
name: {{ .Values.service.name }}
selector:
app: {{ .Chart.Name }}
# Default values for subchart.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: nginx
tag: stable
pullPolicy: IfNotPresent
service:
name: nginx
type: ClusterIP
externalPort: 80
internalPort: 80
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
apiVersion: v1
description: A Helm chart for Kubernetes
name: subchartb
version: 0.1.0
apiVersion: v1
kind: Service
metadata:
name: {{ .Chart.Name }}
labels:
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.externalPort }}
targetPort: {{ .Values.service.internalPort }}
protocol: TCP
name: {{ .Values.service.name }}
selector:
app: {{ .Chart.Name }}
# Default values for subchart.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: nginx
tag: stable
pullPolicy: IfNotPresent
service:
name: nginx
type: ClusterIP
externalPort: 80
internalPort: 80
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
dependencies:
- name: subcharta
repository: http://localhost:10191
version: 0.1.0
condition: subcharta.enabled,subchart1.subcharta.enabled
tags:
- front-end
- subcharta
- name: subchartb
repository: http://localhost:10191
version: 0.1.0
condition: subchartb.enabled
tags:
- front-end
- subchartb
\ No newline at end of file
apiVersion: v1
kind: Service
metadata:
name: {{ .Chart.Name }}
labels:
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.externalPort }}
targetPort: {{ .Values.service.internalPort }}
protocol: TCP
name: {{ .Values.service.name }}
selector:
app: {{ .Chart.Name }}
# Default values for subchart.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: nginx
tag: stable
pullPolicy: IfNotPresent
service:
name: nginx
type: ClusterIP
externalPort: 80
internalPort: 80
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
apiVersion: v1
description: A Helm chart for Kubernetes
name: subchart2
version: 0.1.0
apiVersion: v1
description: A Helm chart for Kubernetes
name: subchartb
version: 0.1.0
apiVersion: v1
kind: Service
metadata:
name: subchart2-{{ .Chart.Name }}
labels:
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.externalPort }}
targetPort: {{ .Values.service.internalPort }}
protocol: TCP
name: subchart2-{{ .Values.service.name }}
selector:
app: {{ .Chart.Name }}
# Default values for subchart.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: nginx
tag: stable
pullPolicy: IfNotPresent
service:
name: nginx
type: ClusterIP
externalPort: 80
internalPort: 80
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
apiVersion: v1
description: A Helm chart for Kubernetes
name: subchartc
version: 0.1.0
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment