From 027cea48bf9a14024fc6855ae20aa09c25acb61b Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Wed, 30 Dec 2020 19:35:58 +0000 Subject: [PATCH 01/12] Handle SIGTERM Change the logic to release Upgrade to handle SIGTERMs Extract logic to 2 goroutine so it is possible to handle SIGTERMS and the release flow Fix go style Signed-off-by: Stephane Moser --- pkg/action/upgrade.go | 51 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 07d9cb40e..db5ae346e 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -20,7 +20,10 @@ import ( "bytes" "context" "fmt" + "os" + "os/signal" "strings" + "syscall" "time" "github.com/pkg/errors" @@ -102,6 +105,11 @@ type Upgrade struct { DependencyUpdate bool } +type ResultMessage struct { + r *release.Release + e error +} + // NewUpgrade creates a new Upgrade object with the given configuration. func NewUpgrade(cfg *Configuration) *Upgrade { return &Upgrade{ @@ -306,11 +314,33 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea if err := u.cfg.Releases.Create(upgradedRelease); err != nil { return nil, err } + rChan := make(chan ResultMessage) + go u.releasingUpgrade(rChan, upgradedRelease, current, target, originalRelease) + go u.handleSignals(rChan, upgradedRelease) + result := <-rChan + + return result.r, result.e +} +func (u *Upgrade) handleSignals(c chan<- ResultMessage, upgradedRelease *release.Release) { + // Handle SIGINT + cSignal := make(chan os.Signal) + signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) + go func() { + <-cSignal + u.cfg.Log("SIGTERM or SIGINT received") + r, e := u.failRelease(upgradedRelease, kube.ResourceList{}, fmt.Errorf("SIGTERM or SIGINT received, release failed")) + c <- ResultMessage{r: r, e: e} + }() +} +func (u *Upgrade) releasingUpgrade(c chan<- ResultMessage, upgradedRelease *release.Release, current kube.ResourceList, target kube.ResourceList, originalRelease *release.Release) { // pre-upgrade hooks + if !u.DisableHooks { if err := u.cfg.execHook(upgradedRelease, release.HookPreUpgrade, u.Timeout); err != nil { - return u.failRelease(upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err)) + r, e := u.failRelease(upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err)) + c <- ResultMessage{r: r, e: e} + return } } else { u.cfg.Log("upgrade hooks disabled for %s", upgradedRelease.Name) @@ -319,7 +349,9 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea results, err := u.cfg.KubeClient.Update(current, target, u.Force) if err != nil { u.cfg.recordRelease(originalRelease) - return u.failRelease(upgradedRelease, results.Created, err) + r, e := u.failRelease(upgradedRelease, results.Created, err) + c <- ResultMessage{r: r, e: e} + return } if u.Recreate { @@ -336,12 +368,16 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea if u.WaitForJobs { if err := u.cfg.KubeClient.WaitWithJobs(target, u.Timeout); err != nil { u.cfg.recordRelease(originalRelease) - return u.failRelease(upgradedRelease, results.Created, err) + r, e := u.failRelease(upgradedRelease, results.Created, err) + c <- ResultMessage{r: r, e: e} + return } } else { if err := u.cfg.KubeClient.Wait(target, u.Timeout); err != nil { u.cfg.recordRelease(originalRelease) - return u.failRelease(upgradedRelease, results.Created, err) + r, e := u.failRelease(upgradedRelease, results.Created, err) + c <- ResultMessage{r: r, e: e} + return } } } @@ -349,7 +385,9 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea // post-upgrade hooks if !u.DisableHooks { if err := u.cfg.execHook(upgradedRelease, release.HookPostUpgrade, u.Timeout); err != nil { - return u.failRelease(upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err)) + r, e := u.failRelease(upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err)) + c <- ResultMessage{r: r, e: e} + return } } @@ -362,8 +400,7 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea } else { upgradedRelease.Info.Description = "Upgrade complete" } - - return upgradedRelease, nil + c <- ResultMessage{r: upgradedRelease, e: nil} } func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, err error) (*release.Release, error) { From d7833eb2b0a3a35b2ac926b4495b2c3ac0dac580 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Thu, 7 Jan 2021 22:11:48 +0000 Subject: [PATCH 02/12] Refactor logic Use mutex to lock the action to report the upstream function Wrap logic to report to upstream function in the function reportToPerformUpgrade Signed-off-by: Stephane Moser --- pkg/action/upgrade.go | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index db5ae346e..8ac0ee714 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -23,6 +23,7 @@ import ( "os" "os/signal" "strings" + "sync" "syscall" "time" @@ -39,6 +40,8 @@ import ( "helm.sh/helm/v3/pkg/storage/driver" ) +var mutex sync.Mutex + // Upgrade is the action for upgrading releases. // // It provides the implementation of 'helm upgrade'. @@ -105,7 +108,7 @@ type Upgrade struct { DependencyUpdate bool } -type ResultMessage struct { +type resultMessage struct { r *release.Release e error } @@ -314,7 +317,7 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea if err := u.cfg.Releases.Create(upgradedRelease); err != nil { return nil, err } - rChan := make(chan ResultMessage) + rChan := make(chan resultMessage) go u.releasingUpgrade(rChan, upgradedRelease, current, target, originalRelease) go u.handleSignals(rChan, upgradedRelease) result := <-rChan @@ -322,24 +325,32 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea return result.r, result.e } -func (u *Upgrade) handleSignals(c chan<- ResultMessage, upgradedRelease *release.Release) { +func (u *Upgrade) reportToPerformUpgrade(c chan<- resultMessage, rel *release.Release, created kube.ResourceList, err error) { + mutex.Lock() + if err != nil { + rel, err = u.failRelease(rel, created, err) + } + c <- resultMessage{r: rel, e: err} + mutex.Unlock() +} +func (u *Upgrade) handleSignals(c chan<- resultMessage, upgradedRelease *release.Release) { // Handle SIGINT cSignal := make(chan os.Signal) signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) go func() { <-cSignal u.cfg.Log("SIGTERM or SIGINT received") - r, e := u.failRelease(upgradedRelease, kube.ResourceList{}, fmt.Errorf("SIGTERM or SIGINT received, release failed")) - c <- ResultMessage{r: r, e: e} + // when the atomic flag is set the ongoing release finish first and doesn't give time for the rollback happens . I need to think in a way to lock the chanel + // Implement function reportToPerformUpgrade(channel, Release, ResourceList, error) if error != nill call u.failRelease + u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, fmt.Errorf("SIGTERM or SIGINT received, release failed")) }() } -func (u *Upgrade) releasingUpgrade(c chan<- ResultMessage, upgradedRelease *release.Release, current kube.ResourceList, target kube.ResourceList, originalRelease *release.Release) { +func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *release.Release, current kube.ResourceList, target kube.ResourceList, originalRelease *release.Release) { // pre-upgrade hooks if !u.DisableHooks { if err := u.cfg.execHook(upgradedRelease, release.HookPreUpgrade, u.Timeout); err != nil { - r, e := u.failRelease(upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err)) - c <- ResultMessage{r: r, e: e} + u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, fmt.Errorf("pre-upgrade hooks failed: %s", err)) return } } else { @@ -349,8 +360,7 @@ func (u *Upgrade) releasingUpgrade(c chan<- ResultMessage, upgradedRelease *rele results, err := u.cfg.KubeClient.Update(current, target, u.Force) if err != nil { u.cfg.recordRelease(originalRelease) - r, e := u.failRelease(upgradedRelease, results.Created, err) - c <- ResultMessage{r: r, e: e} + u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) return } @@ -368,15 +378,13 @@ func (u *Upgrade) releasingUpgrade(c chan<- ResultMessage, upgradedRelease *rele if u.WaitForJobs { if err := u.cfg.KubeClient.WaitWithJobs(target, u.Timeout); err != nil { u.cfg.recordRelease(originalRelease) - r, e := u.failRelease(upgradedRelease, results.Created, err) - c <- ResultMessage{r: r, e: e} + u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) return } } else { if err := u.cfg.KubeClient.Wait(target, u.Timeout); err != nil { u.cfg.recordRelease(originalRelease) - r, e := u.failRelease(upgradedRelease, results.Created, err) - c <- ResultMessage{r: r, e: e} + u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) return } } @@ -385,8 +393,7 @@ func (u *Upgrade) releasingUpgrade(c chan<- ResultMessage, upgradedRelease *rele // post-upgrade hooks if !u.DisableHooks { if err := u.cfg.execHook(upgradedRelease, release.HookPostUpgrade, u.Timeout); err != nil { - r, e := u.failRelease(upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err)) - c <- ResultMessage{r: r, e: e} + u.reportToPerformUpgrade(c, upgradedRelease, results.Created, fmt.Errorf("post-upgrade hooks failed: %s", err)) return } } @@ -400,7 +407,7 @@ func (u *Upgrade) releasingUpgrade(c chan<- ResultMessage, upgradedRelease *rele } else { upgradedRelease.Info.Description = "Upgrade complete" } - c <- ResultMessage{r: upgradedRelease, e: nil} + u.reportToPerformUpgrade(c, upgradedRelease, nil, nil) } func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, err error) (*release.Release, error) { From 3434053d3841b230df4e8863ac6354369bc440f4 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Fri, 8 Jan 2021 11:29:23 +0000 Subject: [PATCH 03/12] Hande SIGINT in install command Replicate the same logic in that was implementd in the upgrade action to handle SIGINT Rename mutexes to isolate the variables Signed-off-by: Stephane Moser --- pkg/action/install.go | 53 ++++++++++++++++++++++++++++++++++++------- pkg/action/upgrade.go | 6 ++--- 2 files changed, 48 insertions(+), 11 deletions(-) diff --git a/pkg/action/install.go b/pkg/action/install.go index 933747d26..7c6ed474a 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -22,9 +22,12 @@ import ( "io/ioutil" "net/url" "os" + "os/signal" "path" "path/filepath" "strings" + "sync" + "syscall" "text/template" "time" @@ -66,6 +69,8 @@ const notesFileSuffix = "NOTES.txt" const defaultDirectoryPermission = 0755 +var installLock sync.Mutex + // Install performs an installation operation. type Install struct { cfg *Configuration @@ -331,11 +336,21 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. // not working. return rel, err } + rChan := make(chan resultMessage) + go i.performInstall(rChan, rel, toBeAdopted, resources) + go i.handleSignals(rChan, rel) + result := <-rChan + //start preformInstall go routine + return result.r, result.e +} + +func (i *Install) performInstall(c chan<- resultMessage, rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) { // pre-install hooks if !i.DisableHooks { if err := i.cfg.execHook(rel, release.HookPreInstall, i.Timeout); err != nil { - return i.failRelease(rel, fmt.Errorf("failed pre-install: %s", err)) + i.reportToRun(c, rel, fmt.Errorf("failed pre-install: %s", err)) + return } } @@ -344,29 +359,34 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. // to true, since that is basically an upgrade operation. if len(toBeAdopted) == 0 && len(resources) > 0 { if _, err := i.cfg.KubeClient.Create(resources); err != nil { - return i.failRelease(rel, err) + i.reportToRun(c, rel, err) + return } } else if len(resources) > 0 { if _, err := i.cfg.KubeClient.Update(toBeAdopted, resources, false); err != nil { - return i.failRelease(rel, err) + i.reportToRun(c, rel, err) + return } } if i.Wait { if i.WaitForJobs { if err := i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout); err != nil { - return i.failRelease(rel, err) + i.reportToRun(c, rel, err) + return } } else { if err := i.cfg.KubeClient.Wait(resources, i.Timeout); err != nil { - return i.failRelease(rel, err) + i.reportToRun(c, rel, err) + return } } } if !i.DisableHooks { if err := i.cfg.execHook(rel, release.HookPostInstall, i.Timeout); err != nil { - return i.failRelease(rel, fmt.Errorf("failed post-install: %s", err)) + i.reportToRun(c, rel, fmt.Errorf("failed post-install: %s", err)) + return } } @@ -387,9 +407,26 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. i.cfg.Log("failed to record the release: %s", err) } - return rel, nil + i.reportToRun(c, rel, nil) +} +func (i *Install) handleSignals(c chan<- resultMessage, rel *release.Release) { + // Handle SIGINT + cSignal := make(chan os.Signal) + signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) + go func() { + <-cSignal + i.cfg.Log("SIGTERM or SIGINT received") + i.reportToRun(c, rel, fmt.Errorf("SIGTERM or SIGINT received, release failed")) + }() +} +func (i *Install) reportToRun(c chan<- resultMessage, rel *release.Release, err error) { + installLock.Lock() + if err != nil { + rel, err = i.failRelease(rel, err) + } + c <- resultMessage{r: rel, e: err} + installLock.Unlock() } - func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) { rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error())) if i.Atomic { diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 8ac0ee714..67bcd094f 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -40,7 +40,7 @@ import ( "helm.sh/helm/v3/pkg/storage/driver" ) -var mutex sync.Mutex +var upgradeLock sync.Mutex // Upgrade is the action for upgrading releases. // @@ -326,12 +326,12 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea } func (u *Upgrade) reportToPerformUpgrade(c chan<- resultMessage, rel *release.Release, created kube.ResourceList, err error) { - mutex.Lock() + upgradeLock.Lock() if err != nil { rel, err = u.failRelease(rel, created, err) } c <- resultMessage{r: rel, e: err} - mutex.Unlock() + upgradeLock.Unlock() } func (u *Upgrade) handleSignals(c chan<- resultMessage, upgradedRelease *release.Release) { // Handle SIGINT From 2fa339b25d0a011d82d3ae3af7f64f6b6cc3a83b Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Sun, 31 Jan 2021 21:02:08 +0000 Subject: [PATCH 04/12] Add test for Upgrade Release Interrupted Implement timer in the fake.go and printer.go to simulate the wait period Add test Upgrade Release when it is interruped with SIGINT Signed-off-by: Stephane Moser --- pkg/action/upgrade_test.go | 67 ++++++++++++++++++++++++++++++++++++++ pkg/kube/fake/fake.go | 6 ++++ pkg/kube/fake/printer.go | 9 +++-- 3 files changed, 80 insertions(+), 2 deletions(-) diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 5cca7ca1a..e03c68011 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -18,7 +18,11 @@ package action import ( "fmt" + "io/ioutil" + "os" + "os/exec" "testing" + gotime "time" "helm.sh/helm/v3/pkg/chart" @@ -296,3 +300,66 @@ func TestUpgradeRelease_Pending(t *testing.T) { _, err := upAction.Run(rel.Name, buildChart(), vals) req.Contains(err.Error(), "progress", err) } + +func TestUpgradeRelease_Interrupted_Wait(t *testing.T) { + if os.Getenv("HANDLE_SIGINT") == "1" { + t.Run("Execute TestUpgradeRelease_Interrupted_Wait", func(t *testing.T) { + is := assert.New(t) + req := require.New(t) + + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "interrupted-release" + rel.Info.Status = release.StatusDeployed + upAction.cfg.Releases.Create(rel) + + failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.PrintingKubeClient.WaitDuration = 10 * gotime.Second + upAction.cfg.KubeClient = failer + upAction.Wait = true + vals := map[string]interface{}{} + res, err := upAction.Run(rel.Name, buildChart(), vals) + + req.Error(err) + is.Contains(res.Info.Description, "Upgrade \"interrupted-release\" failed: SIGTERM or SIGINT received, release failed") + is.Equal(res.Info.Status, release.StatusFailed) + }) + return + + } + t.Run("Setup TestUpgradeRelease_Interrupted_Wait", func(t *testing.T) { + cmd := exec.Command(os.Args[0], "-test.run=TestUpgradeRelease_Interrupted_Wait") + cmd.Env = append(os.Environ(), "HANDLE_SIGINT=1") + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + stderr, err := cmd.StderrPipe() + if err != nil { + t.Fatal(err) + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + go func() { + slurp, _ := ioutil.ReadAll(stdout) + fmt.Printf("%s\n", slurp) + }() + + go func() { + slurp, _ := ioutil.ReadAll(stderr) + fmt.Printf("%s\n", slurp) + }() + + gotime.Sleep(2 * gotime.Second) + p, _ := os.FindProcess(cmd.Process.Pid) + + if err := p.Signal(os.Interrupt); err != nil { + t.Fatal(err) + } + + if err := cmd.Wait(); err != nil { + t.FailNow() + } + }) +} diff --git a/pkg/kube/fake/fake.go b/pkg/kube/fake/fake.go index ff800864c..a80961566 100644 --- a/pkg/kube/fake/fake.go +++ b/pkg/kube/fake/fake.go @@ -40,6 +40,7 @@ type FailingKubeClient struct { BuildError error BuildUnstructuredError error WaitAndGetCompletedPodPhaseError error + WaitDuration time.Duration } // Create returns the configured error if set or prints @@ -52,7 +53,12 @@ func (f *FailingKubeClient) Create(resources kube.ResourceList) (*kube.Result, e // Wait returns the configured error if set or prints func (f *FailingKubeClient) Wait(resources kube.ResourceList, d time.Duration) error { + if f.WaitDuration != 0 { + d = f.WaitDuration + } + if f.WaitError != nil { + time.Sleep(d) return f.WaitError } return f.PrintingKubeClient.Wait(resources, d) diff --git a/pkg/kube/fake/printer.go b/pkg/kube/fake/printer.go index e8bd1845b..d08c71444 100644 --- a/pkg/kube/fake/printer.go +++ b/pkg/kube/fake/printer.go @@ -30,7 +30,8 @@ import ( // PrintingKubeClient implements KubeClient, but simply prints the reader to // the given output. type PrintingKubeClient struct { - Out io.Writer + Out io.Writer + WaitDuration time.Duration } // IsReachable checks if the cluster is reachable @@ -47,7 +48,11 @@ func (p *PrintingKubeClient) Create(resources kube.ResourceList) (*kube.Result, return &kube.Result{Created: resources}, nil } -func (p *PrintingKubeClient) Wait(resources kube.ResourceList, _ time.Duration) error { +func (p *PrintingKubeClient) Wait(resources kube.ResourceList, d time.Duration) error { + if p.WaitDuration != 0 { + time.Sleep(p.WaitDuration) + } + _, err := io.Copy(p.Out, bufferize(resources)) return err } From 660183d65993751d98f92db312ba597a653e4642 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Tue, 2 Feb 2021 18:01:20 +0000 Subject: [PATCH 05/12] Add test for atomic upgrade and install when it is interrupted Add tests when the install release is Interrupted and the flag Wait or Atomic is set Signed-off-by: Stephane Moser --- pkg/action/install_test.go | 119 +++++++++++++++++++++++++++++++++++++ pkg/action/upgrade.go | 9 ++- pkg/action/upgrade_test.go | 68 +++++++++++++++++++++ 3 files changed, 193 insertions(+), 3 deletions(-) diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 63383d778..786f7270e 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -21,10 +21,12 @@ import ( "io/ioutil" "log" "os" + "os/exec" "path/filepath" "regexp" "strings" "testing" + gotime "time" "github.com/stretchr/testify/assert" @@ -361,7 +363,62 @@ func TestInstallRelease_Wait(t *testing.T) { is.Contains(res.Info.Description, "I timed out") is.Equal(res.Info.Status, release.StatusFailed) } +func TestInstallRelease_Wait_Interrupted(t *testing.T) { + if os.Getenv("HANDLE_SIGINT") == "1" { + t.Run("Execute TestInstallRelease_Wait_Interrupted", func(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "interrupted-release" + failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.PrintingKubeClient.WaitDuration = 10 * gotime.Second + instAction.cfg.KubeClient = failer + instAction.Wait = true + vals := map[string]interface{}{} + + res, err := instAction.Run(buildChart(), vals) + is.Error(err) + is.Contains(res.Info.Description, "SIGTERM or SIGINT received, release failed") + is.Equal(res.Info.Status, release.StatusFailed) + }) + return + + } + t.Run("Setup TestInstallRelease_Wait_Interrupted", func(t *testing.T) { + cmd := exec.Command(os.Args[0], "-test.run=TestInstallRelease_Wait_Interrupted") + cmd.Env = append(os.Environ(), "HANDLE_SIGINT=1") + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + stderr, err := cmd.StderrPipe() + if err != nil { + t.Fatal(err) + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + go func() { + slurp, _ := ioutil.ReadAll(stdout) + fmt.Printf("%s\n", slurp) + }() + go func() { + slurp, _ := ioutil.ReadAll(stderr) + fmt.Printf("%s\n", slurp) + }() + + gotime.Sleep(2 * gotime.Second) + p, _ := os.FindProcess(cmd.Process.Pid) + + if err := p.Signal(os.Interrupt); err != nil { + t.Fatal(err) + } + + if err := cmd.Wait(); err != nil { + t.FailNow() + } + }) +} func TestInstallRelease_WaitForJobs(t *testing.T) { is := assert.New(t) instAction := installAction(t) @@ -419,7 +476,69 @@ func TestInstallRelease_Atomic(t *testing.T) { is.Contains(err.Error(), "an error occurred while uninstalling the release") }) } +func TestInstallRelease_Atomic_Interrupted(t *testing.T) { + if os.Getenv("HANDLE_SIGINT") == "1" { + t.Run("Execute TestInstallRelease_Atomic_Interrupted", func(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "interrupted-release" + failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.PrintingKubeClient.WaitDuration = 10 * gotime.Second + instAction.cfg.KubeClient = failer + instAction.Atomic = true + vals := map[string]interface{}{} + + res, err := instAction.Run(buildChart(), vals) + is.Error(err) + is.Contains(err.Error(), "SIGTERM or SIGINT received, release failed") + is.Contains(err.Error(), "atomic") + is.Contains(err.Error(), "uninstalled") + + // Now make sure it isn't in storage any more + _, err = instAction.cfg.Releases.Get(res.Name, res.Version) + is.Error(err) + is.Equal(err, driver.ErrReleaseNotFound) + }) + return + + } + t.Run("Setup TestInstallRelease_Atomic_Interrupted", func(t *testing.T) { + cmd := exec.Command(os.Args[0], "-test.run=TestInstallRelease_Atomic_Interrupted") + cmd.Env = append(os.Environ(), "HANDLE_SIGINT=1") + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + stderr, err := cmd.StderrPipe() + if err != nil { + t.Fatal(err) + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + go func() { + slurp, _ := ioutil.ReadAll(stdout) + fmt.Printf("%s\n", slurp) + }() + + go func() { + slurp, _ := ioutil.ReadAll(stderr) + fmt.Printf("%s\n", slurp) + }() + + gotime.Sleep(2 * gotime.Second) + p, _ := os.FindProcess(cmd.Process.Pid) + + if err := p.Signal(os.Interrupt); err != nil { + t.Fatal(err) + } + + if err := cmd.Wait(); err != nil { + t.FailNow() + } + }) +} func TestNameTemplate(t *testing.T) { testCases := []nameTemplateTestCase{ // Just a straight up nop please diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 67bcd094f..f050ad59f 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -325,6 +325,9 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea return result.r, result.e } +// Function used to lock the Mutex, this is important for the case when the atomic flag is set. +// In that case the upgrade will finish before the rollback is finished so it is necessary to wait for the rollback to finish. +// The rollback will be trigger by the function failRelease func (u *Upgrade) reportToPerformUpgrade(c chan<- resultMessage, rel *release.Release, created kube.ResourceList, err error) { upgradeLock.Lock() if err != nil { @@ -333,15 +336,15 @@ func (u *Upgrade) reportToPerformUpgrade(c chan<- resultMessage, rel *release.Re c <- resultMessage{r: rel, e: err} upgradeLock.Unlock() } + +// Setup listener for SIGINT and SIGTERM func (u *Upgrade) handleSignals(c chan<- resultMessage, upgradedRelease *release.Release) { - // Handle SIGINT cSignal := make(chan os.Signal) signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) go func() { <-cSignal u.cfg.Log("SIGTERM or SIGINT received") - // when the atomic flag is set the ongoing release finish first and doesn't give time for the rollback happens . I need to think in a way to lock the chanel - // Implement function reportToPerformUpgrade(channel, Release, ResourceList, error) if error != nill call u.failRelease + // when the atomic flag is set the ongoing release finish first and doesn't give time for the rollback happens. u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, fmt.Errorf("SIGTERM or SIGINT received, release failed")) }() } diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index e03c68011..0af987e5e 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -363,3 +363,71 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) { } }) } + +func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) { + if os.Getenv("HANDLE_SIGINT") == "1" { + t.Run("Execute TestUpgradeRelease_Interrupted_Atomic", func(t *testing.T) { + is := assert.New(t) + req := require.New(t) + + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "interrupted-release" + rel.Info.Status = release.StatusDeployed + upAction.cfg.Releases.Create(rel) + + failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.PrintingKubeClient.WaitDuration = 5 * gotime.Second + upAction.cfg.KubeClient = failer + upAction.Atomic = true + vals := map[string]interface{}{} + res, err := upAction.Run(rel.Name, buildChart(), vals) + + req.Error(err) + is.Contains(err.Error(), "release interrupted-release failed, and has been rolled back due to atomic being set: SIGTERM or SIGINT received, release failed") + + // Now make sure it is actually upgraded + updatedRes, err := upAction.cfg.Releases.Get(res.Name, 3) + is.NoError(err) + // Should have rolled back to the previous + is.Equal(updatedRes.Info.Status, release.StatusDeployed) + }) + return + + } + t.Run("Setup TestUpgradeRelease_Interrupted_Atomic", func(t *testing.T) { + cmd := exec.Command(os.Args[0], "-test.run=TestUpgradeRelease_Interrupted_Atomic") + cmd.Env = append(os.Environ(), "HANDLE_SIGINT=1") + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + stderr, err := cmd.StderrPipe() + if err != nil { + t.Fatal(err) + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + go func() { + slurp, _ := ioutil.ReadAll(stdout) + fmt.Printf("%s\n", slurp) + }() + + go func() { + slurp, _ := ioutil.ReadAll(stderr) + fmt.Printf("%s\n", slurp) + }() + + gotime.Sleep(2 * gotime.Second) + p, _ := os.FindProcess(cmd.Process.Pid) + + if err := p.Signal(os.Interrupt); err != nil { + t.Fatal(err) + } + + if err := cmd.Wait(); err != nil { + t.FailNow() + } + }) +} From c6c4157c43c336a5bbe72b15067e54dfc1362a61 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Sun, 4 Jul 2021 12:04:35 +0100 Subject: [PATCH 06/12] Move locks from global var to the structs Signed-off-by: Stephane Moser --- pkg/action/install.go | 8 ++++---- pkg/action/upgrade.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/action/install.go b/pkg/action/install.go index 7c6ed474a..a394155ff 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -69,8 +69,6 @@ const notesFileSuffix = "NOTES.txt" const defaultDirectoryPermission = 0755 -var installLock sync.Mutex - // Install performs an installation operation. type Install struct { cfg *Configuration @@ -109,6 +107,8 @@ type Install struct { // OutputDir/ UseReleaseName bool PostRenderer postrender.PostRenderer + // Lock to control raceconditions when the process receives a SIGTERM + Lock sync.Mutex } // ChartPathOptions captures common options used for controlling chart paths @@ -420,12 +420,12 @@ func (i *Install) handleSignals(c chan<- resultMessage, rel *release.Release) { }() } func (i *Install) reportToRun(c chan<- resultMessage, rel *release.Release, err error) { - installLock.Lock() + i.Lock.Lock() if err != nil { rel, err = i.failRelease(rel, err) } c <- resultMessage{r: rel, e: err} - installLock.Unlock() + i.Lock.Unlock() } func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) { rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error())) diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index f050ad59f..f1ca96764 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -40,8 +40,6 @@ import ( "helm.sh/helm/v3/pkg/storage/driver" ) -var upgradeLock sync.Mutex - // Upgrade is the action for upgrading releases. // // It provides the implementation of 'helm upgrade'. @@ -106,6 +104,8 @@ type Upgrade struct { DisableOpenAPIValidation bool // Get missing dependencies DependencyUpdate bool + // Lock to control raceconditions when the process receives a SIGTERM + Lock sync.Mutex } type resultMessage struct { @@ -329,12 +329,12 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea // In that case the upgrade will finish before the rollback is finished so it is necessary to wait for the rollback to finish. // The rollback will be trigger by the function failRelease func (u *Upgrade) reportToPerformUpgrade(c chan<- resultMessage, rel *release.Release, created kube.ResourceList, err error) { - upgradeLock.Lock() + u.Lock.Lock() if err != nil { rel, err = u.failRelease(rel, created, err) } c <- resultMessage{r: rel, e: err} - upgradeLock.Unlock() + u.Lock.Unlock() } // Setup listener for SIGINT and SIGTERM From 2164e3f26c37982fb5d0e563bbf920ad9f43201b Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Thu, 15 Jul 2021 10:08:25 +0100 Subject: [PATCH 07/12] Rename time dependecy Signed-off-by: Stephane Moser --- pkg/action/upgrade_test.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 0af987e5e..04af94e71 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -22,7 +22,7 @@ import ( "os" "os/exec" "testing" - gotime "time" + "time" "helm.sh/helm/v3/pkg/chart" @@ -31,7 +31,7 @@ import ( kubefake "helm.sh/helm/v3/pkg/kube/fake" "helm.sh/helm/v3/pkg/release" - "helm.sh/helm/v3/pkg/time" + helmtime "helm.sh/helm/v3/pkg/time" ) func upgradeAction(t *testing.T) *Upgrade { @@ -229,7 +229,7 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) { withValues(chartDefaultValues), withMetadataDependency(dependency), ) - now := time.Now() + now := helmtime.Now() existingValues := map[string]interface{}{ "subchart": map[string]interface{}{ "enabled": false, @@ -314,7 +314,7 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) { upAction.cfg.Releases.Create(rel) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.PrintingKubeClient.WaitDuration = 10 * gotime.Second + failer.PrintingKubeClient.WaitDuration = 10 * time.Second upAction.cfg.KubeClient = failer upAction.Wait = true vals := map[string]interface{}{} @@ -351,7 +351,7 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) { fmt.Printf("%s\n", slurp) }() - gotime.Sleep(2 * gotime.Second) + time.Sleep(2 * time.Second) p, _ := os.FindProcess(cmd.Process.Pid) if err := p.Signal(os.Interrupt); err != nil { @@ -377,7 +377,7 @@ func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) { upAction.cfg.Releases.Create(rel) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.PrintingKubeClient.WaitDuration = 5 * gotime.Second + failer.PrintingKubeClient.WaitDuration = 5 * time.Second upAction.cfg.KubeClient = failer upAction.Atomic = true vals := map[string]interface{}{} @@ -419,7 +419,7 @@ func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) { fmt.Printf("%s\n", slurp) }() - gotime.Sleep(2 * gotime.Second) + time.Sleep(2 * time.Second) p, _ := os.FindProcess(cmd.Process.Pid) if err := p.Signal(os.Interrupt); err != nil { From 7bfb2a3602953b052db85dcb222157a32db9547a Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Thu, 15 Jul 2021 10:26:49 +0100 Subject: [PATCH 08/12] Improve tests Rename the package time Redesgin the logic to make a FakeKubeClient wait for a ammount time. Remove unneed logic in the PrintingKubeClient Signed-off-by: Stephane Moser --- pkg/action/install_test.go | 24 ++++++++++++------------ pkg/action/upgrade_test.go | 4 ++-- pkg/kube/fake/fake.go | 6 ++---- pkg/kube/fake/printer.go | 7 +------ 4 files changed, 17 insertions(+), 24 deletions(-) diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 786f7270e..46fac72f3 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -26,7 +26,7 @@ import ( "regexp" "strings" "testing" - gotime "time" + "time" "github.com/stretchr/testify/assert" @@ -36,7 +36,7 @@ import ( kubefake "helm.sh/helm/v3/pkg/kube/fake" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/storage/driver" - "helm.sh/helm/v3/pkg/time" + helmtime "helm.sh/helm/v3/pkg/time" ) type nameTemplateTestCase struct { @@ -370,7 +370,7 @@ func TestInstallRelease_Wait_Interrupted(t *testing.T) { instAction := installAction(t) instAction.ReleaseName = "interrupted-release" failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.PrintingKubeClient.WaitDuration = 10 * gotime.Second + failer.WaitDuration = 10 * time.Second instAction.cfg.KubeClient = failer instAction.Wait = true vals := map[string]interface{}{} @@ -407,7 +407,7 @@ func TestInstallRelease_Wait_Interrupted(t *testing.T) { fmt.Printf("%s\n", slurp) }() - gotime.Sleep(2 * gotime.Second) + time.Sleep(2 * time.Second) p, _ := os.FindProcess(cmd.Process.Pid) if err := p.Signal(os.Interrupt); err != nil { @@ -483,7 +483,7 @@ func TestInstallRelease_Atomic_Interrupted(t *testing.T) { instAction := installAction(t) instAction.ReleaseName = "interrupted-release" failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.PrintingKubeClient.WaitDuration = 10 * gotime.Second + failer.WaitDuration = 10 * time.Second instAction.cfg.KubeClient = failer instAction.Atomic = true vals := map[string]interface{}{} @@ -527,7 +527,7 @@ func TestInstallRelease_Atomic_Interrupted(t *testing.T) { fmt.Printf("%s\n", slurp) }() - gotime.Sleep(2 * gotime.Second) + time.Sleep(2 * time.Second) p, _ := os.FindProcess(cmd.Process.Pid) if err := p.Signal(os.Interrupt); err != nil { @@ -743,32 +743,32 @@ func TestNameAndChartGenerateName(t *testing.T) { { "local filepath", "./chart", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, { "dot filepath", ".", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, { "empty filepath", "", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, { "packaged chart", "chart.tgz", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, { "packaged chart with .tar.gz extension", "chart.tar.gz", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, { "packaged chart with local extension", "./chart.tgz", - fmt.Sprintf("chart-%d", time.Now().Unix()), + fmt.Sprintf("chart-%d", helmtime.Now().Unix()), }, } diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 04af94e71..2aad26573 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -314,7 +314,7 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) { upAction.cfg.Releases.Create(rel) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.PrintingKubeClient.WaitDuration = 10 * time.Second + failer.WaitDuration = 10 * time.Second upAction.cfg.KubeClient = failer upAction.Wait = true vals := map[string]interface{}{} @@ -377,7 +377,7 @@ func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) { upAction.cfg.Releases.Create(rel) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.PrintingKubeClient.WaitDuration = 5 * time.Second + failer.WaitDuration = 5 * time.Second upAction.cfg.KubeClient = failer upAction.Atomic = true vals := map[string]interface{}{} diff --git a/pkg/kube/fake/fake.go b/pkg/kube/fake/fake.go index a80961566..1aa390511 100644 --- a/pkg/kube/fake/fake.go +++ b/pkg/kube/fake/fake.go @@ -51,14 +51,12 @@ func (f *FailingKubeClient) Create(resources kube.ResourceList) (*kube.Result, e return f.PrintingKubeClient.Create(resources) } -// Wait returns the configured error if set or prints +// Waits the amount of time defined on f.WaitDuration, then returns the configured error if set or prints. func (f *FailingKubeClient) Wait(resources kube.ResourceList, d time.Duration) error { if f.WaitDuration != 0 { - d = f.WaitDuration + time.Sleep(f.WaitDuration) } - if f.WaitError != nil { - time.Sleep(d) return f.WaitError } return f.PrintingKubeClient.Wait(resources, d) diff --git a/pkg/kube/fake/printer.go b/pkg/kube/fake/printer.go index d08c71444..bdaae6be3 100644 --- a/pkg/kube/fake/printer.go +++ b/pkg/kube/fake/printer.go @@ -30,8 +30,7 @@ import ( // PrintingKubeClient implements KubeClient, but simply prints the reader to // the given output. type PrintingKubeClient struct { - Out io.Writer - WaitDuration time.Duration + Out io.Writer } // IsReachable checks if the cluster is reachable @@ -49,10 +48,6 @@ func (p *PrintingKubeClient) Create(resources kube.ResourceList) (*kube.Result, } func (p *PrintingKubeClient) Wait(resources kube.ResourceList, d time.Duration) error { - if p.WaitDuration != 0 { - time.Sleep(p.WaitDuration) - } - _, err := io.Copy(p.Out, bufferize(resources)) return err } From 4026190e7b07ac0596fe36362dc070987d2df4e7 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Thu, 15 Jul 2021 10:31:59 +0100 Subject: [PATCH 09/12] Revert change in PrintingKubeClient Signed-off-by: Stephane Moser --- pkg/kube/fake/printer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/kube/fake/printer.go b/pkg/kube/fake/printer.go index bdaae6be3..e8bd1845b 100644 --- a/pkg/kube/fake/printer.go +++ b/pkg/kube/fake/printer.go @@ -47,7 +47,7 @@ func (p *PrintingKubeClient) Create(resources kube.ResourceList) (*kube.Result, return &kube.Result{Created: resources}, nil } -func (p *PrintingKubeClient) Wait(resources kube.ResourceList, d time.Duration) error { +func (p *PrintingKubeClient) Wait(resources kube.ResourceList, _ time.Duration) error { _, err := io.Copy(p.Out, bufferize(resources)) return err } From c62ce12bed39ded7ccaa2a09de3b3b351bf2d4b7 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Mon, 26 Jul 2021 01:04:58 +0100 Subject: [PATCH 10/12] Refactor SIGTERM logic Use context to handle SIGTERM in the cmd/helm instead of pkg/action Signed-off-by: Stephane Moser --- cmd/helm/install.go | 21 ++++- cmd/helm/upgrade.go | 19 ++++- pkg/action/install.go | 23 ++--- pkg/action/install_test.go | 145 ++++++++----------------------- pkg/action/upgrade.go | 28 +++--- pkg/action/upgrade_test.go | 170 +++++++++++-------------------------- 6 files changed, 152 insertions(+), 254 deletions(-) diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 04419d730..f72c3d388 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -17,8 +17,13 @@ limitations under the License. package main import ( + "context" + "fmt" "io" "log" + "os" + "os/signal" + "syscall" "time" "github.com/pkg/errors" @@ -239,7 +244,21 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options } client.Namespace = settings.Namespace() - return client.Run(chartRequested, vals) + + // Create context and prepare the handle of SIGTERM + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + + // Handle SIGTERM + cSignal := make(chan os.Signal) + signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) + go func() { + <-cSignal + fmt.Fprintf(out, "Release %s has been cancel.\n", args[0]) + cancel() + }() + + return client.RunWithContext(ctx, chartRequested, vals) } // checkIfInstallable validates if a chart can be installed diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 3bd392d1d..862b6e341 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -17,9 +17,13 @@ limitations under the License. package main import ( + "context" "fmt" "io" "log" + "os" + "os/signal" + "syscall" "time" "github.com/pkg/errors" @@ -174,7 +178,20 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { warning("This chart is deprecated") } - rel, err := client.Run(args[0], ch, vals) + // Create context and prepare the handle of SIGTERM + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + + // Handle SIGTERM + cSignal := make(chan os.Signal) + signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) + go func() { + <-cSignal + fmt.Fprintf(out, "Release %s has been cancel.\n", args[0]) + cancel() + }() + + rel, err := client.RunWithContext(ctx, args[0], ch, vals) if err != nil { return errors.Wrap(err, "UPGRADE FAILED") } diff --git a/pkg/action/install.go b/pkg/action/install.go index a394155ff..03a5cd7f3 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -18,16 +18,15 @@ package action import ( "bytes" + "context" "fmt" "io/ioutil" "net/url" "os" - "os/signal" "path" "path/filepath" "strings" "sync" - "syscall" "text/template" "time" @@ -179,7 +178,14 @@ func (i *Install) installCRDs(crds []chart.CRD) error { // Run executes the installation // // If DryRun is set to true, this will prepare the release, but not install it + func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + ctx := context.Background() + return i.RunWithContext(ctx, chrt, vals) +} + +// Run executes the installation with Context +func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { // Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`) if !i.ClientOnly { if err := i.cfg.KubeClient.IsReachable(); err != nil { @@ -338,7 +344,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. } rChan := make(chan resultMessage) go i.performInstall(rChan, rel, toBeAdopted, resources) - go i.handleSignals(rChan, rel) + go i.handleContext(ctx, rChan, rel) result := <-rChan //start preformInstall go routine return result.r, result.e @@ -409,14 +415,11 @@ func (i *Install) performInstall(c chan<- resultMessage, rel *release.Release, t i.reportToRun(c, rel, nil) } -func (i *Install) handleSignals(c chan<- resultMessage, rel *release.Release) { - // Handle SIGINT - cSignal := make(chan os.Signal) - signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) +func (i *Install) handleContext(ctx context.Context, c chan<- resultMessage, rel *release.Release) { go func() { - <-cSignal - i.cfg.Log("SIGTERM or SIGINT received") - i.reportToRun(c, rel, fmt.Errorf("SIGTERM or SIGINT received, release failed")) + <-ctx.Done() + err := ctx.Err() + i.reportToRun(c, rel, err) }() } func (i *Install) reportToRun(c chan<- resultMessage, rel *release.Release, err error) { diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 46fac72f3..b1844b2ce 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -17,11 +17,11 @@ limitations under the License. package action import ( + "context" "fmt" "io/ioutil" "log" "os" - "os/exec" "path/filepath" "regexp" "strings" @@ -364,60 +364,23 @@ func TestInstallRelease_Wait(t *testing.T) { is.Equal(res.Info.Status, release.StatusFailed) } func TestInstallRelease_Wait_Interrupted(t *testing.T) { - if os.Getenv("HANDLE_SIGINT") == "1" { - t.Run("Execute TestInstallRelease_Wait_Interrupted", func(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.ReleaseName = "interrupted-release" - failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitDuration = 10 * time.Second - instAction.cfg.KubeClient = failer - instAction.Wait = true - vals := map[string]interface{}{} - - res, err := instAction.Run(buildChart(), vals) - is.Error(err) - is.Contains(res.Info.Description, "SIGTERM or SIGINT received, release failed") - is.Equal(res.Info.Status, release.StatusFailed) - }) - return - - } - t.Run("Setup TestInstallRelease_Wait_Interrupted", func(t *testing.T) { - cmd := exec.Command(os.Args[0], "-test.run=TestInstallRelease_Wait_Interrupted") - cmd.Env = append(os.Environ(), "HANDLE_SIGINT=1") - stdout, err := cmd.StdoutPipe() - if err != nil { - t.Fatal(err) - } - stderr, err := cmd.StderrPipe() - if err != nil { - t.Fatal(err) - } - if err := cmd.Start(); err != nil { - t.Fatal(err) - } - go func() { - slurp, _ := ioutil.ReadAll(stdout) - fmt.Printf("%s\n", slurp) - }() - - go func() { - slurp, _ := ioutil.ReadAll(stderr) - fmt.Printf("%s\n", slurp) - }() - - time.Sleep(2 * time.Second) - p, _ := os.FindProcess(cmd.Process.Pid) + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "interrupted-release" + failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitDuration = 10 * time.Second + instAction.cfg.KubeClient = failer + instAction.Wait = true + vals := map[string]interface{}{} - if err := p.Signal(os.Interrupt); err != nil { - t.Fatal(err) - } + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + time.AfterFunc(time.Second, cancel) - if err := cmd.Wait(); err != nil { - t.FailNow() - } - }) + res, err := instAction.RunWithContext(ctx, buildChart(), vals) + is.Error(err) + is.Contains(res.Info.Description, "Release \"interrupted-release\" failed: context canceled") + is.Equal(res.Info.Status, release.StatusFailed) } func TestInstallRelease_WaitForJobs(t *testing.T) { is := assert.New(t) @@ -477,67 +440,31 @@ func TestInstallRelease_Atomic(t *testing.T) { }) } func TestInstallRelease_Atomic_Interrupted(t *testing.T) { - if os.Getenv("HANDLE_SIGINT") == "1" { - t.Run("Execute TestInstallRelease_Atomic_Interrupted", func(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.ReleaseName = "interrupted-release" - failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitDuration = 10 * time.Second - instAction.cfg.KubeClient = failer - instAction.Atomic = true - vals := map[string]interface{}{} - - res, err := instAction.Run(buildChart(), vals) - is.Error(err) - is.Contains(err.Error(), "SIGTERM or SIGINT received, release failed") - is.Contains(err.Error(), "atomic") - is.Contains(err.Error(), "uninstalled") - - // Now make sure it isn't in storage any more - _, err = instAction.cfg.Releases.Get(res.Name, res.Version) - is.Error(err) - is.Equal(err, driver.ErrReleaseNotFound) - - }) - return - } - t.Run("Setup TestInstallRelease_Atomic_Interrupted", func(t *testing.T) { - cmd := exec.Command(os.Args[0], "-test.run=TestInstallRelease_Atomic_Interrupted") - cmd.Env = append(os.Environ(), "HANDLE_SIGINT=1") - stdout, err := cmd.StdoutPipe() - if err != nil { - t.Fatal(err) - } - stderr, err := cmd.StderrPipe() - if err != nil { - t.Fatal(err) - } - if err := cmd.Start(); err != nil { - t.Fatal(err) - } - go func() { - slurp, _ := ioutil.ReadAll(stdout) - fmt.Printf("%s\n", slurp) - }() + is := assert.New(t) + instAction := installAction(t) + instAction.ReleaseName = "interrupted-release" + failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitDuration = 10 * time.Second + instAction.cfg.KubeClient = failer + instAction.Atomic = true + vals := map[string]interface{}{} - go func() { - slurp, _ := ioutil.ReadAll(stderr) - fmt.Printf("%s\n", slurp) - }() + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + time.AfterFunc(time.Second, cancel) - time.Sleep(2 * time.Second) - p, _ := os.FindProcess(cmd.Process.Pid) + res, err := instAction.RunWithContext(ctx, buildChart(), vals) + is.Error(err) + is.Contains(err.Error(), "context canceled") + is.Contains(err.Error(), "atomic") + is.Contains(err.Error(), "uninstalled") - if err := p.Signal(os.Interrupt); err != nil { - t.Fatal(err) - } + // Now make sure it isn't in storage any more + _, err = instAction.cfg.Releases.Get(res.Name, res.Version) + is.Error(err) + is.Equal(err, driver.ErrReleaseNotFound) - if err := cmd.Wait(); err != nil { - t.FailNow() - } - }) } func TestNameTemplate(t *testing.T) { testCases := []nameTemplateTestCase{ diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index f1ca96764..f76dbbc6b 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -20,11 +20,8 @@ import ( "bytes" "context" "fmt" - "os" - "os/signal" "strings" "sync" - "syscall" "time" "github.com/pkg/errors" @@ -120,8 +117,14 @@ func NewUpgrade(cfg *Configuration) *Upgrade { } } -// Run executes the upgrade on the given release. +// Run executes the upgrade on the given release func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + ctx := context.Background() + return u.RunWithContext(ctx, name, chart, vals) +} + +// Run executes the upgrade on the given release with context. +func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) { if err := u.cfg.KubeClient.IsReachable(); err != nil { return nil, err } @@ -142,7 +145,7 @@ func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface u.cfg.Releases.MaxHistory = u.MaxHistory u.cfg.Log("performing update for %s", name) - res, err := u.performUpgrade(currentRelease, upgradedRelease) + res, err := u.performUpgrade(ctx, currentRelease, upgradedRelease) if err != nil { return res, err } @@ -254,7 +257,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin return currentRelease, upgradedRelease, err } -func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Release) (*release.Release, error) { +func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedRelease *release.Release) (*release.Release, error) { current, err := u.cfg.KubeClient.Build(bytes.NewBufferString(originalRelease.Manifest), false) if err != nil { // Checking for removed Kubernetes API error so can provide a more informative error message to the user @@ -319,7 +322,7 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea } rChan := make(chan resultMessage) go u.releasingUpgrade(rChan, upgradedRelease, current, target, originalRelease) - go u.handleSignals(rChan, upgradedRelease) + go u.handleContext(ctx, rChan, upgradedRelease) result := <-rChan return result.r, result.e @@ -338,14 +341,13 @@ func (u *Upgrade) reportToPerformUpgrade(c chan<- resultMessage, rel *release.Re } // Setup listener for SIGINT and SIGTERM -func (u *Upgrade) handleSignals(c chan<- resultMessage, upgradedRelease *release.Release) { - cSignal := make(chan os.Signal) - signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) +func (u *Upgrade) handleContext(ctx context.Context, c chan<- resultMessage, upgradedRelease *release.Release) { + go func() { - <-cSignal - u.cfg.Log("SIGTERM or SIGINT received") + <-ctx.Done() + err := ctx.Err() // when the atomic flag is set the ongoing release finish first and doesn't give time for the rollback happens. - u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, fmt.Errorf("SIGTERM or SIGINT received, release failed")) + u.reportToPerformUpgrade(c, upgradedRelease, kube.ResourceList{}, err) }() } func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *release.Release, current kube.ResourceList, target kube.ResourceList, originalRelease *release.Release) { diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 2aad26573..7c286093e 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -17,10 +17,8 @@ limitations under the License. package action import ( + "context" "fmt" - "io/ioutil" - "os" - "os/exec" "testing" "time" @@ -302,132 +300,64 @@ func TestUpgradeRelease_Pending(t *testing.T) { } func TestUpgradeRelease_Interrupted_Wait(t *testing.T) { - if os.Getenv("HANDLE_SIGINT") == "1" { - t.Run("Execute TestUpgradeRelease_Interrupted_Wait", func(t *testing.T) { - is := assert.New(t) - req := require.New(t) - - upAction := upgradeAction(t) - rel := releaseStub() - rel.Name = "interrupted-release" - rel.Info.Status = release.StatusDeployed - upAction.cfg.Releases.Create(rel) - - failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitDuration = 10 * time.Second - upAction.cfg.KubeClient = failer - upAction.Wait = true - vals := map[string]interface{}{} - res, err := upAction.Run(rel.Name, buildChart(), vals) - - req.Error(err) - is.Contains(res.Info.Description, "Upgrade \"interrupted-release\" failed: SIGTERM or SIGINT received, release failed") - is.Equal(res.Info.Status, release.StatusFailed) - }) - return - - } - t.Run("Setup TestUpgradeRelease_Interrupted_Wait", func(t *testing.T) { - cmd := exec.Command(os.Args[0], "-test.run=TestUpgradeRelease_Interrupted_Wait") - cmd.Env = append(os.Environ(), "HANDLE_SIGINT=1") - stdout, err := cmd.StdoutPipe() - if err != nil { - t.Fatal(err) - } - stderr, err := cmd.StderrPipe() - if err != nil { - t.Fatal(err) - } - if err := cmd.Start(); err != nil { - t.Fatal(err) - } - go func() { - slurp, _ := ioutil.ReadAll(stdout) - fmt.Printf("%s\n", slurp) - }() - go func() { - slurp, _ := ioutil.ReadAll(stderr) - fmt.Printf("%s\n", slurp) - }() + is := assert.New(t) + req := require.New(t) - time.Sleep(2 * time.Second) - p, _ := os.FindProcess(cmd.Process.Pid) + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "interrupted-release" + rel.Info.Status = release.StatusDeployed + upAction.cfg.Releases.Create(rel) - if err := p.Signal(os.Interrupt); err != nil { - t.Fatal(err) - } + failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitDuration = 10 * time.Second + upAction.cfg.KubeClient = failer + upAction.Wait = true + vals := map[string]interface{}{} + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + time.AfterFunc(time.Second, cancel) + + res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) + + req.Error(err) + is.Contains(res.Info.Description, "Upgrade \"interrupted-release\" failed: context canceled") + is.Equal(res.Info.Status, release.StatusFailed) - if err := cmd.Wait(); err != nil { - t.FailNow() - } - }) } func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) { - if os.Getenv("HANDLE_SIGINT") == "1" { - t.Run("Execute TestUpgradeRelease_Interrupted_Atomic", func(t *testing.T) { - is := assert.New(t) - req := require.New(t) - - upAction := upgradeAction(t) - rel := releaseStub() - rel.Name = "interrupted-release" - rel.Info.Status = release.StatusDeployed - upAction.cfg.Releases.Create(rel) - - failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitDuration = 5 * time.Second - upAction.cfg.KubeClient = failer - upAction.Atomic = true - vals := map[string]interface{}{} - res, err := upAction.Run(rel.Name, buildChart(), vals) - - req.Error(err) - is.Contains(err.Error(), "release interrupted-release failed, and has been rolled back due to atomic being set: SIGTERM or SIGINT received, release failed") - - // Now make sure it is actually upgraded - updatedRes, err := upAction.cfg.Releases.Get(res.Name, 3) - is.NoError(err) - // Should have rolled back to the previous - is.Equal(updatedRes.Info.Status, release.StatusDeployed) - }) - return - - } - t.Run("Setup TestUpgradeRelease_Interrupted_Atomic", func(t *testing.T) { - cmd := exec.Command(os.Args[0], "-test.run=TestUpgradeRelease_Interrupted_Atomic") - cmd.Env = append(os.Environ(), "HANDLE_SIGINT=1") - stdout, err := cmd.StdoutPipe() - if err != nil { - t.Fatal(err) - } - stderr, err := cmd.StderrPipe() - if err != nil { - t.Fatal(err) - } - if err := cmd.Start(); err != nil { - t.Fatal(err) - } - go func() { - slurp, _ := ioutil.ReadAll(stdout) - fmt.Printf("%s\n", slurp) - }() - go func() { - slurp, _ := ioutil.ReadAll(stderr) - fmt.Printf("%s\n", slurp) - }() + is := assert.New(t) + req := require.New(t) - time.Sleep(2 * time.Second) - p, _ := os.FindProcess(cmd.Process.Pid) + upAction := upgradeAction(t) + rel := releaseStub() + rel.Name = "interrupted-release" + rel.Info.Status = release.StatusDeployed + upAction.cfg.Releases.Create(rel) - if err := p.Signal(os.Interrupt); err != nil { - t.Fatal(err) - } + failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) + failer.WaitDuration = 5 * time.Second + upAction.cfg.KubeClient = failer + upAction.Atomic = true + vals := map[string]interface{}{} + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + time.AfterFunc(time.Second, cancel) + + res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) + + req.Error(err) + is.Contains(err.Error(), "release interrupted-release failed, and has been rolled back due to atomic being set: context canceled") + + // Now make sure it is actually upgraded + updatedRes, err := upAction.cfg.Releases.Get(res.Name, 3) + is.NoError(err) + // Should have rolled back to the previous + is.Equal(updatedRes.Info.Status, release.StatusDeployed) - if err := cmd.Wait(); err != nil { - t.FailNow() - } - }) } From 4bc901c95f3589b6d6b4b3bc0843347334cd0f68 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Fri, 6 Aug 2021 17:54:00 +0100 Subject: [PATCH 11/12] Resolve PR comments Fix typos Remove condition arround time.Sleep Because a negative or zero duration causes Sleep to return immediately. Signed-off-by: Stephane Moser --- cmd/helm/install.go | 2 +- cmd/helm/upgrade.go | 2 +- pkg/kube/fake/fake.go | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/cmd/helm/install.go b/cmd/helm/install.go index f72c3d388..00bae1373 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -254,7 +254,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) go func() { <-cSignal - fmt.Fprintf(out, "Release %s has been cancel.\n", args[0]) + fmt.Fprintf(out, "Release %s has been cancelled.\n", args[0]) cancel() }() diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 862b6e341..28f8f9b3e 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -187,7 +187,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM) go func() { <-cSignal - fmt.Fprintf(out, "Release %s has been cancel.\n", args[0]) + fmt.Fprintf(out, "Release %s has been cancelled.\n", args[0]) cancel() }() diff --git a/pkg/kube/fake/fake.go b/pkg/kube/fake/fake.go index 1aa390511..00d88f75b 100644 --- a/pkg/kube/fake/fake.go +++ b/pkg/kube/fake/fake.go @@ -53,9 +53,7 @@ func (f *FailingKubeClient) Create(resources kube.ResourceList) (*kube.Result, e // Waits the amount of time defined on f.WaitDuration, then returns the configured error if set or prints. func (f *FailingKubeClient) Wait(resources kube.ResourceList, d time.Duration) error { - if f.WaitDuration != 0 { - time.Sleep(f.WaitDuration) - } + time.Sleep(f.WaitDuration) if f.WaitError != nil { return f.WaitError } From 101370af3dfe804a5bdb422adcf9e170dab097bc Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Fri, 27 Aug 2021 01:50:24 +0100 Subject: [PATCH 12/12] Wrap error To make the install comand consistent with upgrade comand when handling errors Signed-off-by: Stephane Moser --- cmd/helm/install.go | 2 +- cmd/helm/install_test.go | 2 +- cmd/helm/template_test.go | 2 +- cmd/helm/testdata/output/install-chart-bad-type.txt | 2 +- cmd/helm/testdata/output/install-lib-chart.txt | 1 + cmd/helm/testdata/output/schema-negative-cli.txt | 2 +- cmd/helm/testdata/output/schema-negative.txt | 2 +- cmd/helm/testdata/output/subchart-schema-cli-negative.txt | 2 +- cmd/helm/testdata/output/subchart-schema-negative.txt | 2 +- cmd/helm/testdata/output/template-chart-bad-type.txt | 1 + 10 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 cmd/helm/testdata/output/install-lib-chart.txt create mode 100644 cmd/helm/testdata/output/template-chart-bad-type.txt diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 00bae1373..666e56b0b 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -124,7 +124,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { RunE: func(_ *cobra.Command, args []string) error { rel, err := runInstall(args, client, valueOpts, out) if err != nil { - return err + return errors.Wrap(err, "INSTALLATION FAILED") } return outfmt.Write(out, &statusPrinter{rel, settings.Debug, false}) diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index b7349e3d5..ff025b809 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -169,7 +169,7 @@ func TestInstall(t *testing.T) { name: "install library chart", cmd: "install libchart testdata/testcharts/lib-chart", wantError: true, - golden: "output/template-lib-chart.txt", + golden: "output/install-lib-chart.txt", }, // Install, chart with bad type { diff --git a/cmd/helm/template_test.go b/cmd/helm/template_test.go index 30ee2722b..8fb8292e2 100644 --- a/cmd/helm/template_test.go +++ b/cmd/helm/template_test.go @@ -62,7 +62,7 @@ func TestTemplateCmd(t *testing.T) { name: "check chart bad type", cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/chart-bad-type"), wantError: true, - golden: "output/install-chart-bad-type.txt", + golden: "output/template-chart-bad-type.txt", }, { name: "check chart with dependency which is an app chart acting as a library chart", diff --git a/cmd/helm/testdata/output/install-chart-bad-type.txt b/cmd/helm/testdata/output/install-chart-bad-type.txt index d8a3bf275..c482a793d 100644 --- a/cmd/helm/testdata/output/install-chart-bad-type.txt +++ b/cmd/helm/testdata/output/install-chart-bad-type.txt @@ -1 +1 @@ -Error: validation: chart.metadata.type must be application or library +Error: INSTALLATION FAILED: validation: chart.metadata.type must be application or library diff --git a/cmd/helm/testdata/output/install-lib-chart.txt b/cmd/helm/testdata/output/install-lib-chart.txt new file mode 100644 index 000000000..c482a793d --- /dev/null +++ b/cmd/helm/testdata/output/install-lib-chart.txt @@ -0,0 +1 @@ +Error: INSTALLATION FAILED: validation: chart.metadata.type must be application or library diff --git a/cmd/helm/testdata/output/schema-negative-cli.txt b/cmd/helm/testdata/output/schema-negative-cli.txt index d6f096e14..c4a5cc516 100644 --- a/cmd/helm/testdata/output/schema-negative-cli.txt +++ b/cmd/helm/testdata/output/schema-negative-cli.txt @@ -1,4 +1,4 @@ -Error: values don't meet the specifications of the schema(s) in the following chart(s): +Error: INSTALLATION FAILED: values don't meet the specifications of the schema(s) in the following chart(s): empty: - age: Must be greater than or equal to 0 diff --git a/cmd/helm/testdata/output/schema-negative.txt b/cmd/helm/testdata/output/schema-negative.txt index f7c89dd56..929af5518 100644 --- a/cmd/helm/testdata/output/schema-negative.txt +++ b/cmd/helm/testdata/output/schema-negative.txt @@ -1,4 +1,4 @@ -Error: values don't meet the specifications of the schema(s) in the following chart(s): +Error: INSTALLATION FAILED: values don't meet the specifications of the schema(s) in the following chart(s): empty: - (root): employmentInfo is required - age: Must be greater than or equal to 0 diff --git a/cmd/helm/testdata/output/subchart-schema-cli-negative.txt b/cmd/helm/testdata/output/subchart-schema-cli-negative.txt index c0883a8e8..7396b4bfe 100644 --- a/cmd/helm/testdata/output/subchart-schema-cli-negative.txt +++ b/cmd/helm/testdata/output/subchart-schema-cli-negative.txt @@ -1,4 +1,4 @@ -Error: values don't meet the specifications of the schema(s) in the following chart(s): +Error: INSTALLATION FAILED: values don't meet the specifications of the schema(s) in the following chart(s): subchart-with-schema: - age: Must be greater than or equal to 0 diff --git a/cmd/helm/testdata/output/subchart-schema-negative.txt b/cmd/helm/testdata/output/subchart-schema-negative.txt index 5a84170fd..7b1f654a2 100644 --- a/cmd/helm/testdata/output/subchart-schema-negative.txt +++ b/cmd/helm/testdata/output/subchart-schema-negative.txt @@ -1,4 +1,4 @@ -Error: values don't meet the specifications of the schema(s) in the following chart(s): +Error: INSTALLATION FAILED: values don't meet the specifications of the schema(s) in the following chart(s): chart-without-schema: - (root): lastname is required subchart-with-schema: diff --git a/cmd/helm/testdata/output/template-chart-bad-type.txt b/cmd/helm/testdata/output/template-chart-bad-type.txt new file mode 100644 index 000000000..d8a3bf275 --- /dev/null +++ b/cmd/helm/testdata/output/template-chart-bad-type.txt @@ -0,0 +1 @@ +Error: validation: chart.metadata.type must be application or library