feat(hip-0025): add ordered wait flags and template output

Signed-off-by: Rohit Gudi <50377477+caretak3r@users.noreply.github.com>
pull/32038/head
Rohit Gudi 3 days ago
parent 95f0b469e9
commit fbf6249d28
No known key found for this signature in database
GPG Key ID: F9223D6BD3386ABB

@ -25,6 +25,7 @@ import (
"path/filepath"
"sort"
"strings"
"time"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
@ -65,42 +66,95 @@ func AddWaitFlag(cmd *cobra.Command, wait *kube.WaitStrategy) {
cmd.Flags().Lookup("wait").NoOptDefVal = string(kube.StatusWatcherStrategy)
}
type waitValue kube.WaitStrategy
func AddOrderedWaitFlag(cmd *cobra.Command, wait *kube.WaitStrategy) {
cmd.Flags().Var(
newOrderedWaitValue(kube.HookOnlyStrategy, wait),
"wait",
"wait until resources are ready (up to --timeout). Use '--wait' alone for 'watcher' strategy, or specify one of: 'watcher', 'hookOnly', 'legacy', 'ordered'. Default when flag is omitted: 'hookOnly'.",
)
cmd.Flags().Lookup("wait").NoOptDefVal = string(kube.StatusWatcherStrategy)
}
type waitValue struct {
wait *kube.WaitStrategy
allowOrdered bool
}
func newWaitValue(defaultValue kube.WaitStrategy, ws *kube.WaitStrategy) *waitValue {
return newConfiguredWaitValue(defaultValue, ws, false)
}
func newOrderedWaitValue(defaultValue kube.WaitStrategy, ws *kube.WaitStrategy) *waitValue {
return newConfiguredWaitValue(defaultValue, ws, true)
}
func newConfiguredWaitValue(defaultValue kube.WaitStrategy, ws *kube.WaitStrategy, allowOrdered bool) *waitValue {
*ws = defaultValue
return (*waitValue)(ws)
return &waitValue{wait: ws, allowOrdered: allowOrdered}
}
func (ws *waitValue) String() string {
if ws == nil {
if ws == nil || ws.wait == nil {
return ""
}
return string(*ws)
return string(*ws.wait)
}
func (ws *waitValue) Set(s string) error {
switch s {
case string(kube.StatusWatcherStrategy), string(kube.LegacyStrategy), string(kube.HookOnlyStrategy):
*ws = waitValue(s)
*ws.wait = kube.WaitStrategy(s)
return nil
case string(kube.OrderedWaitStrategy):
if !ws.allowOrdered {
break
}
*ws.wait = kube.WaitStrategy(s)
return nil
case "true":
slog.Warn("--wait=true is deprecated (boolean value) and can be replaced with --wait=watcher")
*ws = waitValue(kube.StatusWatcherStrategy)
*ws.wait = kube.StatusWatcherStrategy
return nil
case "false":
slog.Warn("--wait=false is deprecated (boolean value) and can be replaced with --wait=hookOnly")
*ws = waitValue(kube.HookOnlyStrategy)
*ws.wait = kube.HookOnlyStrategy
return nil
default:
return fmt.Errorf("invalid wait input %q. Valid inputs are %s, %s, and %s", s, kube.StatusWatcherStrategy, kube.HookOnlyStrategy, kube.LegacyStrategy)
}
return fmt.Errorf("invalid wait input %q. Valid inputs are %s", s, formatWaitInputs(ws.allowOrdered))
}
func (ws *waitValue) Type() string {
return "WaitStrategy"
}
func addReadinessTimeoutFlag(f *pflag.FlagSet, readinessTimeout *time.Duration) {
f.DurationVar(readinessTimeout, "readiness-timeout", time.Minute, "per-batch timeout when --wait=ordered is used; each resource batch must become ready within this duration (must not exceed --timeout)")
}
func formatWaitInputs(allowOrdered bool) string {
valid := []string{
string(kube.StatusWatcherStrategy),
string(kube.HookOnlyStrategy),
string(kube.LegacyStrategy),
}
if allowOrdered {
valid = append(valid, string(kube.OrderedWaitStrategy))
}
switch len(valid) {
case 0:
return ""
case 1:
return valid[0]
case 2:
return fmt.Sprintf("%s and %s", valid[0], valid[1])
default:
return fmt.Sprintf("%s, and %s", strings.Join(valid[:len(valid)-1], ", "), valid[len(valid)-1])
}
}
func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) {
f.StringVar(&c.Version, "version", "", "specify a version constraint for the chart version to use. This constraint can be a specific tag (e.g. 1.1.1) or it may reference a valid range (e.g. ^2.0.0). If this is not specified, the latest version is used")
f.BoolVar(&c.Verify, "verify", false, "verify the package before using it")

@ -173,6 +173,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f := cmd.Flags()
addInstallFlags(cmd, f, client, valueOpts)
addReadinessTimeoutFlag(f, &client.ReadinessTimeout)
// hide-secret is not available in all places the install flags are used so
// it is added separately
f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag")
@ -212,7 +213,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal
f.BoolVar(&client.TakeOwnership, "take-ownership", false, "if set, install will ignore the check for helm annotations and take ownership of the existing resources")
addValueOptionsFlags(f, valueOpts)
addChartPathOptionsFlags(f, &client.ChartPathOptions)
AddWaitFlag(cmd, &client.WaitStrategy)
AddOrderedWaitFlag(cmd, &client.WaitStrategy)
cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts")
cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts")

@ -22,7 +22,9 @@ import (
"fmt"
"io"
"io/fs"
"log/slog"
"os"
"path"
"path/filepath"
"regexp"
"slices"
@ -32,11 +34,16 @@ import (
release "helm.sh/helm/v4/pkg/release/v1"
"github.com/spf13/cobra"
"sigs.k8s.io/yaml"
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/chart/common"
chart "helm.sh/helm/v4/pkg/chart/v2"
"helm.sh/helm/v4/pkg/chart/v2/loader"
chartutil "helm.sh/helm/v4/pkg/chart/v2/util"
"helm.sh/helm/v4/pkg/cli/values"
"helm.sh/helm/v4/pkg/cmd/require"
"helm.sh/helm/v4/pkg/kube"
releaseutil "helm.sh/helm/v4/pkg/release/v1/util"
)
@ -105,6 +112,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client.Replace = true // Skip the name check
client.APIVersions = common.VersionSet(extraAPIs)
client.IncludeCRDs = includeCrds
orderedTemplateOutput := client.WaitStrategy == kube.OrderedWaitStrategy && len(showFiles) == 0 && client.OutputDir == ""
rel, err := runInstall(args, client, valueOpts, out)
if err != nil && !settings.Debug {
@ -117,84 +125,101 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
// We ignore a potential error here because, when the --debug flag was specified,
// we always want to print the YAML, even if it is not valid. The error is still returned afterwards.
if rel != nil {
var manifests bytes.Buffer
fmt.Fprintln(&manifests, strings.TrimSpace(rel.Manifest))
if !client.DisableHooks {
fileWritten := make(map[string]bool)
for _, m := range rel.Hooks {
if skipTests && isTestHook(m) {
continue
}
if client.OutputDir == "" {
fmt.Fprintf(&manifests, "---\n# Source: %s\n%s\n", m.Path, m.Manifest)
} else {
newDir := client.OutputDir
if client.UseReleaseName {
newDir = filepath.Join(client.OutputDir, client.ReleaseName)
if orderedTemplateOutput {
templateChart, err := loadTemplateChart(args, client)
if err != nil {
return err
}
if err := renderOrderedTemplate(templateChart, strings.TrimSpace(rel.Manifest), out); err != nil {
return err
}
if !client.DisableHooks {
for _, m := range rel.Hooks {
if skipTests && isTestHook(m) {
continue
}
_, err := os.Stat(filepath.Join(newDir, m.Path))
if err == nil {
fileWritten[m.Path] = true
fmt.Fprintf(out, "---\n# Source: %s\n%s\n", m.Path, m.Manifest)
}
}
} else {
var manifests bytes.Buffer
fmt.Fprintln(&manifests, strings.TrimSpace(rel.Manifest))
if !client.DisableHooks {
fileWritten := make(map[string]bool)
for _, m := range rel.Hooks {
if skipTests && isTestHook(m) {
continue
}
err = writeToFile(newDir, m.Path, m.Manifest, fileWritten[m.Path])
if err != nil {
return err
if client.OutputDir == "" {
fmt.Fprintf(&manifests, "---\n# Source: %s\n%s\n", m.Path, m.Manifest)
} else {
newDir := client.OutputDir
if client.UseReleaseName {
newDir = filepath.Join(client.OutputDir, client.ReleaseName)
}
_, err := os.Stat(filepath.Join(newDir, m.Path))
if err == nil {
fileWritten[m.Path] = true
}
err = writeToFile(newDir, m.Path, m.Manifest, fileWritten[m.Path])
if err != nil {
return err
}
}
}
}
}
// if we have a list of files to render, then check that each of the
// provided files exists in the chart.
if len(showFiles) > 0 {
// This is necessary to ensure consistent manifest ordering when using --show-only
// with globs or directory names.
splitManifests := releaseutil.SplitManifests(manifests.String())
manifestsKeys := make([]string, 0, len(splitManifests))
for k := range splitManifests {
manifestsKeys = append(manifestsKeys, k)
}
sort.Sort(releaseutil.BySplitManifestsOrder(manifestsKeys))
manifestNameRegex := regexp.MustCompile("# Source: [^/]+/(.+)")
var manifestsToRender []string
for _, f := range showFiles {
missing := true
// Use linux-style filepath separators to unify user's input path
f = filepath.ToSlash(f)
for _, manifestKey := range manifestsKeys {
manifest := splitManifests[manifestKey]
submatch := manifestNameRegex.FindStringSubmatch(manifest)
if len(submatch) == 0 {
continue
// if we have a list of files to render, then check that each of the
// provided files exists in the chart.
if len(showFiles) > 0 {
// This is necessary to ensure consistent manifest ordering when using --show-only
// with globs or directory names.
splitManifests := releaseutil.SplitManifests(manifests.String())
manifestsKeys := make([]string, 0, len(splitManifests))
for k := range splitManifests {
manifestsKeys = append(manifestsKeys, k)
}
sort.Sort(releaseutil.BySplitManifestsOrder(manifestsKeys))
manifestNameRegex := regexp.MustCompile("# Source: [^/]+/(.+)")
var manifestsToRender []string
for _, f := range showFiles {
missing := true
// Use linux-style filepath separators to unify user's input path
f = filepath.ToSlash(f)
for _, manifestKey := range manifestsKeys {
manifest := splitManifests[manifestKey]
submatch := manifestNameRegex.FindStringSubmatch(manifest)
if len(submatch) == 0 {
continue
}
manifestName := submatch[1]
// manifest.Name is rendered using linux-style filepath separators on Windows as
// well as macOS/linux.
manifestPathSplit := strings.Split(manifestName, "/")
// manifest.Path is connected using linux-style filepath separators on Windows as
// well as macOS/linux
manifestPath := strings.Join(manifestPathSplit, "/")
// if the filepath provided matches a manifest path in the
// chart, render that manifest
if matched, _ := filepath.Match(f, manifestPath); !matched {
continue
}
manifestsToRender = append(manifestsToRender, manifest)
missing = false
}
manifestName := submatch[1]
// manifest.Name is rendered using linux-style filepath separators on Windows as
// well as macOS/linux.
manifestPathSplit := strings.Split(manifestName, "/")
// manifest.Path is connected using linux-style filepath separators on Windows as
// well as macOS/linux
manifestPath := strings.Join(manifestPathSplit, "/")
// if the filepath provided matches a manifest path in the
// chart, render that manifest
if matched, _ := filepath.Match(f, manifestPath); !matched {
continue
if missing {
return fmt.Errorf("could not find template %s in chart", f)
}
manifestsToRender = append(manifestsToRender, manifest)
missing = false
}
if missing {
return fmt.Errorf("could not find template %s in chart", f)
for _, m := range manifestsToRender {
fmt.Fprintf(out, "---\n%s\n", m)
}
} else {
fmt.Fprintf(out, "%s", manifests.String())
}
for _, m := range manifestsToRender {
fmt.Fprintf(out, "---\n%s\n", m)
}
} else {
fmt.Fprintf(out, "%s", manifests.String())
}
}
@ -225,6 +250,244 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return cmd
}
func loadTemplateChart(args []string, client *action.Install) (*chart.Chart, error) {
_, chartRef, err := client.NameAndChart(args)
if err != nil {
return nil, err
}
chartPath, err := client.LocateChart(chartRef, settings)
if err != nil {
return nil, err
}
return loader.Load(chartPath)
}
func renderOrderedTemplate(chrt *chart.Chart, manifest string, out io.Writer) error {
if manifest == "" {
return nil
}
sortedManifests, err := parseTemplateManifests(manifest)
if err != nil {
return fmt.Errorf("parsing manifests for ordered output: %w", err)
}
return renderOrderedChartLevel(chrt, sortedManifests, chrt.Name(), out)
}
func parseTemplateManifests(manifest string) ([]releaseutil.Manifest, error) {
rawManifests := releaseutil.SplitManifests(manifest)
keys := make([]string, 0, len(rawManifests))
for key := range rawManifests {
keys = append(keys, key)
}
sort.Sort(releaseutil.BySplitManifestsOrder(keys))
manifests := make([]releaseutil.Manifest, 0, len(keys))
for _, key := range keys {
content := rawManifests[key]
name := manifestSourcePath(content)
if name == "" {
name = key
}
var head releaseutil.SimpleHead
if err := yaml.Unmarshal([]byte(content), &head); err != nil {
return nil, fmt.Errorf("YAML parse error on %s: %w", name, err)
}
manifests = append(manifests, releaseutil.Manifest{
Name: name,
Content: content,
Head: &head,
})
}
return manifests, nil
}
func manifestSourcePath(content string) string {
for _, line := range strings.Split(content, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "# Source: ") {
return strings.TrimPrefix(line, "# Source: ")
}
}
return ""
}
func renderOrderedChartLevel(chrt *chart.Chart, manifests []releaseutil.Manifest, chartPath string, out io.Writer) error {
if len(manifests) == 0 {
return nil
}
grouped := groupManifestsByChartPath(manifests, chartPath)
renderedSubcharts := make(map[string]struct{})
dag, err := chartutil.BuildSubchartDAG(chrt)
if err != nil {
return fmt.Errorf("building subchart DAG for %s: %w", chartPath, err)
}
batches, err := dag.GetBatches()
if err != nil {
return fmt.Errorf("getting subchart batches for %s: %w", chartPath, err)
}
for _, batch := range batches {
for _, subchartName := range batch {
if err := renderOrderedSubchart(chrt, chartPath, subchartName, grouped[subchartName], out); err != nil {
return err
}
renderedSubcharts[subchartName] = struct{}{}
}
}
var remainingSubcharts []string
for subchartName := range grouped {
if subchartName == "" {
continue
}
if _, ok := renderedSubcharts[subchartName]; ok {
continue
}
remainingSubcharts = append(remainingSubcharts, subchartName)
}
sort.Strings(remainingSubcharts)
for _, subchartName := range remainingSubcharts {
if err := renderOrderedSubchart(chrt, chartPath, subchartName, grouped[subchartName], out); err != nil {
return err
}
}
return renderOrderedResourceGroups(grouped[""], chartPath, out)
}
func renderOrderedSubchart(chrt *chart.Chart, chartPath, subchartName string, manifests []releaseutil.Manifest, out io.Writer) error {
if len(manifests) == 0 {
return nil
}
subchartPath := path.Join(chartPath, subchartName)
subChart := findTemplateSubchart(chrt, subchartName)
if subChart == nil {
slog.Warn("subchart not found in chart dependencies during template rendering; falling back to flat ordered output", "subchart", subchartName)
return renderOrderedResourceGroups(manifests, subchartPath, out)
}
return renderOrderedChartLevel(subChart, manifests, subchartPath, out)
}
func renderOrderedResourceGroups(manifests []releaseutil.Manifest, chartPath string, out io.Writer) error {
if len(manifests) == 0 {
return nil
}
result, warnings, err := releaseutil.ParseResourceGroups(manifests)
if err != nil {
return fmt.Errorf("parsing resource groups for %s: %w", chartPath, err)
}
for _, warning := range warnings {
slog.Warn("resource-group annotation warning during template rendering", "chartPath", chartPath, "warning", warning)
}
if len(result.Groups) > 0 {
dag, err := releaseutil.BuildResourceGroupDAG(result)
if err != nil {
return fmt.Errorf("building resource-group DAG for %s: %w", chartPath, err)
}
batches, err := dag.GetBatches()
if err != nil {
return fmt.Errorf("getting resource-group batches for %s: %w", chartPath, err)
}
for _, batch := range batches {
for _, groupName := range batch {
fmt.Fprintf(out, "## START resource-group: %s %s\n", chartPath, groupName)
for _, manifest := range result.Groups[groupName] {
fmt.Fprintf(out, "---\n%s\n", manifest.Content)
}
fmt.Fprintf(out, "## END resource-group: %s %s\n", chartPath, groupName)
}
}
}
for _, manifest := range result.Unsequenced {
fmt.Fprintf(out, "---\n%s\n", manifest.Content)
}
return nil
}
func groupManifestsByChartPath(manifests []releaseutil.Manifest, chartPath string) map[string][]releaseutil.Manifest {
result := make(map[string][]releaseutil.Manifest)
chartsPrefix := chartManifestPrefix(chartPath) + "/charts/"
for _, manifest := range manifests {
if !strings.HasPrefix(manifest.Name, chartsPrefix) {
result[""] = append(result[""], manifest)
continue
}
rest := manifest.Name[len(chartsPrefix):]
idx := strings.Index(rest, "/")
if idx < 0 {
result[""] = append(result[""], manifest)
continue
}
subchartName := rest[:idx]
result[subchartName] = append(result[subchartName], manifest)
}
return result
}
func chartManifestPrefix(chartPath string) string {
parts := strings.Split(chartPath, "/")
if len(parts) == 0 {
return chartPath
}
prefix := parts[0]
for _, part := range parts[1:] {
prefix = path.Join(prefix, "charts", part)
}
return prefix
}
func findTemplateSubchart(chrt *chart.Chart, nameOrAlias string) *chart.Chart {
if chrt == nil || chrt.Metadata == nil {
return nil
}
aliases := make(map[string]string, len(chrt.Metadata.Dependencies))
for _, dep := range chrt.Metadata.Dependencies {
effectiveName := dep.Name
if dep.Alias != "" {
effectiveName = dep.Alias
}
aliases[dep.Name] = effectiveName
}
for _, dep := range chrt.Dependencies() {
effectiveName := dep.Name()
if alias, ok := aliases[dep.Name()]; ok {
effectiveName = alias
}
if effectiveName == nameOrAlias || dep.Name() == nameOrAlias {
return dep
}
}
return nil
}
func isTestHook(h *release.Hook) bool {
return slices.Contains(h.Events, release.HookTest)
}

@ -305,7 +305,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
addValueOptionsFlags(f, valueOpts)
bindOutputFlag(cmd, &outfmt)
bindPostRenderFlag(cmd, &client.PostRenderer, settings)
AddWaitFlag(cmd, &client.WaitStrategy)
AddOrderedWaitFlag(cmd, &client.WaitStrategy)
addReadinessTimeoutFlag(f, &client.ReadinessTimeout)
cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts")
cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts")

Loading…
Cancel
Save