mirror of
https://github.com/helmfile/helmfile.git
synced 2026-10-18 20:12:13 +02:00
Allow running helmfile against a subset of releases (#30)
This adds `releases[].labels` in which you can set arbitrary number of key-value pairs, so that commands like `helmfile sync --selector key=value` can be used to run the helmfile subcommand against a subnet of declared releases. `labels` and `selector` are named as such on purpose of being consistent with terminology of Kubernetes and other tools in the K8S ecosystem, including kubectl, stern, helm, and so on. Resolves #8
This commit is contained in:
committed by
KUOKA Yusuke
parent
e81541d65c
commit
4b08ea9292
@@ -0,0 +1,64 @@
|
||||
package state
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ReleaseFilter is used to determine if a given release should be used during helmfile execution
|
||||
type ReleaseFilter interface {
|
||||
// Match returns true if the ReleaseSpec matches the Filter
|
||||
Match(r ReleaseSpec) bool
|
||||
}
|
||||
|
||||
// LabelFilter matches a release with the given positive lables. Negative labels
|
||||
// invert the match for cases such as tier!=backend
|
||||
type LabelFilter struct {
|
||||
positiveLabels map[string]string
|
||||
negativeLabels map[string]string
|
||||
}
|
||||
|
||||
// Match will match a release that has the same labels as the filter
|
||||
func (l LabelFilter) Match(r ReleaseSpec) bool {
|
||||
if len(l.positiveLabels) > 0 {
|
||||
for k, v := range l.positiveLabels {
|
||||
if rVal, ok := r.Labels[k]; !ok {
|
||||
return false
|
||||
} else if rVal != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(l.negativeLabels) > 0 {
|
||||
for k, v := range l.negativeLabels {
|
||||
if rVal, ok := r.Labels[k]; !ok {
|
||||
return true
|
||||
} else if rVal == v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ParseLabels takes a label in the form foo=bar,baz!=bat and returns a LabelFilter that will match the labels
|
||||
func ParseLabels(l string) (LabelFilter, error) {
|
||||
lf := LabelFilter{}
|
||||
lf.positiveLabels = map[string]string{}
|
||||
lf.negativeLabels = map[string]string{}
|
||||
var err error
|
||||
labels := strings.Split(l, ",")
|
||||
for _, label := range labels {
|
||||
if match, _ := regexp.MatchString("^[a-zA-Z0-9_-]+!=[a-zA-Z0-9_-]+$", label); match == true { // k!=v case
|
||||
kv := strings.Split(label, "!=")
|
||||
lf.negativeLabels[kv[0]] = kv[1]
|
||||
} else if match, _ := regexp.MatchString("^[a-zA-Z0-9_-]+=[a-zA-Z0-9_-]+$", label); match == true { // k=v case
|
||||
kv := strings.Split(label, "=")
|
||||
lf.positiveLabels[kv[0]] = kv[1]
|
||||
} else { // malformed case
|
||||
err = fmt.Errorf("Malformed label: %s. Expected label in form k=v or k!=v", label)
|
||||
}
|
||||
}
|
||||
return lf, err
|
||||
}
|
||||
+45
-9
@@ -42,11 +42,12 @@ type ReleaseSpec struct {
|
||||
Verify bool `yaml:"verify"`
|
||||
|
||||
// Name is the name of this release
|
||||
Name string `yaml:"name"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
Values []string `yaml:"values"`
|
||||
Secrets []string `yaml:"secrets"`
|
||||
SetValues []SetValue `yaml:"set"`
|
||||
Name string `yaml:"name"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
Labels map[string]string `yaml:"labels"`
|
||||
Values []string `yaml:"values"`
|
||||
Secrets []string `yaml:"secrets"`
|
||||
SetValues []SetValue `yaml:"set"`
|
||||
|
||||
// The 'env' section is not really necessary any longer, as 'set' would now provide the same functionality
|
||||
EnvValues []SetValue `yaml:"env"`
|
||||
@@ -148,7 +149,7 @@ func (state *HelmState) SyncRepos(helm helmexec.Interface) []error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (state *HelmState) SyncReleases(helm helmexec.Interface, additonalValues []string, workerLimit int) []error {
|
||||
func (state *HelmState) SyncReleases(helm helmexec.Interface, additionalValues []string, workerLimit int) []error {
|
||||
errs := []error{}
|
||||
jobQueue := make(chan ReleaseSpec)
|
||||
doneQueue := make(chan bool)
|
||||
@@ -169,7 +170,7 @@ func (state *HelmState) SyncReleases(helm helmexec.Interface, additonalValues []
|
||||
}
|
||||
|
||||
haveValueErr := false
|
||||
for _, value := range additonalValues {
|
||||
for _, value := range additionalValues {
|
||||
valfile, err := filepath.Abs(value)
|
||||
if err != nil {
|
||||
errQueue <- err
|
||||
@@ -214,7 +215,7 @@ func (state *HelmState) SyncReleases(helm helmexec.Interface, additonalValues []
|
||||
return nil
|
||||
}
|
||||
|
||||
func (state *HelmState) DiffReleases(helm helmexec.Interface, additonalValues []string) []error {
|
||||
func (state *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string) []error {
|
||||
var wg sync.WaitGroup
|
||||
errs := []error{}
|
||||
|
||||
@@ -229,7 +230,7 @@ func (state *HelmState) DiffReleases(helm helmexec.Interface, additonalValues []
|
||||
errs = append(errs, flagsErr)
|
||||
}
|
||||
|
||||
for _, value := range additonalValues {
|
||||
for _, value := range additionalValues {
|
||||
valfile, err := filepath.Abs(value)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
@@ -295,6 +296,41 @@ func (state *HelmState) Clean() []error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilterReleases allows for the execution of helm commands against a subset of the releases in the helmfile.
|
||||
func (state *HelmState) FilterReleases(labels []string) error {
|
||||
var filteredReleases []ReleaseSpec
|
||||
releaseSet := map[string]ReleaseSpec{}
|
||||
filters := []ReleaseFilter{}
|
||||
for _, label := range labels {
|
||||
f, err := ParseLabels(label)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filters = append(filters, f)
|
||||
}
|
||||
for _, r := range state.Releases {
|
||||
if r.Labels == nil {
|
||||
r.Labels = map[string]string{}
|
||||
}
|
||||
// Let the release name be used as a tag
|
||||
r.Labels["name"] = r.Name
|
||||
for _, f := range filters {
|
||||
if r.Labels == nil {
|
||||
r.Labels = map[string]string{}
|
||||
}
|
||||
if f.Match(r) {
|
||||
releaseSet[r.Name] = r
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, r := range releaseSet {
|
||||
filteredReleases = append(filteredReleases, r)
|
||||
}
|
||||
state.Releases = filteredReleases
|
||||
return nil
|
||||
}
|
||||
|
||||
// normalizeChart allows for the distinction between a file path reference and repository references.
|
||||
// - Any single (or double character) followed by a `/` will be considered a local file reference and
|
||||
// be constructed relative to the `base path`.
|
||||
|
||||
@@ -62,6 +62,73 @@ releases:
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFromYaml_FilterReleasesOnLabels(t *testing.T) {
|
||||
yamlFile := "example/path/to/yaml/file"
|
||||
yamlContent := []byte(`releases:
|
||||
- name: myrelease1
|
||||
chart: mychart1
|
||||
labels:
|
||||
tier: frontend
|
||||
foo: bar
|
||||
- name: myrelease2
|
||||
chart: mychart2
|
||||
labels:
|
||||
tier: frontend
|
||||
- name: myrelease3
|
||||
chart: mychart3
|
||||
labels:
|
||||
tier: backend
|
||||
`)
|
||||
cases := []struct {
|
||||
filter LabelFilter
|
||||
results []bool
|
||||
}{
|
||||
{LabelFilter{positiveLabels: map[string]string{"tier": "frontend"}},
|
||||
[]bool{true, true, false}},
|
||||
{LabelFilter{positiveLabels: map[string]string{"tier": "frontend", "foo": "bar"}},
|
||||
[]bool{true, false, false}},
|
||||
{LabelFilter{negativeLabels: map[string]string{"tier": "frontend"}},
|
||||
[]bool{false, false, true}},
|
||||
{LabelFilter{positiveLabels: map[string]string{"tier": "frontend"}, negativeLabels: map[string]string{"foo": "bar"}},
|
||||
[]bool{false, true, false}},
|
||||
}
|
||||
state, err := readFromYaml(yamlContent, yamlFile)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
for idx, c := range cases {
|
||||
for idx2, expected := range c.results {
|
||||
if f := c.filter.Match(state.Releases[idx2]); f != expected {
|
||||
t.Errorf("[case: %d][outcome: %d] Unexpected outcome wanted %t, got %t", idx, idx2, expected, f)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelParsing(t *testing.T) {
|
||||
cases := []struct {
|
||||
labelString string
|
||||
expectedFilter LabelFilter
|
||||
errorExected bool
|
||||
}{
|
||||
{"foo=bar", LabelFilter{positiveLabels: map[string]string{"foo": "bar"}, negativeLabels: map[string]string{}}, false},
|
||||
{"foo!=bar", LabelFilter{positiveLabels: map[string]string{}, negativeLabels: map[string]string{"foo": "bar"}}, false},
|
||||
{"foo!=bar,baz=bat", LabelFilter{positiveLabels: map[string]string{"baz": "bat"}, negativeLabels: map[string]string{"foo": "bar"}}, false},
|
||||
{"foo", LabelFilter{positiveLabels: map[string]string{}, negativeLabels: map[string]string{}}, true},
|
||||
{"foo!=bar=baz", LabelFilter{positiveLabels: map[string]string{}, negativeLabels: map[string]string{}}, true},
|
||||
{"=bar", LabelFilter{positiveLabels: map[string]string{}, negativeLabels: map[string]string{}}, true},
|
||||
}
|
||||
for idx, c := range cases {
|
||||
filter, err := ParseLabels(c.labelString)
|
||||
if err != nil && !c.errorExected {
|
||||
t.Errorf("[%d] Didn't expect an error parsing labels: %s", idx, err)
|
||||
} else if err == nil && c.errorExected {
|
||||
t.Errorf("[%d] Expected %s to result in an error but got none", idx, c.labelString)
|
||||
} else if !reflect.DeepEqual(filter, c.expectedFilter) {
|
||||
t.Errorf("[%d] parsed label did not result in expected filter: %v", idx, filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
func TestHelmState_applyDefaultsTo(t *testing.T) {
|
||||
type fields struct {
|
||||
BaseChartPath string
|
||||
|
||||
Reference in New Issue
Block a user