Files
helm/pkg/cmd/release_testing.go
Zhaofeng Miao 3bcf965f99 perf: enable concurrent status computation to prevent multi-minute delays (#32043)
* fix: enable concurrent status computation to prevent multi-minute delays

Set StatusComputeWorkers=8 on DefaultStatusWatcher for Wait,
WaitWithJobs, and WatchUntilReady. This opts in to the async status
computation added in fluxcd/cli-utils#20, preventing the informer
notification pipeline from being blocked by slow API calls when many
resources are updated simultaneously.

Without this, status computation for resources like Deployments (which
require additional LIST ReplicaSets/Pods calls) runs serially inside
the informer, causing growing delays of 1-3+ minutes when upgrading
many resources at once (e.g., ~20 Deployments via Helm).

Signed-off-by: maplemiao <maplemiao@tencent.com>

* refactor(kube): expose StatusComputeWorkers as a WaitOption

Replace the package-level DefaultStatusComputeWorkers variable with a
WithStatusComputeWorkers WaitOption threaded through waitOptions into
the statusWaiter. This removes global mutable state from pkg/kube and
lets callers opt in explicitly.

SDK consumers (e.g. helm-controller) inherit the zero value, which
preserves the upstream cli-utils synchronous behavior and avoids an
unexpected fan-out of status-compute goroutines when many releases
reconcile concurrently. The Helm CLI continues to enable 8 workers by
default via a shared pkg/cmd/flags.go helper, so install/upgrade/
rollback/uninstall/test users still get the fix for multi-minute
informer stalls out of the box.

Signed-off-by: maplemiao <maplemiao@tencent.com>

* refactor(kube): clamp negative StatusComputeWorkers values to zero

Address review feedback: when a caller passes a negative value to
WithStatusComputeWorkers, coerce it to zero rather than propagating
it to the underlying cli-utils watcher, where the behavior is
undefined. Zero is the safe default and matches the SDK opt-in
contract.

Signed-off-by: maplemiao <maplemiao@tencent.com>

---------

Signed-off-by: maplemiao <maplemiao@tencent.com>
Signed-off-by: Maple Miao <maplemiao@yunzhanghu.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 07:10:36 -06:00

119 lines
3.6 KiB
Go

/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"errors"
"fmt"
"io"
"regexp"
"strings"
"time"
"github.com/spf13/cobra"
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/cli/output"
"helm.sh/helm/v4/pkg/cmd/require"
)
const releaseTestHelp = `
The test command runs the tests for a release.
The argument this command takes is the name of a deployed release.
The tests to be run are defined in the chart that was installed.
`
func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client := action.NewReleaseTesting(cfg)
client.WaitOptions = append(client.WaitOptions, defaultCLIWaitOptions()...)
outfmt := output.Table
var outputLogs bool
var filter []string
cmd := &cobra.Command{
Use: "test [RELEASE]",
Short: "run tests for a release",
Long: releaseTestHelp,
Args: require.ExactArgs(1),
ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return noMoreArgsComp()
}
return compListReleases(toComplete, args, cfg)
},
RunE: func(_ *cobra.Command, args []string) (returnError error) {
client.Namespace = settings.Namespace()
notName := regexp.MustCompile(`^!\s?name=`)
for _, f := range filter {
if after, ok := strings.CutPrefix(f, "name="); ok {
client.Filters[action.IncludeNameFilter] = append(client.Filters[action.IncludeNameFilter], after)
} else if notName.MatchString(f) {
client.Filters[action.ExcludeNameFilter] = append(client.Filters[action.ExcludeNameFilter], notName.ReplaceAllLiteralString(f, ""))
}
}
reli, shutdown, runErr := client.Run(args[0])
defer func() {
if shutdownErr := shutdown(); shutdownErr != nil {
if returnError == nil {
returnError = shutdownErr
}
}
}()
// We only return an error if we weren't even able to get the
// release, otherwise we keep going so we can print status and logs
// if requested
if runErr != nil && reli == nil {
return runErr
}
rel, err := releaserToV1Release(reli)
if err != nil {
return err
}
if err := outfmt.Write(out, &statusPrinter{
release: rel,
debug: settings.Debug,
showMetadata: false,
hideNotes: true,
noColor: settings.ShouldDisableColor(),
}); err != nil {
return err
}
if outputLogs {
// Print a newline to stdout to separate the output
fmt.Fprintln(out)
if err := client.GetPodLogs(out, rel); err != nil {
return errors.Join(runErr, err)
}
}
return runErr
},
}
f := cmd.Flags()
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&outputLogs, "logs", false, "dump the logs from test pods (this runs after all tests are complete, but before any cleanup)")
f.StringSliceVar(&filter, "filter", []string{}, "specify tests by attribute (currently \"name\") using attribute=value syntax or '!attribute=value' to exclude a test (can specify multiple or separate values with commas: name=test1,name=test2)")
return cmd
}