Merge 9187d4ed91 into 55adae872e
This commit is contained in:
commit
a8b30c1f0f
|
|
@ -328,6 +328,9 @@ releases:
|
||||||
reuseValues: false
|
reuseValues: false
|
||||||
# set `false` to uninstall this release on sync. (default true)
|
# set `false` to uninstall this release on sync. (default true)
|
||||||
installed: true
|
installed: true
|
||||||
|
# Defines the strategy to use when updating. Possible value is:
|
||||||
|
# - "reinstallIfForbidden": Performs an uninstall before the update only if the update is forbidden (e.g., due to permission issues or conflicts).
|
||||||
|
updateStrategy: ""
|
||||||
# restores previous state in case of failed release (default false)
|
# restores previous state in case of failed release (default false)
|
||||||
atomic: true
|
atomic: true
|
||||||
# when true, cleans up any new resources created during a failed release (default false)
|
# when true, cleans up any new resources created during a failed release (default false)
|
||||||
|
|
|
||||||
|
|
@ -415,4 +415,28 @@ releases:
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("show diff on changed selected release with reinstall", func(t *testing.T) {
|
||||||
|
check(t, testcase{
|
||||||
|
helmfile: `
|
||||||
|
releases:
|
||||||
|
- name: a
|
||||||
|
chart: incubator/raw
|
||||||
|
namespace: default
|
||||||
|
updateStrategy: reinstallIfForbidden
|
||||||
|
- name: b
|
||||||
|
chart: incubator/raw
|
||||||
|
namespace: default
|
||||||
|
`,
|
||||||
|
selectors: []string{"name=a"},
|
||||||
|
lists: map[exectest.ListKey]string{
|
||||||
|
{Filter: "^a$", Flags: listFlags("default", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||||
|
foo 4 Fri Nov 1 08:40:07 2019 DEPLOYED raw-3.1.0 3.1.0 default
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
diffed: []exectest.Release{
|
||||||
|
{Name: "a", Flags: []string{"--kube-context", "default", "--namespace", "default", "--reset-values"}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,97 @@ releases:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpdateStrategyParamValidation(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
files map[string]string
|
||||||
|
updateStrategy string
|
||||||
|
isValid bool
|
||||||
|
}{
|
||||||
|
{map[string]string{
|
||||||
|
"/path/to/helmfile.yaml": `releases:
|
||||||
|
- name: zipkin
|
||||||
|
chart: stable/zipkin
|
||||||
|
updateStrategy: reinstallIfForbidden
|
||||||
|
`},
|
||||||
|
"reinstallIfForbidden",
|
||||||
|
true},
|
||||||
|
{map[string]string{
|
||||||
|
"/path/to/helmfile.yaml": `releases:
|
||||||
|
- name: zipkin
|
||||||
|
chart: stable/zipkin
|
||||||
|
updateStrategy: reinstallIfForbidden
|
||||||
|
`},
|
||||||
|
"reinstallIfForbidden",
|
||||||
|
true},
|
||||||
|
{map[string]string{
|
||||||
|
"/path/to/helmfile.yaml": `releases:
|
||||||
|
- name: zipkin
|
||||||
|
chart: stable/zipkin
|
||||||
|
updateStrategy:
|
||||||
|
`},
|
||||||
|
"",
|
||||||
|
true},
|
||||||
|
{map[string]string{
|
||||||
|
"/path/to/helmfile.yaml": `releases:
|
||||||
|
- name: zipkin
|
||||||
|
chart: stable/zipkin
|
||||||
|
updateStrategy: foo
|
||||||
|
`},
|
||||||
|
"foo",
|
||||||
|
false},
|
||||||
|
{map[string]string{
|
||||||
|
"/path/to/helmfile.yaml": `releases:
|
||||||
|
- name: zipkin
|
||||||
|
chart: stable/zipkin
|
||||||
|
updateStrategy: reinstal
|
||||||
|
`},
|
||||||
|
"reinstal",
|
||||||
|
false},
|
||||||
|
{map[string]string{
|
||||||
|
"/path/to/helmfile.yaml": `releases:
|
||||||
|
- name: zipkin
|
||||||
|
chart: stable/zipkin
|
||||||
|
updateStrategy: reinstall1
|
||||||
|
`},
|
||||||
|
"reinstall1",
|
||||||
|
false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, c := range cases {
|
||||||
|
fs := testhelper.NewTestFs(c.files)
|
||||||
|
app := &App{
|
||||||
|
OverrideHelmBinary: DefaultHelmBinary,
|
||||||
|
OverrideKubeContext: "default",
|
||||||
|
Logger: newAppTestLogger(),
|
||||||
|
Namespace: "",
|
||||||
|
Env: "default",
|
||||||
|
FileOrDir: "helmfile.yaml",
|
||||||
|
}
|
||||||
|
|
||||||
|
expectNoCallsToHelm(app)
|
||||||
|
|
||||||
|
app = injectFs(app, fs)
|
||||||
|
|
||||||
|
err := app.ForEachState(
|
||||||
|
Noop,
|
||||||
|
false,
|
||||||
|
SetFilter(true),
|
||||||
|
)
|
||||||
|
|
||||||
|
if c.isValid && err != nil {
|
||||||
|
t.Errorf("[case: %d] Unexpected error for valid case: %v", idx, err)
|
||||||
|
} else if !c.isValid {
|
||||||
|
var invalidUpdateStrategy state.InvalidUpdateStrategyError
|
||||||
|
invalidUpdateStrategy.UpdateStrategy = c.updateStrategy
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("[case: %d] Expected error for invalid case", idx)
|
||||||
|
} else if !strings.Contains(err.Error(), invalidUpdateStrategy.Error()) {
|
||||||
|
t.Errorf("[case: %d] Unexpected error returned for invalid case\ngot: %v\nexpected underlying error: %s", idx, err, invalidUpdateStrategy.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestVisitDesiredStatesWithReleasesFiltered_Issue1008_MissingNonDefaultEnvInBase(t *testing.T) {
|
func TestVisitDesiredStatesWithReleasesFiltered_Issue1008_MissingNonDefaultEnvInBase(t *testing.T) {
|
||||||
files := map[string]string{
|
files := map[string]string{
|
||||||
"/path/to/base.yaml": `
|
"/path/to/base.yaml": `
|
||||||
|
|
@ -3076,6 +3167,97 @@ baz 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart3-3.1.0 3.1.0 defau
|
||||||
concurrency: 1,
|
concurrency: 1,
|
||||||
},
|
},
|
||||||
//
|
//
|
||||||
|
// install with upgrade with reinstallIfForbidden
|
||||||
|
//
|
||||||
|
{
|
||||||
|
name: "install-with-upgrade-with-reinstallIfForbidden",
|
||||||
|
loc: location(),
|
||||||
|
files: map[string]string{
|
||||||
|
"/path/to/helmfile.yaml": `
|
||||||
|
releases:
|
||||||
|
- name: baz
|
||||||
|
chart: stable/mychart3
|
||||||
|
disableValidationOnInstall: true
|
||||||
|
updateStrategy: reinstallIfForbidden
|
||||||
|
- name: foo
|
||||||
|
chart: stable/mychart1
|
||||||
|
disableValidationOnInstall: true
|
||||||
|
needs:
|
||||||
|
- bar
|
||||||
|
- name: bar
|
||||||
|
chart: stable/mychart2
|
||||||
|
disableValidation: true
|
||||||
|
updateStrategy: reinstallIfForbidden
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
diffs: map[exectest.DiffKey]error{
|
||||||
|
{Name: "baz", Chart: "stable/mychart3", Flags: "--kube-context default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2},
|
||||||
|
{Name: "foo", Chart: "stable/mychart1", Flags: "--disable-validation --kube-context default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2},
|
||||||
|
{Name: "bar", Chart: "stable/mychart2", Flags: "--disable-validation --kube-context default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2},
|
||||||
|
},
|
||||||
|
lists: map[exectest.ListKey]string{
|
||||||
|
{Filter: "^foo$", Flags: listFlags("", "default")}: ``,
|
||||||
|
{Filter: "^bar$", Flags: listFlags("", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||||
|
bar 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart2-3.1.0 3.1.0 default
|
||||||
|
`,
|
||||||
|
{Filter: "^baz$", Flags: listFlags("", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||||
|
baz 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart3-3.1.0 3.1.0 default
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
upgraded: []exectest.Release{
|
||||||
|
{Name: "baz", Flags: []string{"--kube-context", "default"}},
|
||||||
|
{Name: "bar", Flags: []string{"--kube-context", "default"}},
|
||||||
|
{Name: "foo", Flags: []string{"--kube-context", "default"}},
|
||||||
|
},
|
||||||
|
deleted: []exectest.Release{},
|
||||||
|
concurrency: 1,
|
||||||
|
},
|
||||||
|
//
|
||||||
|
// install with upgrade and --skip-diff-on-install with reinstallIfForbidden
|
||||||
|
//
|
||||||
|
{
|
||||||
|
name: "install-with-upgrade-with-skip-diff-on-install-with-reinstallIfForbidden",
|
||||||
|
loc: location(),
|
||||||
|
skipDiffOnInstall: true,
|
||||||
|
files: map[string]string{
|
||||||
|
"/path/to/helmfile.yaml": `
|
||||||
|
releases:
|
||||||
|
- name: baz
|
||||||
|
chart: stable/mychart3
|
||||||
|
disableValidationOnInstall: true
|
||||||
|
updateStrategy: reinstallIfForbidden
|
||||||
|
- name: foo
|
||||||
|
chart: stable/mychart1
|
||||||
|
disableValidationOnInstall: true
|
||||||
|
needs:
|
||||||
|
- bar
|
||||||
|
- name: bar
|
||||||
|
chart: stable/mychart2
|
||||||
|
disableValidation: true
|
||||||
|
updateStrategy: reinstallIfForbidden
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
diffs: map[exectest.DiffKey]error{
|
||||||
|
{Name: "baz", Chart: "stable/mychart3", Flags: "--kube-context default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2},
|
||||||
|
{Name: "bar", Chart: "stable/mychart2", Flags: "--disable-validation --kube-context default --reset-values --detailed-exitcode"}: helmexec.ExitError{Code: 2},
|
||||||
|
},
|
||||||
|
lists: map[exectest.ListKey]string{
|
||||||
|
{Filter: "^foo$", Flags: listFlags("", "default")}: ``,
|
||||||
|
{Filter: "^bar$", Flags: listFlags("", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||||
|
bar 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart2-3.1.0 3.1.0 default
|
||||||
|
`,
|
||||||
|
{Filter: "^baz$", Flags: listFlags("", "default")}: `NAME REVISION UPDATED STATUS CHART APP VERSION NAMESPACE
|
||||||
|
baz 4 Fri Nov 1 08:40:07 2019 DEPLOYED mychart3-3.1.0 3.1.0 default
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
upgraded: []exectest.Release{
|
||||||
|
{Name: "baz", Flags: []string{"--kube-context", "default"}},
|
||||||
|
{Name: "bar", Flags: []string{"--kube-context", "default"}},
|
||||||
|
{Name: "foo", Flags: []string{"--kube-context", "default"}},
|
||||||
|
},
|
||||||
|
concurrency: 1,
|
||||||
|
},
|
||||||
|
//
|
||||||
// upgrades
|
// upgrades
|
||||||
//
|
//
|
||||||
{
|
{
|
||||||
|
|
@ -3772,7 +3954,7 @@ releases:
|
||||||
}
|
}
|
||||||
for flagIdx := range wantDeletes[relIdx].Flags {
|
for flagIdx := range wantDeletes[relIdx].Flags {
|
||||||
if wantDeletes[relIdx].Flags[flagIdx] != helm.Deleted[relIdx].Flags[flagIdx] {
|
if wantDeletes[relIdx].Flags[flagIdx] != helm.Deleted[relIdx].Flags[flagIdx] {
|
||||||
t.Errorf("releaes[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Deleted[relIdx].Flags[flagIdx], wantDeletes[relIdx].Flags[flagIdx])
|
t.Errorf("releases[%d].flags[%d]: got %v, want %v", relIdx, flagIdx, helm.Deleted[relIdx].Flags[flagIdx], wantDeletes[relIdx].Flags[flagIdx])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"dario.cat/mergo"
|
"dario.cat/mergo"
|
||||||
"github.com/helmfile/vals"
|
"github.com/helmfile/vals"
|
||||||
|
|
@ -285,6 +286,18 @@ func (ld *desiredStateLoader) load(env, overrodeEnv *environment.Environment, ba
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate updateStrategy value if set in the releases
|
||||||
|
for i := range finalState.Releases {
|
||||||
|
if finalState.Releases[i].UpdateStrategy != "" {
|
||||||
|
if !slices.Contains(state.ValidUpdateStrategyValues, finalState.Releases[i].UpdateStrategy) {
|
||||||
|
return nil, &state.StateLoadError{
|
||||||
|
Msg: fmt.Sprintf("failed to read %s", finalState.FilePath),
|
||||||
|
Cause: &state.InvalidUpdateStrategyError{UpdateStrategy: finalState.Releases[i].UpdateStrategy},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
finalState.OrginReleases = finalState.Releases
|
finalState.OrginReleases = finalState.Releases
|
||||||
return finalState, nil
|
return finalState, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
pkg/app/testdata/app_diff_test/show_diff_on_changed_selected_release_with_reinstall
vendored
Normal file
11
pkg/app/testdata/app_diff_test/show_diff_on_changed_selected_release_with_reinstall
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
processing file "helmfile.yaml" in directory "."
|
||||||
|
changing working directory to "/path/to"
|
||||||
|
merged environment: &{default map[] map[]}
|
||||||
|
1 release(s) matching name=a found in helmfile.yaml
|
||||||
|
|
||||||
|
processing 1 groups of releases in this order:
|
||||||
|
GROUP RELEASES
|
||||||
|
1 default/default/a
|
||||||
|
|
||||||
|
processing releases in group 1/1: default/default/a
|
||||||
|
changing working directory back to "/path/to"
|
||||||
35
pkg/app/testdata/testapply/install-with-upgrade-with-reinstallifforbidden/log
vendored
Normal file
35
pkg/app/testdata/testapply/install-with-upgrade-with-reinstallifforbidden/log
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
processing file "helmfile.yaml" in directory "."
|
||||||
|
changing working directory to "/path/to"
|
||||||
|
merged environment: &{default map[] map[]}
|
||||||
|
3 release(s) found in helmfile.yaml
|
||||||
|
|
||||||
|
Affected releases are:
|
||||||
|
bar (stable/mychart2) UPDATED
|
||||||
|
baz (stable/mychart3) UPDATED
|
||||||
|
foo (stable/mychart1) UPDATED
|
||||||
|
|
||||||
|
invoking preapply hooks for 2 groups of releases in this order:
|
||||||
|
GROUP RELEASES
|
||||||
|
1 default//foo
|
||||||
|
2 default//baz, default//bar
|
||||||
|
|
||||||
|
invoking preapply hooks for releases in group 1/2: default//foo
|
||||||
|
invoking preapply hooks for releases in group 2/2: default//baz, default//bar
|
||||||
|
processing 2 groups of releases in this order:
|
||||||
|
GROUP RELEASES
|
||||||
|
1 default//baz, default//bar
|
||||||
|
2 default//foo
|
||||||
|
|
||||||
|
processing releases in group 1/2: default//baz, default//bar
|
||||||
|
update strategy - sync success
|
||||||
|
update strategy - sync success
|
||||||
|
processing releases in group 2/2: default//foo
|
||||||
|
getting deployed release version failed: Failed to get the version for: mychart1
|
||||||
|
|
||||||
|
UPDATED RELEASES:
|
||||||
|
NAME NAMESPACE CHART VERSION DURATION
|
||||||
|
baz stable/mychart3 3.1.0 0s
|
||||||
|
bar stable/mychart2 3.1.0 0s
|
||||||
|
foo stable/mychart1 0s
|
||||||
|
|
||||||
|
changing working directory back to "/path/to"
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
processing file "helmfile.yaml" in directory "."
|
||||||
|
changing working directory to "/path/to"
|
||||||
|
merged environment: &{default map[] map[]}
|
||||||
|
3 release(s) found in helmfile.yaml
|
||||||
|
|
||||||
|
Affected releases are:
|
||||||
|
bar (stable/mychart2) UPDATED
|
||||||
|
baz (stable/mychart3) UPDATED
|
||||||
|
foo (stable/mychart1) UPDATED
|
||||||
|
|
||||||
|
invoking preapply hooks for 2 groups of releases in this order:
|
||||||
|
GROUP RELEASES
|
||||||
|
1 default//foo
|
||||||
|
2 default//baz, default//bar
|
||||||
|
|
||||||
|
invoking preapply hooks for releases in group 1/2: default//foo
|
||||||
|
invoking preapply hooks for releases in group 2/2: default//baz, default//bar
|
||||||
|
processing 2 groups of releases in this order:
|
||||||
|
GROUP RELEASES
|
||||||
|
1 default//baz, default//bar
|
||||||
|
2 default//foo
|
||||||
|
|
||||||
|
processing releases in group 1/2: default//baz, default//bar
|
||||||
|
update strategy - sync success
|
||||||
|
update strategy - sync success
|
||||||
|
processing releases in group 2/2: default//foo
|
||||||
|
getting deployed release version failed: Failed to get the version for: mychart1
|
||||||
|
|
||||||
|
UPDATED RELEASES:
|
||||||
|
NAME NAMESPACE CHART VERSION DURATION
|
||||||
|
baz stable/mychart3 3.1.0 0s
|
||||||
|
bar stable/mychart2 3.1.0 0s
|
||||||
|
foo stable/mychart1 0s
|
||||||
|
|
||||||
|
changing working directory back to "/path/to"
|
||||||
|
|
@ -56,9 +56,10 @@ type Release struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Affected struct {
|
type Affected struct {
|
||||||
Upgraded []*Release
|
Upgraded []*Release
|
||||||
Deleted []*Release
|
Reinstalled []*Release
|
||||||
Failed []*Release
|
Deleted []*Release
|
||||||
|
Failed []*Release
|
||||||
}
|
}
|
||||||
|
|
||||||
func (helm *Helm) UpdateDeps(chart string) error {
|
func (helm *Helm) UpdateDeps(chart string) error {
|
||||||
|
|
@ -107,7 +108,24 @@ func (helm *Helm) RegistryLogin(name, username, password, caFile, certFile, keyF
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (helm *Helm) SyncRelease(context helmexec.HelmContext, name, chart, namespace string, flags ...string) error {
|
func (helm *Helm) SyncRelease(context helmexec.HelmContext, name, chart, namespace string, flags ...string) error {
|
||||||
if strings.Contains(name, "error") {
|
if strings.Contains(name, "forbidden") {
|
||||||
|
releaseExists := false
|
||||||
|
for _, release := range helm.Releases {
|
||||||
|
if release.Name == name {
|
||||||
|
releaseExists = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
releaseDeleted := false
|
||||||
|
for _, release := range helm.Deleted {
|
||||||
|
if release.Name == name {
|
||||||
|
releaseDeleted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only fail if the release is present in the helm.Releases to simulate a forbidden update if it exists
|
||||||
|
if releaseExists && !releaseDeleted {
|
||||||
|
return fmt.Errorf("cannot patch %q with kind StatefulSet: StatefulSet.apps %q is invalid: spec: Forbidden: updates to statefulset spec for fields other than 'replicas', 'ordinals', 'template', 'updateStrategy', 'persistentVolumeClaimRetentionPolicy' and 'minReadySeconds' are forbidden", name, name)
|
||||||
|
}
|
||||||
|
} else if strings.Contains(name, "error") {
|
||||||
return errors.New("error")
|
return errors.New("error")
|
||||||
}
|
}
|
||||||
helm.sync(helm.ReleasesMutex, func() {
|
helm.sync(helm.ReleasesMutex, func() {
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@ const (
|
||||||
DefaultHCLFileExtension = ".hcl"
|
DefaultHCLFileExtension = ".hcl"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ValidUpdateStrategyValues = []string{UpdateStrategyReinstallIfForbidden}
|
||||||
|
|
||||||
type StateLoadError struct {
|
type StateLoadError struct {
|
||||||
Msg string
|
Msg string
|
||||||
Cause error
|
Cause error
|
||||||
|
|
@ -43,6 +45,14 @@ func (e *UndefinedEnvError) Error() string {
|
||||||
return fmt.Sprintf("environment \"%s\" is not defined", e.Env)
|
return fmt.Sprintf("environment \"%s\" is not defined", e.Env)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InvalidUpdateStrategyError struct {
|
||||||
|
UpdateStrategy string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *InvalidUpdateStrategyError) Error() string {
|
||||||
|
return fmt.Sprintf("updateStrategy %q is invalid, valid values are: %s or not set", e.UpdateStrategy, strings.Join(ValidUpdateStrategyValues, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
type StateCreator struct {
|
type StateCreator struct {
|
||||||
logger *zap.SugaredLogger
|
logger *zap.SugaredLogger
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,9 @@ const (
|
||||||
// This is used by an interim solution to make the urfave/cli command report to the helmfile internal about that the
|
// This is used by an interim solution to make the urfave/cli command report to the helmfile internal about that the
|
||||||
// --timeout flag is missingl
|
// --timeout flag is missingl
|
||||||
EmptyTimeout = -1
|
EmptyTimeout = -1
|
||||||
|
|
||||||
|
// Valid enum for updateStrategy values
|
||||||
|
UpdateStrategyReinstallIfForbidden = "reinstallIfForbidden"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ReleaseSetSpec is release set spec
|
// ReleaseSetSpec is release set spec
|
||||||
|
|
@ -277,6 +280,8 @@ type ReleaseSpec struct {
|
||||||
Force *bool `yaml:"force,omitempty"`
|
Force *bool `yaml:"force,omitempty"`
|
||||||
// Installed, when set to true, `delete --purge` the release
|
// Installed, when set to true, `delete --purge` the release
|
||||||
Installed *bool `yaml:"installed,omitempty"`
|
Installed *bool `yaml:"installed,omitempty"`
|
||||||
|
// UpdateStrategy, when set, indicate the strategy to use to update the release
|
||||||
|
UpdateStrategy string `yaml:"updateStrategy,omitempty"`
|
||||||
// Atomic, when set to true, restore previous state in case of a failed install/upgrade attempt
|
// Atomic, when set to true, restore previous state in case of a failed install/upgrade attempt
|
||||||
Atomic *bool `yaml:"atomic,omitempty"`
|
Atomic *bool `yaml:"atomic,omitempty"`
|
||||||
// CleanupOnFail, when set to true, the --cleanup-on-fail helm flag is passed to the upgrade command
|
// CleanupOnFail, when set to true, the --cleanup-on-fail helm flag is passed to the upgrade command
|
||||||
|
|
@ -467,6 +472,7 @@ type SetValue struct {
|
||||||
// AffectedReleases hold the list of released that where updated, deleted, or in error
|
// AffectedReleases hold the list of released that where updated, deleted, or in error
|
||||||
type AffectedReleases struct {
|
type AffectedReleases struct {
|
||||||
Upgraded []*ReleaseSpec
|
Upgraded []*ReleaseSpec
|
||||||
|
Reinstalled []*ReleaseSpec
|
||||||
Deleted []*ReleaseSpec
|
Deleted []*ReleaseSpec
|
||||||
Failed []*ReleaseSpec
|
Failed []*ReleaseSpec
|
||||||
DeleteFailed []*ReleaseSpec
|
DeleteFailed []*ReleaseSpec
|
||||||
|
|
@ -1037,20 +1043,24 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
|
||||||
}
|
}
|
||||||
m.Unlock()
|
m.Unlock()
|
||||||
}
|
}
|
||||||
} else if err := helm.SyncRelease(context, release.Name, chart, release.Namespace, flags...); err != nil {
|
} else if release.UpdateStrategy == UpdateStrategyReinstallIfForbidden {
|
||||||
m.Lock()
|
relErr = st.performSyncOrReinstallOfRelease(affectedReleases, helm, context, release, chart, m, flags...)
|
||||||
affectedReleases.Failed = append(affectedReleases.Failed, release)
|
|
||||||
m.Unlock()
|
|
||||||
relErr = newReleaseFailedError(release, err)
|
|
||||||
} else {
|
} else {
|
||||||
m.Lock()
|
if err := helm.SyncRelease(context, release.Name, chart, release.Namespace, flags...); err != nil {
|
||||||
affectedReleases.Upgraded = append(affectedReleases.Upgraded, release)
|
m.Lock()
|
||||||
m.Unlock()
|
affectedReleases.Failed = append(affectedReleases.Failed, release)
|
||||||
installedVersion, err := st.getDeployedVersion(context, helm, release)
|
m.Unlock()
|
||||||
if err != nil { // err is not really impacting so just log it
|
relErr = newReleaseFailedError(release, err)
|
||||||
st.logger.Debugf("getting deployed release version failed: %v", err)
|
|
||||||
} else {
|
} else {
|
||||||
release.installedVersion = installedVersion
|
m.Lock()
|
||||||
|
affectedReleases.Upgraded = append(affectedReleases.Upgraded, release)
|
||||||
|
m.Unlock()
|
||||||
|
installedVersion, err := st.getDeployedVersion(context, helm, release)
|
||||||
|
if err != nil { // err is not really impacting so just log it
|
||||||
|
st.logger.Debugf("getting deployed release version failed: %v", err)
|
||||||
|
} else {
|
||||||
|
release.installedVersion = installedVersion
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1096,6 +1106,77 @@ func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helme
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (st *HelmState) performSyncOrReinstallOfRelease(affectedReleases *AffectedReleases, helm helmexec.Interface, context helmexec.HelmContext, release *ReleaseSpec, chart string, m *sync.Mutex, flags ...string) *ReleaseError {
|
||||||
|
if err := helm.SyncRelease(context, release.Name, chart, release.Namespace, flags...); err != nil {
|
||||||
|
st.logger.Debugf("update strategy - sync failed: %s", err.Error())
|
||||||
|
// Only fail if a different error than forbidden updates
|
||||||
|
if !strings.Contains(err.Error(), "Forbidden: updates") {
|
||||||
|
st.logger.Debugf("update strategy - sync failed not due to Forbidden updates")
|
||||||
|
m.Lock()
|
||||||
|
affectedReleases.Failed = append(affectedReleases.Failed, release)
|
||||||
|
m.Unlock()
|
||||||
|
return newReleaseFailedError(release, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
st.logger.Debugf("update strategy - sync success")
|
||||||
|
m.Lock()
|
||||||
|
affectedReleases.Upgraded = append(affectedReleases.Upgraded, release)
|
||||||
|
m.Unlock()
|
||||||
|
installedVersion, err := st.getDeployedVersion(context, helm, release)
|
||||||
|
if err != nil { // err is not really impacting so just log it
|
||||||
|
st.logger.Debugf("update strategy - getting deployed release version failed: %v", err)
|
||||||
|
} else {
|
||||||
|
release.installedVersion = installedVersion
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
st.logger.Infof("Failed to sync due to forbidden updates, attempting to reinstall %q allowed by update strategy", release.Name)
|
||||||
|
installed, err := st.isReleaseInstalled(context, helm, *release)
|
||||||
|
if err != nil {
|
||||||
|
return newReleaseFailedError(release, err)
|
||||||
|
}
|
||||||
|
if installed {
|
||||||
|
var args []string
|
||||||
|
if release.Namespace != "" {
|
||||||
|
args = append(args, "--namespace", release.Namespace)
|
||||||
|
}
|
||||||
|
deleteWaitFlag := true
|
||||||
|
release.DeleteWait = &deleteWaitFlag
|
||||||
|
args = st.appendDeleteWaitFlags(args, release)
|
||||||
|
deletionFlags := st.appendConnectionFlags(args, release)
|
||||||
|
m.Lock()
|
||||||
|
if _, err := st.triggerReleaseEvent("preuninstall", nil, release, "sync"); err != nil {
|
||||||
|
affectedReleases.Failed = append(affectedReleases.Failed, release)
|
||||||
|
return newReleaseFailedError(release, err)
|
||||||
|
} else if err := helm.DeleteRelease(context, release.Name, deletionFlags...); err != nil {
|
||||||
|
affectedReleases.Failed = append(affectedReleases.Failed, release)
|
||||||
|
return newReleaseFailedError(release, err)
|
||||||
|
} else if _, err := st.triggerReleaseEvent("postuninstall", nil, release, "sync"); err != nil {
|
||||||
|
affectedReleases.Failed = append(affectedReleases.Failed, release)
|
||||||
|
return newReleaseFailedError(release, err)
|
||||||
|
}
|
||||||
|
m.Unlock()
|
||||||
|
}
|
||||||
|
if err := helm.SyncRelease(context, release.Name, chart, release.Namespace, flags...); err != nil {
|
||||||
|
m.Lock()
|
||||||
|
affectedReleases.Failed = append(affectedReleases.Failed, release)
|
||||||
|
m.Unlock()
|
||||||
|
return newReleaseFailedError(release, err)
|
||||||
|
} else {
|
||||||
|
m.Lock()
|
||||||
|
affectedReleases.Reinstalled = append(affectedReleases.Reinstalled, release)
|
||||||
|
m.Unlock()
|
||||||
|
installedVersion, err := st.getDeployedVersion(context, helm, release)
|
||||||
|
if err != nil { // err is not really impacting so just log it
|
||||||
|
st.logger.Debugf("update strategy - getting deployed release version failed: %v", err)
|
||||||
|
} else {
|
||||||
|
release.installedVersion = installedVersion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (st *HelmState) listReleases(context helmexec.HelmContext, helm helmexec.Interface, release *ReleaseSpec) (string, error) {
|
func (st *HelmState) listReleases(context helmexec.HelmContext, helm helmexec.Interface, release *ReleaseSpec) (string, error) {
|
||||||
flags := st.kubeConnectionFlags(release)
|
flags := st.kubeConnectionFlags(release)
|
||||||
if release.Namespace != "" {
|
if release.Namespace != "" {
|
||||||
|
|
@ -3735,6 +3816,28 @@ func (ar *AffectedReleases) DisplayAffectedReleases(logger *zap.SugaredLogger) {
|
||||||
}
|
}
|
||||||
logger.Info(tbl.String())
|
logger.Info(tbl.String())
|
||||||
}
|
}
|
||||||
|
if len(ar.Reinstalled) > 0 {
|
||||||
|
logger.Info("\nREINSTALLED RELEASES:")
|
||||||
|
tbl, _ := prettytable.NewTable(prettytable.Column{Header: "NAME"},
|
||||||
|
prettytable.Column{Header: "NAMESPACE", MinWidth: 6},
|
||||||
|
prettytable.Column{Header: "CHART", MinWidth: 6},
|
||||||
|
prettytable.Column{Header: "VERSION", MinWidth: 6},
|
||||||
|
prettytable.Column{Header: "DURATION", AlignRight: true},
|
||||||
|
)
|
||||||
|
tbl.Separator = " "
|
||||||
|
for _, release := range ar.Reinstalled {
|
||||||
|
modifiedChart, modErr := hideChartCredentials(release.Chart)
|
||||||
|
if modErr != nil {
|
||||||
|
logger.Warn("Could not modify chart credentials, %v", modErr)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := tbl.AddRow(release.Name, release.Namespace, modifiedChart, release.installedVersion, release.duration.Round(time.Second))
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("Could not add row, %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.Info(tbl.String())
|
||||||
|
}
|
||||||
if len(ar.Deleted) > 0 {
|
if len(ar.Deleted) > 0 {
|
||||||
logger.Info("\nDELETED RELEASES:")
|
logger.Info("\nDELETED RELEASES:")
|
||||||
tbl, _ := prettytable.NewTable(prettytable.Column{Header: "NAME"},
|
tbl, _ := prettytable.NewTable(prettytable.Column{Header: "NAME"},
|
||||||
|
|
|
||||||
|
|
@ -1640,6 +1640,112 @@ func TestHelmState_SyncReleasesAffectedRealeases(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHelmState_SyncReleasesAffectedReleasesWithReinstallIfForbidden(t *testing.T) {
|
||||||
|
no := false
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
releases []ReleaseSpec
|
||||||
|
installed []bool
|
||||||
|
wantAffected exectest.Affected
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "2 new",
|
||||||
|
releases: []ReleaseSpec{
|
||||||
|
{
|
||||||
|
Name: "releaseNameFoo-forbidden",
|
||||||
|
Chart: "foo",
|
||||||
|
UpdateStrategy: "reinstallIfForbidden",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "releaseNameBar-forbidden",
|
||||||
|
Chart: "foo",
|
||||||
|
UpdateStrategy: "reinstallIfForbidden",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantAffected: exectest.Affected{
|
||||||
|
Upgraded: []*exectest.Release{
|
||||||
|
{Name: "releaseNameFoo-forbidden", Flags: []string{}},
|
||||||
|
{Name: "releaseNameBar-forbidden", Flags: []string{}},
|
||||||
|
},
|
||||||
|
Reinstalled: nil,
|
||||||
|
Deleted: nil,
|
||||||
|
Failed: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1 removed, 1 new, 1 reinstalled first new",
|
||||||
|
releases: []ReleaseSpec{
|
||||||
|
{
|
||||||
|
Name: "releaseNameFoo-forbidden",
|
||||||
|
Chart: "foo",
|
||||||
|
UpdateStrategy: "reinstallIfForbidden",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "releaseNameBar",
|
||||||
|
Chart: "foo",
|
||||||
|
UpdateStrategy: "reinstallIfForbidden",
|
||||||
|
Installed: &no,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "releaseNameFoo-forbidden",
|
||||||
|
Chart: "foo",
|
||||||
|
UpdateStrategy: "reinstallIfForbidden",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
installed: []bool{true, true, true},
|
||||||
|
wantAffected: exectest.Affected{
|
||||||
|
Upgraded: []*exectest.Release{
|
||||||
|
{Name: "releaseNameFoo-forbidden", Flags: []string{}},
|
||||||
|
},
|
||||||
|
Reinstalled: []*exectest.Release{
|
||||||
|
{Name: "releaseNameFoo-forbidden", Flags: []string{}},
|
||||||
|
},
|
||||||
|
Deleted: []*exectest.Release{
|
||||||
|
{Name: "releaseNameBar", Flags: []string{}},
|
||||||
|
},
|
||||||
|
Failed: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
state := &HelmState{
|
||||||
|
ReleaseSetSpec: ReleaseSetSpec{
|
||||||
|
Releases: tt.releases,
|
||||||
|
},
|
||||||
|
logger: logger,
|
||||||
|
valsRuntime: valsRuntime,
|
||||||
|
RenderedValues: map[string]any{},
|
||||||
|
}
|
||||||
|
helm := &exectest.Helm{
|
||||||
|
Lists: map[exectest.ListKey]string{},
|
||||||
|
}
|
||||||
|
//simulate the release is already installed
|
||||||
|
for i, release := range tt.releases {
|
||||||
|
if tt.installed != nil && tt.installed[i] {
|
||||||
|
helm.Lists[exectest.ListKey{Filter: "^" + release.Name + "$", Flags: "--uninstalling --deployed --failed --pending"}] = release.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
affectedReleases := AffectedReleases{}
|
||||||
|
if err := state.SyncReleases(&affectedReleases, helm, []string{}, 1); err != nil {
|
||||||
|
if !testEq(affectedReleases.Failed, tt.wantAffected.Failed) {
|
||||||
|
t.Errorf("HelmState.SyncReleases() error failed for [%s] = %v, want %v", tt.name, affectedReleases.Failed, tt.wantAffected.Failed)
|
||||||
|
} //else expected error
|
||||||
|
}
|
||||||
|
if !testEq(affectedReleases.Upgraded, tt.wantAffected.Upgraded) {
|
||||||
|
t.Errorf("HelmState.SyncReleases() upgrade failed for [%s] = %v, want %v", tt.name, affectedReleases.Upgraded, tt.wantAffected.Upgraded)
|
||||||
|
}
|
||||||
|
if !testEq(affectedReleases.Reinstalled, tt.wantAffected.Reinstalled) {
|
||||||
|
t.Errorf("HelmState.SyncReleases() reinstalled failed for [%s] = %v, want %v", tt.name, affectedReleases.Reinstalled, tt.wantAffected.Reinstalled)
|
||||||
|
}
|
||||||
|
if !testEq(affectedReleases.Deleted, tt.wantAffected.Deleted) {
|
||||||
|
t.Errorf("HelmState.SyncReleases() deleted failed for [%s] = %v, want %v", tt.name, affectedReleases.Deleted, tt.wantAffected.Deleted)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testEq(a []*ReleaseSpec, b []*exectest.Release) bool {
|
func testEq(a []*ReleaseSpec, b []*exectest.Release) bool {
|
||||||
// If one is nil, the other must also be nil.
|
// If one is nil, the other must also be nil.
|
||||||
if (a == nil) != (b == nil) {
|
if (a == nil) != (b == nil) {
|
||||||
|
|
|
||||||
|
|
@ -38,39 +38,39 @@ func TestGenerateID(t *testing.T) {
|
||||||
run(testcase{
|
run(testcase{
|
||||||
subject: "baseline",
|
subject: "baseline",
|
||||||
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
|
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
|
||||||
want: "foo-values-7d454b9558",
|
want: "foo-values-67dc97cbcb",
|
||||||
})
|
})
|
||||||
|
|
||||||
run(testcase{
|
run(testcase{
|
||||||
subject: "different bytes content",
|
subject: "different bytes content",
|
||||||
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
|
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
|
||||||
data: []byte(`{"k":"v"}`),
|
data: []byte(`{"k":"v"}`),
|
||||||
want: "foo-values-59c86d55bf",
|
want: "foo-values-75d7c4758c",
|
||||||
})
|
})
|
||||||
|
|
||||||
run(testcase{
|
run(testcase{
|
||||||
subject: "different map content",
|
subject: "different map content",
|
||||||
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
|
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw"},
|
||||||
data: map[string]any{"k": "v"},
|
data: map[string]any{"k": "v"},
|
||||||
want: "foo-values-6f87c5cd79",
|
want: "foo-values-685f8cf685",
|
||||||
})
|
})
|
||||||
|
|
||||||
run(testcase{
|
run(testcase{
|
||||||
subject: "different chart",
|
subject: "different chart",
|
||||||
release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"},
|
release: ReleaseSpec{Name: "foo", Chart: "stable/envoy"},
|
||||||
want: "foo-values-5dfd748475",
|
want: "foo-values-75597d9c57",
|
||||||
})
|
})
|
||||||
|
|
||||||
run(testcase{
|
run(testcase{
|
||||||
subject: "different name",
|
subject: "different name",
|
||||||
release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"},
|
release: ReleaseSpec{Name: "bar", Chart: "incubator/raw"},
|
||||||
want: "bar-values-858b9c55cc",
|
want: "bar-values-7b77df65ff",
|
||||||
})
|
})
|
||||||
|
|
||||||
run(testcase{
|
run(testcase{
|
||||||
subject: "specific ns",
|
subject: "specific ns",
|
||||||
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"},
|
release: ReleaseSpec{Name: "foo", Chart: "incubator/raw", Namespace: "myns"},
|
||||||
want: "myns-foo-values-58dc9c6667",
|
want: "myns-foo-values-85f979545c",
|
||||||
})
|
})
|
||||||
|
|
||||||
for id, n := range ids {
|
for id, n := range ids {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue