feat: add labels for helm release (#1046)

feat: add labels for k8s resources

Signed-off-by: yxxhero <aiopsclub@163.com>
This commit is contained in:
yxxhero
2025-03-30 19:24:41 -04:00
committed by GitHub
parent 7f44408541
commit e4273d050e
18 changed files with 251 additions and 80 deletions
+7 -1
View File
@@ -1106,13 +1106,17 @@ func (a *App) visitStatesWithSelectorsAndRemoteSupport(fileOrDir string, converg
}
}
// pre-overrides HelmState
// pre-handles HelmState
fHelmStatsWithOverrides := func(st *state.HelmState) (bool, []error) {
var err error
// override release settings
st.Releases, err = st.GetReleasesWithOverrides()
if err != nil {
return false, []error{err}
}
// override release labels
st.Releases = st.GetReleasesWithLabels()
return f(st)
}
@@ -1502,6 +1506,7 @@ Do you really want to apply?
SyncArgs: c.SyncArgs(),
HideNotes: c.HideNotes(),
TakeOwnership: c.TakeOwnership(),
SyncReleaseLabels: c.SyncReleaseLabels(),
}
return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), syncOpts)
}))
@@ -1900,6 +1905,7 @@ Do you really want to sync?
HideNotes: c.HideNotes(),
TakeOwnership: c.TakeOwnership(),
SkipSchemaValidation: c.SkipSchemaValidation(),
SyncReleaseLabels: c.SyncReleaseLabels(),
}
return subst.SyncReleases(&affectedReleases, helm, c.Values(), c.Concurrency(), opts)
}))
+28 -28
View File
@@ -159,17 +159,17 @@ releases:
t.Run("default environment includes all releases", func(t *testing.T) {
check(t, testcase{
environment: "default",
expected: `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
logging kube-system true true incubator/raw
kubernetes-external-secrets kube-system true true incubator/raw
external-secrets default true true app:test incubator/raw
my-release default true true app:test incubator/raw
disabled kube-system true false incubator/raw
test2 true true incubator/raw
test3 true true incubator/raw
cache my-app true true app:test bitnami/redis 17.0.7
database my-app true true bitnami/postgres 11.6.22
global kube-system true true incubator/raw
expected: `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
logging kube-system true true chart:raw,name:logging,namespace:kube-system incubator/raw
kubernetes-external-secrets kube-system true true chart:raw,name:kubernetes-external-secrets,namespace:kube-system incubator/raw
external-secrets default true true app:test,chart:raw,name:external-secrets,namespace:default incubator/raw
my-release default true true app:test,chart:raw,name:my-release,namespace:default incubator/raw
disabled kube-system true false chart:raw,name:disabled,namespace:kube-system incubator/raw
test2 true true chart:raw,name:test2,namespace: incubator/raw
test3 true true chart:raw,name:test3,namespace: incubator/raw
cache my-app true true app:test,chart:redis,name:cache,namespace:my-app bitnami/redis 17.0.7
database my-app true true chart:postgres,name:database,namespace:my-app bitnami/postgres 11.6.22
global kube-system true true chart:raw,name:global,namespace:kube-system incubator/raw
`,
}, cfg)
})
@@ -185,9 +185,9 @@ global kube-system true true incubator/raw
check(t, testcase{
environment: "development",
selectors: []string{"app=test"},
expected: `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
external-secrets default true true app:test incubator/raw
my-release default true true app:test incubator/raw
expected: `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
external-secrets default true true app:test,chart:raw,name:external-secrets,namespace:default incubator/raw
my-release default true true app:test,chart:raw,name:my-release,namespace:default incubator/raw
`,
}, cfg)
})
@@ -195,9 +195,9 @@ my-release default true true app:test incubator/raw
t.Run("filters releases for environment used in one file only", func(t *testing.T) {
check(t, testcase{
environment: "test",
expected: `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
cache my-app true true app:test bitnami/redis 17.0.7
database my-app true true bitnami/postgres 11.6.22
expected: `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
cache my-app true true app:test,chart:redis,name:cache,namespace:my-app bitnami/redis 17.0.7
database my-app true true chart:postgres,name:database,namespace:my-app bitnami/postgres 11.6.22
`,
}, cfg)
})
@@ -206,16 +206,16 @@ database my-app true true bitnami/postgres 11.6.22
check(t, testcase{
environment: "shared",
// 'global' release has no environments, so is still excluded
expected: `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
logging kube-system true true incubator/raw
kubernetes-external-secrets kube-system true true incubator/raw
external-secrets default true true app:test incubator/raw
my-release default true true app:test incubator/raw
disabled kube-system true false incubator/raw
test2 true true incubator/raw
test3 true true incubator/raw
cache my-app true true app:test bitnami/redis 17.0.7
database my-app true true bitnami/postgres 11.6.22
expected: `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
logging kube-system true true chart:raw,name:logging,namespace:kube-system incubator/raw
kubernetes-external-secrets kube-system true true chart:raw,name:kubernetes-external-secrets,namespace:kube-system incubator/raw
external-secrets default true true app:test,chart:raw,name:external-secrets,namespace:default incubator/raw
my-release default true true app:test,chart:raw,name:my-release,namespace:default incubator/raw
disabled kube-system true false chart:raw,name:disabled,namespace:kube-system incubator/raw
test2 true true chart:raw,name:test2,namespace: incubator/raw
test3 true true chart:raw,name:test3,namespace: incubator/raw
cache my-app true true app:test,chart:redis,name:cache,namespace:my-app bitnami/redis 17.0.7
database my-app true true chart:postgres,name:database,namespace:my-app bitnami/postgres 11.6.22
`,
}, cfg)
})
@@ -285,7 +285,7 @@ releases:
})
assert.NoError(t, err)
expected := `[{"name":"myrelease1","namespace":"testNamespace","enabled":true,"installed":false,"labels":"id:myrelease1","chart":"mychart1","version":""},{"name":"myrelease2","namespace":"testNamespace","enabled":false,"installed":true,"labels":"","chart":"mychart1","version":""},{"name":"myrelease3","namespace":"testNamespace","enabled":true,"installed":true,"labels":"","chart":"mychart1","version":""},{"name":"myrelease4","namespace":"testNamespace","enabled":true,"installed":true,"labels":"id:myrelease1","chart":"mychart1","version":""}]
expected := `[{"name":"myrelease1","namespace":"testNamespace","enabled":true,"installed":false,"labels":"chart:mychart1,id:myrelease1,name:myrelease1,namespace:testNamespace","chart":"mychart1","version":""},{"name":"myrelease2","namespace":"testNamespace","enabled":false,"installed":true,"labels":"chart:mychart1,name:myrelease2,namespace:testNamespace","chart":"mychart1","version":""},{"name":"myrelease3","namespace":"testNamespace","enabled":true,"installed":true,"labels":"chart:mychart1,name:myrelease3,namespace:testNamespace","chart":"mychart1","version":""},{"name":"myrelease4","namespace":"testNamespace","enabled":true,"installed":true,"labels":"chart:mychart1,id:myrelease1,name:myrelease4,namespace:testNamespace","chart":"mychart1","version":""}]
`
assert.Equal(t, expected, out)
}
+10 -5
View File
@@ -2259,6 +2259,7 @@ type applyConfig struct {
showOnly []string
hideNotes bool
takeOwnership bool
syncReleaseLabels bool
// template-only options
includeCRDs, skipTests bool
@@ -2453,6 +2454,10 @@ func (a applyConfig) TakeOwnership() bool {
return a.takeOwnership
}
func (a applyConfig) SyncReleaseLabels() bool {
return a.syncReleaseLabels
}
type depsConfig struct {
skipRepos bool
includeTransitiveNeeds bool
@@ -3977,11 +3982,11 @@ releases:
})
assert.NoError(t, err)
expected := `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
myrelease1 testNamespace true false common:label,id:myrelease1 mychart1
myrelease2 testNamespace false true common:label mychart1
myrelease3 testNamespace true true mychart1
myrelease4 testNamespace true true id:myrelease1 mychart1
expected := `NAME NAMESPACE ENABLED INSTALLED LABELS CHART VERSION
myrelease1 testNamespace true false chart:mychart1,common:label,id:myrelease1,name:myrelease1,namespace:testNamespace mychart1
myrelease2 testNamespace false true chart:mychart1,common:label,name:myrelease2,namespace:testNamespace mychart1
myrelease3 testNamespace true true chart:mychart1,name:myrelease3,namespace:testNamespace mychart1
myrelease4 testNamespace true true chart:mychart1,id:myrelease1,name:myrelease4,namespace:testNamespace mychart1
`
assert.Equal(t, expected, out)
+5
View File
@@ -80,6 +80,8 @@ type ApplyConfigProvider interface {
DiffArgs() string
SyncArgs() string
SyncReleaseLabels() bool
DAGConfig
concurrencyConfig
@@ -112,6 +114,9 @@ type SyncConfigProvider interface {
SkipNeeds() bool
IncludeNeeds() bool
IncludeTransitiveNeeds() bool
SyncReleaseLabels() bool
DAGConfig
concurrencyConfig
+6
View File
@@ -71,6 +71,8 @@ type ApplyOptions struct {
// TakeOwnership is true if the ownership should be taken
TakeOwnership bool
SyncReleaseLabels bool
}
// NewApply creates a new Apply
@@ -266,3 +268,7 @@ func (a *ApplyImpl) HideNotes() bool {
func (a *ApplyImpl) TakeOwnership() bool {
return a.ApplyOptions.TakeOwnership
}
func (a *ApplyImpl) SyncReleaseLabels() bool {
return a.ApplyOptions.SyncReleaseLabels
}
+6
View File
@@ -42,6 +42,8 @@ type SyncOptions struct {
HideNotes bool
// TakeOwnership is the take ownership flag
TakeOwnership bool
// SyncReleaseLabels is the sync release labels flag
SyncReleaseLabels bool
}
// NewSyncOptions creates a new Apply
@@ -167,3 +169,7 @@ func (t *SyncImpl) HideNotes() bool {
func (t *SyncImpl) TakeOwnership() bool {
return t.SyncOptions.TakeOwnership
}
func (t *SyncImpl) SyncReleaseLabels() bool {
return t.SyncOptions.SyncReleaseLabels
}
+37
View File
@@ -4,9 +4,13 @@ import (
"fmt"
"os"
"path/filepath"
"slices"
"sort"
"strconv"
"strings"
"github.com/helmfile/chartify"
"helm.sh/helm/v3/pkg/storage/driver"
"github.com/helmfile/helmfile/pkg/helmexec"
"github.com/helmfile/helmfile/pkg/remote"
@@ -26,6 +30,39 @@ func (st *HelmState) appendHelmXFlags(flags []string, release *ReleaseSpec) []st
return flags
}
func formatLabels(labels map[string]string) string {
var labelsList, keys []string
for k := range labels {
if k == "" || slices.Contains(driver.GetSystemLabels(), k) {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
if len(keys) == 0 {
return ""
}
for _, k := range keys {
val := labels[k]
labelsList = append(labelsList, fmt.Sprintf("%s=%s", k, val))
}
return strings.Join(labelsList, ",")
}
// append labels flags to helm flags, starting from helm v3.13.0
func (st *HelmState) appendLabelsFlags(flags []string, helm helmexec.Interface, release *ReleaseSpec, syncReleaseLabels bool) []string {
if helm.IsVersionAtLeast("3.13.0") && (syncReleaseLabels || release.SyncReleaseLabels) {
labels := formatLabels(release.Labels)
if labels != "" {
flags = append(flags, "--labels", labels)
}
}
return flags
}
// append post-renderer flags to helm flags
func (st *HelmState) appendPostRenderFlags(flags []string, release *ReleaseSpec, postRenderer string) []string {
switch {
+51
View File
@@ -431,3 +431,54 @@ func TestAppendTakeOwnershipFlags(t *testing.T) {
})
}
}
func TestFormatLabels(t *testing.T) {
tests := []struct {
name string
labels map[string]string
want string
}{
{
name: "empty labels",
labels: map[string]string{},
want: "",
},
{
name: "single label",
labels: map[string]string{"foo": "bar"},
want: "foo=bar",
},
{
name: "multiple labels",
labels: map[string]string{"foo": "bar", "baz": "qux"},
want: "baz=qux,foo=bar",
},
{
name: "multiple labels with empty value",
labels: map[string]string{"foo": "bar", "baz": "qux", "quux": ""},
want: "baz=qux,foo=bar,quux=",
},
{
name: "multiple labels with empty key",
labels: map[string]string{"foo": "bar", "baz": "qux", "": "quux"},
want: "baz=qux,foo=bar",
},
{
name: "empty label value",
labels: map[string]string{"foo": ""},
want: "foo=",
},
{
name: "empty label key",
labels: map[string]string{"": "bar"},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := formatLabels(tt.labels)
require.Equal(t, tt.want, got, "formatLabels() = %v, want %v", got, tt.want)
})
}
}
+8 -8
View File
@@ -108,14 +108,14 @@ func TestSelectReleasesWithOverrides(t *testing.T) {
}.MustLoadState(t, "/helmfile.yaml", "default")
for _, tc := range testcases {
var err error
state.Selectors = tc.selector
releases, err := state.GetReleasesWithOverrides()
state.Releases = releases
state.Releases, err = state.GetReleasesWithOverrides()
if err != nil {
t.Fatalf("%s %s: %v", tc.selector, tc.subject, err)
}
state.Releases = state.GetReleasesWithLabels()
rs, err := state.GetSelectedReleases(false)
if err != nil {
@@ -144,13 +144,13 @@ func TestSelectReleasesWithOverridesWithIncludedTransitives(t *testing.T) {
testcases := []testcase{
{
subject: "include transitives",
subject: "include transitives is false",
selector: []string{"name=serviceA"},
want: []string{"serviceA"},
includeTransitiveNeeds: false,
},
{
subject: "include transitives",
subject: "include transitives is true",
selector: []string{"name=serviceA"},
want: []string{"serviceA", "serviceB", "serviceC"},
includeTransitiveNeeds: true,
@@ -184,13 +184,13 @@ func TestSelectReleasesWithOverridesWithIncludedTransitives(t *testing.T) {
}.MustLoadState(t, "/helmfile.yaml", "default")
for _, tc := range testcases {
var err error
state.Selectors = tc.selector
releases, err := state.GetReleasesWithOverrides()
state.Releases, err = state.GetReleasesWithOverrides()
if err != nil {
t.Fatalf("%s %s: %v", tc.selector, tc.subject, err)
}
state.Releases = releases
state.Releases = state.GetReleasesWithLabels()
rs, err := state.GetSelectedReleases(tc.includeTransitiveNeeds)
if err != nil {
+40 -29
View File
@@ -7,7 +7,6 @@ import (
"errors"
"fmt"
"io"
"maps"
"net/url"
"os"
"path/filepath"
@@ -208,6 +207,8 @@ type HelmSpec struct {
DeleteWait bool `yaml:"deleteWait"`
// Timeout is the time in seconds to wait for helmfile delete command (default 300)
DeleteTimeout int `yaml:"deleteTimeout"`
// SyncReleaseLabels is true if the release labels should be synced with the helmfile labels
SyncReleaseLabels bool `yaml:"syncReleaseLabels"`
}
// RepositorySpec that defines values for a helm repo
@@ -408,6 +409,8 @@ type ReleaseSpec struct {
DeleteWait *bool `yaml:"deleteWait,omitempty"`
// Timeout is the time in seconds to wait for helmfile delete command (default 300)
DeleteTimeout *int `yaml:"deleteTimeout,omitempty"`
// SyncReleaseLabels is true if the release labels should be synced with the helmfile labels
SyncReleaseLabels bool `yaml:"syncReleaseLabels"`
}
func (r *Inherits) UnmarshalYAML(unmarshal func(any) error) error {
@@ -783,6 +786,7 @@ type SyncOpts struct {
Wait bool
WaitRetries int
WaitForJobs bool
SyncReleaseLabels bool
ReuseValues bool
ResetValues bool
PostRenderer string
@@ -2262,16 +2266,40 @@ func (st *HelmState) GetReleasesWithOverrides() ([]ReleaseSpec, error) {
return rs, nil
}
func (st *HelmState) GetReleasesWithLabels() []ReleaseSpec {
var rs []ReleaseSpec
for _, r := range st.Releases {
spec := r
labels := map[string]string{}
// apply common labels
for k, v := range st.CommonLabels {
labels[k] = v
}
for k, v := range spec.Labels {
labels[k] = v
}
// Let the release name, namespace, and chart be used as a tag
labels["name"] = r.Name
labels["namespace"] = r.Namespace
// Strip off just the last portion for the name stable/newrelic would give newrelic
chartSplit := strings.Split(r.Chart, "/")
labels["chart"] = chartSplit[len(chartSplit)-1]
spec.Labels = labels
rs = append(rs, spec)
}
return rs
}
func (st *HelmState) SelectReleases(includeTransitiveNeeds bool) ([]Release, error) {
values := st.Values()
rs, err := markExcludedReleases(st.Releases, st.Selectors, st.CommonLabels, values, includeTransitiveNeeds)
rs, err := markExcludedReleases(st.Releases, st.Selectors, values, includeTransitiveNeeds)
if err != nil {
return nil, err
}
return rs, nil
}
func markExcludedReleases(releases []ReleaseSpec, selectors []string, commonLabels map[string]string, values map[string]any, includeTransitiveNeeds bool) ([]Release, error) {
func markExcludedReleases(releases []ReleaseSpec, selectors []string, values map[string]any, includeTransitiveNeeds bool) ([]Release, error) {
var filteredReleases []Release
filters := []ReleaseFilter{}
for _, label := range selectors {
@@ -2282,29 +2310,8 @@ func markExcludedReleases(releases []ReleaseSpec, selectors []string, commonLabe
filters = append(filters, f)
}
for _, r := range releases {
orginReleaseLabel := maps.Clone(r.Labels)
if r.Labels == nil {
r.Labels = map[string]string{}
} else {
// Make a copy of the labels to avoid mutating the original
r.Labels = maps.Clone(r.Labels)
}
// Let the release name, namespace, and chart be used as a tag
r.Labels["name"] = r.Name
r.Labels["namespace"] = r.Namespace
// Strip off just the last portion for the name stable/newrelic would give newrelic
chartSplit := strings.Split(r.Chart, "/")
r.Labels["chart"] = chartSplit[len(chartSplit)-1]
// Merge CommonLabels into release labels
for k, v := range commonLabels {
r.Labels[k] = v
}
var filterMatch bool
for _, f := range filters {
if r.Labels == nil {
r.Labels = map[string]string{}
}
if f.Match(r) {
filterMatch = true
break
@@ -2316,7 +2323,6 @@ func markExcludedReleases(releases []ReleaseSpec, selectors []string, commonLabe
return nil, fmt.Errorf("failed to parse condition in release %s: %w", r.Name, err)
}
// reset the labels to the original
r.Labels = orginReleaseLabel
res := Release{
ReleaseSpec: r,
Filtered: (len(filters) > 0 && !filterMatch) || (!conditionMatch),
@@ -2776,15 +2782,20 @@ func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSp
flags = append(flags, "--disable-openapi-validation")
}
postRenderer := ""
syncReleaseLabels := false
if opt != nil {
postRenderer = opt.PostRenderer
syncReleaseLabels = opt.SyncReleaseLabels
}
flags = st.appendConnectionFlags(flags, release)
flags = st.appendChartDownloadFlags(flags, release)
flags = st.appendHelmXFlags(flags, release)
postRenderer := ""
if opt != nil {
postRenderer = opt.PostRenderer
}
flags = st.appendLabelsFlags(flags, helm, release, syncReleaseLabels)
flags = st.appendPostRenderFlags(flags, release, postRenderer)
var postRendererArgs []string
+6 -6
View File
@@ -38,39 +38,39 @@ func TestGenerateID(t *testing.T) {
run(testcase{
subject: "baseline",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
want: "foo-values-669d45cd7b",
want: "foo-values-6584bf5db7",
})
run(testcase{
subject: "different bytes content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: []byte(`{"k":"v"}`),
want: "foo-values-67d8c67fcf",
want: "foo-values-6b8c446b76",
})
run(testcase{
subject: "different map content",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
data: map[string]any{"k": "v"},
want: "foo-values-b9bc64677",
want: "foo-values-6b499b6fb6",
})
run(testcase{
subject: "different chart",
release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"},
want: "foo-values-585c4565f5",
want: "foo-values-775cbccfb",
})
run(testcase{
subject: "different name",
release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"},
want: "bar-values-c94846459",
want: "bar-values-849dcf78b4",
})
run(testcase{
subject: "specific ns",
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"},
want: "myns-foo-values-798d69477",
want: "myns-foo-values-d57499c58",
})
for id, n := range ids {