feat: use helm status to find helm release (#1640)

* feat: use helm status to find helm release

Signed-off-by: yxxhero <aiopsclub@163.com>
This commit is contained in:
yxxhero 2024-07-30 13:40:44 +08:00 committed by GitHub
parent 8f44f12f07
commit 75ad24e6dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 346 additions and 260 deletions

View File

@ -534,6 +534,7 @@ Helmfile uses some OS environment variables to override default behaviour:
* `HELMFILE_DISABLE_INSECURE_FEATURES` - disable insecure features, expecting `true` lower case
* `HELMFILE_DISABLE_RUNNER_UNIQUE_ID` - disable unique logging ID, expecting any non-empty value
* `HELMFILE_SKIP_INSECURE_TEMPLATE_FUNCTIONS` - disable insecure template functions, expecting `true` lower case
* `HELMFILE_USE_HELM_STATUS_TO_CHECK_RELEASE_EXISTENCE` - expecting non-empty value to use `helm status` to check release existence, instead of `helm list` which is the default behaviour
* `HELMFILE_EXPERIMENTAL` - enable experimental features, expecting `true` lower case
* `HELMFILE_ENVIRONMENT` - specify [Helmfile environment](https://helmfile.readthedocs.io/en/latest/#environment), it has lower priority than CLI argument `--environment`
* `HELMFILE_TEMPDIR` - specify directory to store temporary files

View File

@ -2729,8 +2729,34 @@ func TestApply(t *testing.T) {
diffs map[exectest.DiffKey]error
upgraded []exectest.Release
deleted []exectest.Release
envs map[string]string
log string
}{
//
// helm-status-check-to-release-existence
//
{
name: "helm-status-check-to-release-existence",
loc: location(),
files: map[string]string{
"/path/to/helmfile.yaml": `
releases:
- name: bar
chart: stable/mychart2
- name: foo_notFound
chart: stable/mychart1
installed: false
`,
},
diffs: map[exectest.DiffKey]error{
{Name: "bar", Chart: "stable/mychart2", Flags: "--kube-context default --detailed-exitcode --reset-values"}: nil,
{Name: "foo_notFound", Chart: "stable/mychart1", Flags: "--kube-context default --detailed-exitcode --reset-values"}: nil,
},
lists: map[exectest.ListKey]string{},
upgraded: []exectest.Release{},
deleted: []exectest.Release{},
envs: map[string]string{"HELMFILE_USE_HELM_STATUS_TO_CHECK_RELEASE_EXISTENCE": "true"},
},
//
// complex test cases for smoke testing
//
@ -3457,73 +3483,6 @@ releases:
upgraded: []exectest.Release{},
// as we check for log output, set concurrency to 1 to avoid non-deterministic test result
concurrency: 1,
log: `processing file "helmfile.yaml" in directory "."
changing working directory to "/path/to"
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
merged environment: &{default map[] map[]}
2 release(s) matching app=test found in helmfile.yaml
err: release "default/default/external-secrets" depends on "default/kube-system/kubernetes-external-secrets" which does not match the selectors. Please add a selector like "--selector name=kubernetes-external-secrets", or indicate whether to skip (--skip-needs) or include (--include-needs) these dependencies
changing working directory back to "/path/to"
`,
},
{
// see https://github.com/roboll/helmfile/issues/919#issuecomment-549831747
@ -3561,72 +3520,6 @@ releases:
error: "err: no releases found that matches specified selector(app=test_non_existent) and environment(default), in any helmfile",
// as we check for log output, set concurrency to 1 to avoid non-deterministic test result
concurrency: 1,
log: `processing file "helmfile.yaml" in directory "."
changing working directory to "/path/to"
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
merged environment: &{default map[] map[]}
0 release(s) matching app=test_non_existent found in helmfile.yaml
changing working directory back to "/path/to"
`,
},
//
// error cases
@ -3656,45 +3549,6 @@ releases:
deleted: []exectest.Release{},
concurrency: 1,
error: `in ./helmfile.yaml: release "default//foo" depends on "default/ns1/bar" which does not match the selectors. Please add a selector like "--selector name=bar", or indicate whether to skip (--skip-needs) or include (--include-needs) these dependencies`,
log: `processing file "helmfile.yaml" in directory "."
changing working directory to "/path/to"
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: bar
3: namespace: ns1
4: chart: mychart3
5: - name: foo
6: chart: mychart1
7: needs:
8: - ns1/bar
9:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: bar
3: namespace: ns1
4: chart: mychart3
5: - name: foo
6: chart: mychart1
7: needs:
8: - ns1/bar
9:
merged environment: &{default map[] map[]}
1 release(s) matching name=foo found in helmfile.yaml
err: release "default//foo" depends on "default/ns1/bar" which does not match the selectors. Please add a selector like "--selector name=bar", or indicate whether to skip (--skip-needs) or include (--include-needs) these dependencies
changing working directory back to "/path/to"
`,
},
{
name: "non-existent release in needs",
@ -3720,46 +3574,6 @@ releases:
deleted: []exectest.Release{},
concurrency: 1,
error: "in ./helmfile.yaml: release(s) \"default//foo\" depend(s) on an undefined release \"default/ns1/bar\". Perhaps you made a typo in \"needs\" or forgot defining a release named \"bar\" with appropriate \"namespace\" and \"kubeContext\"?",
log: `processing file "helmfile.yaml" in directory "."
changing working directory to "/path/to"
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: baz
3: namespace: ns1
4: chart: mychart3
5: - name: foo
6: chart: mychart1
7: needs:
8: - ns1/bar
9:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: baz
3: namespace: ns1
4: chart: mychart3
5: - name: foo
6: chart: mychart1
7: needs:
8: - ns1/bar
9:
merged environment: &{default map[] map[]}
WARNING: release foo needs bar, but bar is not installed due to installed: false. Either mark bar as installed or remove bar from foo's needs
2 release(s) found in helmfile.yaml
err: release(s) "default//foo" depend(s) on an undefined release "default/ns1/bar". Perhaps you made a typo in "needs" or forgot defining a release named "bar" with appropriate "namespace" and "kubeContext"?
changing working directory back to "/path/to"
`,
},
{
name: "duplicate releases",
@ -3789,56 +3603,17 @@ releases:
deleted: []exectest.Release{},
concurrency: 1,
error: "in ./helmfile.yaml: found 2 duplicate releases with ID \"default//foo\"",
log: `processing file "helmfile.yaml" in directory "."
changing working directory to "/path/to"
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: bar
3: namespace: ns1
4: chart: mychart3
5: - name: foo
6: chart: mychart2
7: needs:
8: - ns1/bar
9: - name: foo
10: chart: mychart1
11: needs:
12: - ns1/bar
13:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: bar
3: namespace: ns1
4: chart: mychart3
5: - name: foo
6: chart: mychart2
7: needs:
8: - ns1/bar
9: - name: foo
10: chart: mychart1
11: needs:
12: - ns1/bar
13:
merged environment: &{default map[] map[]}
err: found 2 duplicate releases with ID "default//foo"
changing working directory back to "/path/to"
`,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
if tc.envs != nil {
for k, v := range tc.envs {
os.Setenv(k, v)
defer os.Unsetenv(k)
}
}
wantUpgrades := tc.upgraded
wantDeletes := tc.deleted

View File

@ -0,0 +1,44 @@
processing file "helmfile.yaml" in directory "."
changing working directory to "/path/to"
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: bar
3: namespace: ns1
4: chart: mychart3
5: - name: foo
6: chart: mychart2
7: needs:
8: - ns1/bar
9: - name: foo
10: chart: mychart1
11: needs:
12: - ns1/bar
13:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: bar
3: namespace: ns1
4: chart: mychart3
5: - name: foo
6: chart: mychart2
7: needs:
8: - ns1/bar
9: - name: foo
10: chart: mychart1
11: needs:
12: - ns1/bar
13:
merged environment: &{default map[] map[]}
err: found 2 duplicate releases with ID "default//foo"
changing working directory back to "/path/to"

View File

@ -0,0 +1,39 @@
processing file "helmfile.yaml" in directory "."
changing working directory to "/path/to"
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: bar
3: chart: stable/mychart2
4: - name: foo_notFound
5: chart: stable/mychart1
6: installed: false
7:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: bar
3: chart: stable/mychart2
4: - name: foo_notFound
5: chart: stable/mychart1
6: installed: false
7:
merged environment: &{default map[] map[]}
2 release(s) found in helmfile.yaml
Checking release existence using `helm status` for release foo_notFound
invoking preapply hooks for 1 groups of releases in this order:
GROUP RELEASES
1 default//bar, default//foo_notFound
invoking preapply hooks for releases in group 1/1: default//bar, default//foo_notFound
changing working directory back to "/path/to"

View File

@ -0,0 +1,39 @@
processing file "helmfile.yaml" in directory "."
changing working directory to "/path/to"
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: baz
3: namespace: ns1
4: chart: mychart3
5: - name: foo
6: chart: mychart1
7: needs:
8: - ns1/bar
9:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: baz
3: namespace: ns1
4: chart: mychart3
5: - name: foo
6: chart: mychart1
7: needs:
8: - ns1/bar
9:
merged environment: &{default map[] map[]}
WARNING: release foo needs bar, but bar is not installed due to installed: false. Either mark bar as installed or remove bar from foo's needs
2 release(s) found in helmfile.yaml
err: release(s) "default//foo" depend(s) on an undefined release "default/ns1/bar". Perhaps you made a typo in "needs" or forgot defining a release named "bar" with appropriate "namespace" and "kubeContext"?
changing working directory back to "/path/to"

View File

@ -0,0 +1,38 @@
processing file "helmfile.yaml" in directory "."
changing working directory to "/path/to"
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: bar
3: namespace: ns1
4: chart: mychart3
5: - name: foo
6: chart: mychart1
7: needs:
8: - ns1/bar
9:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1: releases:
2: - name: bar
3: namespace: ns1
4: chart: mychart3
5: - name: foo
6: chart: mychart1
7: needs:
8: - ns1/bar
9:
merged environment: &{default map[] map[]}
1 release(s) matching name=foo found in helmfile.yaml
err: release "default//foo" depends on "default/ns1/bar" which does not match the selectors. Please add a selector like "--selector name=bar", or indicate whether to skip (--skip-needs) or include (--include-needs) these dependencies
changing working directory back to "/path/to"

View File

@ -0,0 +1,65 @@
processing file "helmfile.yaml" in directory "."
changing working directory to "/path/to"
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
merged environment: &{default map[] map[]}
0 release(s) matching app=test_non_existent found in helmfile.yaml
changing working directory back to "/path/to"

View File

@ -0,0 +1,66 @@
processing file "helmfile.yaml" in directory "."
changing working directory to "/path/to"
first-pass rendering starting for "helmfile.yaml.part.0": inherited=&{default map[] map[]}, overrode=<nil>
first-pass uses: &{default map[] map[]}
first-pass rendering output of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
first-pass produced: &{default map[] map[]}
first-pass rendering result of "helmfile.yaml.part.0": {default map[] map[]}
vals:
map[]
defaultVals:[]
second-pass rendering result of "helmfile.yaml.part.0":
0:
1:
2:
3: releases:
4: - name: kubernetes-external-secrets
5: chart: incubator/raw
6: namespace: kube-system
7:
8: - name: external-secrets
9: chart: incubator/raw
10: namespace: default
11: labels:
12: app: test
13: needs:
14: - kube-system/kubernetes-external-secrets
15:
16: - name: my-release
17: chart: incubator/raw
18: namespace: default
19: labels:
20: app: test
21: needs:
22: - default/external-secrets
23:
merged environment: &{default map[] map[]}
2 release(s) matching app=test found in helmfile.yaml
err: release "default/default/external-secrets" depends on "default/kube-system/kubernetes-external-secrets" which does not match the selectors. Please add a selector like "--selector name=kubernetes-external-secrets", or indicate whether to skip (--skip-needs) or include (--include-needs) these dependencies
changing working directory back to "/path/to"

View File

@ -3,6 +3,9 @@ package envvar
const (
DisableInsecureFeatures = "HELMFILE_DISABLE_INSECURE_FEATURES"
// use helm status to check if a release exists before installing it
UseHelmStatusToCheckReleaseExistence = "HELMFILE_USE_HELM_STATUS_TO_CHECK_RELEASE_EXISTENCE"
// TODO: Remove this function once Helmfile v0.x
SkipInsecureTemplateFunctions = "HELMFILE_SKIP_INSECURE_TEMPLATE_FUNCTIONS"

View File

@ -138,6 +138,9 @@ func (helm *Helm) DiffRelease(context helmexec.HelmContext, name, chart, namespa
return err
}
func (helm *Helm) ReleaseStatus(context helmexec.HelmContext, release string, flags ...string) error {
if strings.Contains(release, "notFound") {
return errors.New("Error: release: not found")
}
if strings.Contains(release, "error") {
return errors.New("error")
}

View File

@ -29,6 +29,7 @@ import (
"github.com/helmfile/helmfile/pkg/argparser"
"github.com/helmfile/helmfile/pkg/environment"
"github.com/helmfile/helmfile/pkg/envvar"
"github.com/helmfile/helmfile/pkg/event"
"github.com/helmfile/helmfile/pkg/filesystem"
"github.com/helmfile/helmfile/pkg/helmexec"
@ -698,13 +699,25 @@ func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValu
}
func (st *HelmState) isReleaseInstalled(context helmexec.HelmContext, helm helmexec.Interface, release ReleaseSpec) (bool, error) {
if os.Getenv(envvar.UseHelmStatusToCheckReleaseExistence) != "" {
st.logger.Debugf("Checking release existence using `helm status` for release %s", release.Name)
flags := st.kubeConnectionFlags(&release)
if release.Namespace != "" {
flags = append(flags, "--namespace", release.Namespace)
}
err := helm.ReleaseStatus(context, release.Name, flags...)
if err != nil && strings.Contains(err.Error(), "Error: release: not found") {
return false, nil
}
return true, err
}
out, err := st.listReleases(context, helm, &release)
if err != nil {
return false, err
} else if out != "" {
return true, nil
}
return false, nil
return out != "", nil
}
func (st *HelmState) DetectReleasesToBeDeletedForSync(helm helmexec.Interface, releases []ReleaseSpec) ([]ReleaseSpec, error) {