pull/32549/merge
Karthik Chowdary 18 hours ago committed by GitHub
commit a5fd7670b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -100,12 +100,14 @@ type Install struct {
Devel bool Devel bool
DependencyUpdate bool DependencyUpdate bool
Timeout time.Duration Timeout time.Duration
Namespace string // waitProgress is called immediately before waiting for resources.
ReleaseName string waitProgress func(time.Duration)
GenerateName bool Namespace string
NameTemplate string ReleaseName string
Description string GenerateName bool
OutputDir string NameTemplate string
Description string
OutputDir string
// RollbackOnFailure enables rolling back (uninstalling) the release on failure if set // RollbackOnFailure enables rolling back (uninstalling) the release on failure if set
RollbackOnFailure bool RollbackOnFailure bool
SkipCRDs bool SkipCRDs bool
@ -182,6 +184,11 @@ func (i *Install) GetRegistryClient() *registry.Client {
return i.registryClient return i.registryClient
} }
// SetWaitProgress configures a callback invoked immediately before waiting for resources.
func (i *Install) SetWaitProgress(waitProgress func(time.Duration)) {
i.waitProgress = waitProgress
}
func (i *Install) installCRDs(crds []chart.CRD) error { func (i *Install) installCRDs(crds []chart.CRD) error {
// We do these one file at a time in the order they were read. // We do these one file at a time in the order they were read.
totalItems := []*resource.Info{} totalItems := []*resource.Info{}
@ -543,6 +550,10 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted, resources ku
return rel, fmt.Errorf("failed to get waiter: %w", err) return rel, fmt.Errorf("failed to get waiter: %w", err)
} }
if i.waitProgress != nil {
i.waitProgress(i.Timeout)
}
if i.WaitForJobs { if i.WaitForJobs {
err = waiter.WaitWithJobs(resources, i.Timeout) err = waiter.WaitWithJobs(resources, i.Timeout)
} else { } else {

@ -618,6 +618,11 @@ func TestInstallRelease_Wait(t *testing.T) {
failer.WaitError = errors.New("I timed out") failer.WaitError = errors.New("I timed out")
instAction.cfg.KubeClient = failer instAction.cfg.KubeClient = failer
instAction.WaitStrategy = kube.StatusWatcherStrategy instAction.WaitStrategy = kube.StatusWatcherStrategy
progressCalls := 0
instAction.SetWaitProgress(func(timeout time.Duration) {
assert.Equal(t, instAction.Timeout, timeout)
progressCalls++
})
vals := map[string]any{} vals := map[string]any{}
goroutines := instAction.getGoroutineCount() goroutines := instAction.getGoroutineCount()
@ -628,6 +633,7 @@ func TestInstallRelease_Wait(t *testing.T) {
req.NoError(err) req.NoError(err)
is.Contains(res.Info.Description, "I timed out") is.Contains(res.Info.Description, "I timed out")
is.Equal(rcommon.StatusFailed, res.Info.Status) is.Equal(rcommon.StatusFailed, res.Info.Status)
is.Equal(1, progressCalls)
is.Equal(goroutines, instAction.getGoroutineCount()) is.Equal(goroutines, instAction.getGoroutineCount())
} }

@ -76,6 +76,8 @@ type Upgrade struct {
WaitOptions []kube.WaitOption WaitOptions []kube.WaitOption
// WaitForJobs determines whether the wait operation for the Jobs should be performed after the upgrade is requested. // WaitForJobs determines whether the wait operation for the Jobs should be performed after the upgrade is requested.
WaitForJobs bool WaitForJobs bool
// waitProgress is called immediately before waiting for resources.
waitProgress func(time.Duration)
// DisableHooks disables hook processing if set to true. // DisableHooks disables hook processing if set to true.
DisableHooks bool DisableHooks bool
// DryRunStrategy can be set to prepare, but not execute the operation and whether or not to interact with the remote cluster // DryRunStrategy can be set to prepare, but not execute the operation and whether or not to interact with the remote cluster
@ -160,6 +162,11 @@ func (u *Upgrade) SetRegistryClient(client *registry.Client) {
u.registryClient = client u.registryClient = client
} }
// SetWaitProgress configures a callback invoked immediately before waiting for resources.
func (u *Upgrade) SetWaitProgress(waitProgress func(time.Duration)) {
u.waitProgress = waitProgress
}
// Run executes the upgrade on the given release. // Run executes the upgrade on the given release.
func (u *Upgrade) Run(name string, chart chart.Charter, vals map[string]any) (ri.Releaser, error) { func (u *Upgrade) Run(name string, chart chart.Charter, vals map[string]any) (ri.Releaser, error) {
ctx := context.Background() ctx := context.Background()
@ -489,6 +496,10 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele
u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err)
return return
} }
if u.waitProgress != nil {
u.waitProgress(u.Timeout)
}
if u.WaitForJobs { if u.WaitForJobs {
if err := waiter.WaitWithJobs(target, u.Timeout); err != nil { if err := waiter.WaitWithJobs(target, u.Timeout); err != nil {
u.cfg.recordRelease(originalRelease) u.cfg.recordRelease(originalRelease)

@ -94,6 +94,11 @@ func TestUpgradeRelease_Wait(t *testing.T) {
failer.WaitError = errors.New("I timed out") failer.WaitError = errors.New("I timed out")
upAction.cfg.KubeClient = failer upAction.cfg.KubeClient = failer
upAction.WaitStrategy = kube.StatusWatcherStrategy upAction.WaitStrategy = kube.StatusWatcherStrategy
progressCalls := 0
upAction.SetWaitProgress(func(timeout time.Duration) {
assert.Equal(t, upAction.Timeout, timeout)
progressCalls++
})
vals := map[string]any{} vals := map[string]any{}
resi, err := upAction.Run(rel.Name, buildChart(), vals) resi, err := upAction.Run(rel.Name, buildChart(), vals)
@ -102,6 +107,7 @@ func TestUpgradeRelease_Wait(t *testing.T) {
req.NoError(err) req.NoError(err)
is.Contains(res.Info.Description, "I timed out") is.Contains(res.Info.Description, "I timed out")
is.Equal(common.StatusFailed, res.Info.Status) is.Equal(common.StatusFailed, res.Info.Status)
is.Equal(1, progressCalls)
} }
func TestUpgradeRelease_WaitForJobs(t *testing.T) { func TestUpgradeRelease_WaitForJobs(t *testing.T) {

@ -38,6 +38,7 @@ import (
"helm.sh/helm/v4/pkg/cmd/require" "helm.sh/helm/v4/pkg/cmd/require"
"helm.sh/helm/v4/pkg/downloader" "helm.sh/helm/v4/pkg/downloader"
"helm.sh/helm/v4/pkg/getter" "helm.sh/helm/v4/pkg/getter"
"helm.sh/helm/v4/pkg/kube"
release "helm.sh/helm/v4/pkg/release/v1" release "helm.sh/helm/v4/pkg/release/v1"
) )
@ -157,6 +158,8 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
} }
client.DryRunStrategy = dryRunStrategy client.DryRunStrategy = dryRunStrategy
configureWaitProgress(client, out, outfmt)
rel, err := runInstall(args, client, valueOpts, out) rel, err := runInstall(args, client, valueOpts, out)
if err != nil { if err != nil {
return fmt.Errorf("INSTALLATION FAILED: %w", err) return fmt.Errorf("INSTALLATION FAILED: %w", err)
@ -370,6 +373,29 @@ func checkIfInstallable(ch chart.Accessor) error {
return fmt.Errorf("%s charts are not installable", meta["Type"]) return fmt.Errorf("%s charts are not installable", meta["Type"])
} }
func configureWaitProgress(client *action.Install, out io.Writer, outfmt output.Format) {
client.SetWaitProgress(nil)
if outfmt != output.Table || client.DryRunStrategy != action.DryRunNone {
return
}
if client.WaitStrategy == kube.HookOnlyStrategy && !client.RollbackOnFailure {
return
}
client.SetWaitProgress(func(timeout time.Duration) {
printWaitMessage(out, outfmt, kube.StatusWatcherStrategy, false, action.DryRunNone, timeout)
})
}
func printWaitMessage(out io.Writer, outfmt output.Format, strategy kube.WaitStrategy, rollbackOnFailure bool, dryRun action.DryRunStrategy, timeout time.Duration) {
if strategy == kube.HookOnlyStrategy && rollbackOnFailure {
strategy = kube.StatusWatcherStrategy
}
if outfmt != output.Table || strategy == kube.HookOnlyStrategy || dryRun != action.DryRunNone {
return
}
fmt.Fprintf(out, "Waiting for resources to become ready (timeout: %s)\n", timeout)
}
// Provide dynamic auto-completion for the install and template commands // Provide dynamic auto-completion for the install and template commands
func compInstall(args []string, toComplete string, client *action.Install) ([]string, cobra.ShellCompDirective) { func compInstall(args []string, toComplete string, client *action.Install) ([]string, cobra.ShellCompDirective) {
requiredArgs := 1 requiredArgs := 1

@ -17,14 +17,19 @@ limitations under the License.
package cmd package cmd
import ( import (
"bytes"
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"path/filepath" "path/filepath"
"testing" "testing"
"time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v4/pkg/kube"
"helm.sh/helm/v4/pkg/repo/v1/repotest" "helm.sh/helm/v4/pkg/repo/v1/repotest"
) )
@ -323,3 +328,56 @@ func TestInstallFileCompletion(t *testing.T) {
checkFileCompletion(t, "install myname", true) checkFileCompletion(t, "install myname", true)
checkFileCompletion(t, "install myname mychart", false) checkFileCompletion(t, "install myname mychart", false)
} }
func TestPrintWaitMessage(t *testing.T) {
const message = "Waiting for resources to become ready (timeout: 42s)\n"
tests := []struct {
name string
format output.Format
strategy kube.WaitStrategy
rollbackOnFailure bool
dryRun action.DryRunStrategy
want string
}{
{name: "explicit watcher wait", format: output.Table, strategy: kube.StatusWatcherStrategy, dryRun: action.DryRunNone, want: message},
{name: "implicit watcher wait", format: output.Table, strategy: kube.HookOnlyStrategy, rollbackOnFailure: true, dryRun: action.DryRunNone, want: message},
{name: "hook-only wait", format: output.Table, strategy: kube.HookOnlyStrategy, dryRun: action.DryRunNone},
{name: "structured output", format: output.JSON, strategy: kube.StatusWatcherStrategy, dryRun: action.DryRunNone},
{name: "client dry run", format: output.Table, strategy: kube.StatusWatcherStrategy, dryRun: action.DryRunClient},
{name: "server dry run", format: output.Table, strategy: kube.StatusWatcherStrategy, dryRun: action.DryRunServer},
{name: "implicit wait client dry run", format: output.Table, strategy: kube.HookOnlyStrategy, rollbackOnFailure: true, dryRun: action.DryRunClient},
{name: "implicit wait server dry run", format: output.Table, strategy: kube.HookOnlyStrategy, rollbackOnFailure: true, dryRun: action.DryRunServer},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var out bytes.Buffer
printWaitMessage(&out, tt.format, tt.strategy, tt.rollbackOnFailure, tt.dryRun, 42*time.Second)
require.Equal(t, tt.want, out.String())
})
}
}
func TestInstallImplicitWaitProgress(t *testing.T) {
for _, flag := range []string{"--rollback-on-failure", "--atomic"} {
t.Run(flag, func(t *testing.T) {
defer resetEnv()()
_, out, err := executeActionCommand("install implicit-wait testdata/testcharts/empty " + flag)
require.NoError(t, err)
require.Contains(t, out, "Waiting for resources to become ready (timeout: 5m0s)\n")
})
}
}
func TestInstallDryRunSuppressesWaitProgress(t *testing.T) {
for _, dryRun := range []string{"client", "server"} {
t.Run(dryRun, func(t *testing.T) {
defer resetEnv()()
_, out, err := executeActionCommand("install dry-run-wait testdata/testcharts/empty --rollback-on-failure --dry-run=" + dryRun)
require.NoError(t, err)
require.NotContains(t, out, "Waiting for resources to become ready")
})
}
}

@ -1,3 +1,4 @@
Waiting for resources to become ready (timeout: 5m0s)
NAME: apollo NAME: apollo
LAST DEPLOYED: Fri Sep 2 22:04:05 1977 LAST DEPLOYED: Fri Sep 2 22:04:05 1977
NAMESPACE: default NAMESPACE: default

@ -1,3 +1,4 @@
Waiting for resources to become ready (timeout: 5m0s)
NAME: apollo NAME: apollo
LAST DEPLOYED: Fri Sep 2 22:04:05 1977 LAST DEPLOYED: Fri Sep 2 22:04:05 1977
NAMESPACE: default NAMESPACE: default

@ -1,3 +1,4 @@
Waiting for resources to become ready (timeout: 5m0s)
Release "crazy-bunny" has been upgraded. Happy Helming! Release "crazy-bunny" has been upgraded. Happy Helming!
NAME: crazy-bunny NAME: crazy-bunny
LAST DEPLOYED: Fri Sep 2 22:04:05 1977 LAST DEPLOYED: Fri Sep 2 22:04:05 1977

@ -1,3 +1,4 @@
Waiting for resources to become ready (timeout: 5m0s)
Release "crazy-bunny" has been upgraded. Happy Helming! Release "crazy-bunny" has been upgraded. Happy Helming!
NAME: crazy-bunny NAME: crazy-bunny
LAST DEPLOYED: Fri Sep 2 22:04:05 1977 LAST DEPLOYED: Fri Sep 2 22:04:05 1977

@ -38,6 +38,7 @@ import (
"helm.sh/helm/v4/pkg/cmd/require" "helm.sh/helm/v4/pkg/cmd/require"
"helm.sh/helm/v4/pkg/downloader" "helm.sh/helm/v4/pkg/downloader"
"helm.sh/helm/v4/pkg/getter" "helm.sh/helm/v4/pkg/getter"
"helm.sh/helm/v4/pkg/kube"
ri "helm.sh/helm/v4/pkg/release" ri "helm.sh/helm/v4/pkg/release"
"helm.sh/helm/v4/pkg/release/common" "helm.sh/helm/v4/pkg/release/common"
"helm.sh/helm/v4/pkg/storage/driver" "helm.sh/helm/v4/pkg/storage/driver"
@ -82,6 +83,19 @@ which can contain sensitive values. To hide Kubernetes Secrets use the
--hide-secret flag. Please carefully consider how and when these flags are used. --hide-secret flag. Please carefully consider how and when these flags are used.
` `
func configureUpgradeWaitProgress(client *action.Upgrade, out io.Writer, outfmt output.Format) {
client.SetWaitProgress(nil)
if outfmt != output.Table || client.DryRunStrategy != action.DryRunNone {
return
}
if client.WaitStrategy == kube.HookOnlyStrategy && !client.RollbackOnFailure {
return
}
client.SetWaitProgress(func(timeout time.Duration) {
printWaitMessage(out, outfmt, kube.StatusWatcherStrategy, false, action.DryRunNone, timeout)
})
}
func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewUpgrade(cfg) client := action.NewUpgrade(cfg)
client.WaitOptions = append(client.WaitOptions, defaultCLIWaitOptions()...) client.WaitOptions = append(client.WaitOptions, defaultCLIWaitOptions()...)
@ -163,6 +177,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
instClient.Replace = true instClient.Replace = true
} }
configureWaitProgress(instClient, out, outfmt)
rel, err := runInstall(args, instClient, valueOpts, out) rel, err := runInstall(args, instClient, valueOpts, out)
if err != nil { if err != nil {
return err return err
@ -257,6 +273,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
cancel() cancel()
}() }()
configureUpgradeWaitProgress(client, out, outfmt)
rel, err := client.RunWithContext(ctx, args[0], ch, vals) rel, err := client.RunWithContext(ctx, args[0], ch, vals)
if err != nil { if err != nil {
return fmt.Errorf("UPGRADE FAILED: %w", err) return fmt.Errorf("UPGRADE FAILED: %w", err)

@ -587,3 +587,34 @@ func TestUpgradeInstallServerSideApply(t *testing.T) {
}) })
} }
} }
func TestUpgradeImplicitWaitProgress(t *testing.T) {
tests := []struct {
name string
install bool
}{
{name: "upgrade"},
{name: "upgrade install", install: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer resetEnv()()
releaseName := "implicit-wait"
relMock, ch, chartPath := prepareMockRelease(t, releaseName)
store := storageFixture()
installFlag := ""
if tt.install {
installFlag = "--install"
} else {
require.NoError(t, store.Create(relMock(releaseName, 1, ch)))
}
cmd := fmt.Sprintf("upgrade %s %s --rollback-on-failure '%s'", releaseName, installFlag, chartPath)
_, out, err := executeActionCommandC(store, cmd)
require.NoError(t, err)
assert.Contains(t, out, "Waiting for resources to become ready (timeout: 5m0s)\n")
})
}
}

Loading…
Cancel
Save