pull/30833/merge
George Jenkins 3 days ago committed by GitHub
commit 88ae00beec
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -70,6 +70,21 @@ var (
errPending = errors.New("another operation (install/upgrade/rollback) is in progress")
)
type DryRunStrategy string
const (
// DryRunNone indicates the client will make all mutating calls
DryRunNone DryRunStrategy = "none"
// DryRunClient, or client-side dry-run, indicates the client will avoid
// making calls to the server
DryRunClient DryRunStrategy = "client"
// DryRunServer, or server-side dry-run, indicates the client will send
// calls to the APIServer with the dry-run parameter to prevent persisting changes
DryRunServer DryRunStrategy = "server"
)
// Configuration injects the dependencies that all actions share.
type Configuration struct {
// RESTClientGetter is an interface that loads Kubernetes clients.
@ -528,3 +543,13 @@ func determineReleaseSSApplyMethod(serverSideApply bool) release.ApplyMethod {
}
return release.ApplyMethodClientSideApply
}
// isDryRun returns true if the strategy is set to run as a DryRun
func isDryRun(strategy DryRunStrategy) bool {
return strategy == DryRunClient || strategy == DryRunServer
}
// interactWithServer determine whether or not to interact with a remote Kubernetes server
func interactWithServer(strategy DryRunStrategy) bool {
return strategy == DryRunNone || strategy == DryRunServer
}

@ -951,3 +951,15 @@ func TestDetermineReleaseSSAApplyMethod(t *testing.T) {
assert.Equal(t, release.ApplyMethodClientSideApply, determineReleaseSSApplyMethod(false))
assert.Equal(t, release.ApplyMethodServerSideApply, determineReleaseSSApplyMethod(true))
}
func TestIsDryRun(t *testing.T) {
assert.False(t, isDryRun(DryRunNone))
assert.True(t, isDryRun(DryRunClient))
assert.True(t, isDryRun(DryRunServer))
}
func TestInteractWithServer(t *testing.T) {
assert.True(t, interactWithServer(DryRunNone))
assert.False(t, interactWithServer(DryRunClient))
assert.True(t, interactWithServer(DryRunServer))
}

@ -75,7 +75,6 @@ type Install struct {
ChartPathOptions
ClientOnly bool
// ForceReplace will, if set to `true`, ignore certain warnings and perform the install anyway.
//
// This should be used with caution.
@ -87,8 +86,8 @@ type Install struct {
// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/
ServerSideApply bool
CreateNamespace bool
DryRun bool
DryRunOption string
// DryRunStrategy can be set to prepare, but not execute the operation and whether or not to interact with the remote cluster
DryRunStrategy DryRunStrategy
// HideSecret can be set to true when DryRun is enabled in order to hide
// Kubernetes Secrets in the output. It cannot be used outside of DryRun.
HideSecret bool
@ -159,6 +158,7 @@ func NewInstall(cfg *Configuration) *Install {
in := &Install{
cfg: cfg,
ServerSideApply: true,
DryRunStrategy: DryRunNone,
}
in.registryClient = cfg.RegistryClient
@ -264,8 +264,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
return nil, errors.New("invalid chart apiVersion")
}
// Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`)
if !i.ClientOnly {
if interactWithServer(i.DryRunStrategy) {
if err := i.cfg.KubeClient.IsReachable(); err != nil {
slog.Error(fmt.Sprintf("cluster reachability check failed: %v", err))
return nil, fmt.Errorf("cluster reachability check failed: %w", err)
@ -273,7 +272,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
}
// HideSecret must be used with dry run. Otherwise, return an error.
if !i.isDryRun() && i.HideSecret {
if !isDryRun(i.DryRunStrategy) && i.HideSecret {
slog.Error("hiding Kubernetes secrets requires a dry-run mode")
return nil, errors.New("hiding Kubernetes secrets requires a dry-run mode")
}
@ -288,23 +287,18 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
return nil, fmt.Errorf("chart dependencies processing failed: %w", err)
}
var interactWithRemote bool
if !i.isDryRun() || i.DryRunOption == "server" || i.DryRunOption == "none" || i.DryRunOption == "false" {
interactWithRemote = true
}
// Pre-install anything in the crd/ directory. We do this before Helm
// contacts the upstream server and builds the capabilities object.
if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 {
if crds := chrt.CRDObjects(); interactWithServer(i.DryRunStrategy) && !i.SkipCRDs && len(crds) > 0 {
// On dry run, bail here
if i.isDryRun() {
if isDryRun(i.DryRunStrategy) {
slog.Warn("This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.")
} else if err := i.installCRDs(crds); err != nil {
return nil, err
}
}
if i.ClientOnly {
if !interactWithServer(i.DryRunStrategy) {
// Add mock objects in here so it doesn't use Kube API server
// NOTE(bacongobbler): used for `helm template`
i.cfg.Capabilities = common.DefaultCapabilities.Copy()
@ -317,7 +311,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
mem := driver.NewMemory()
mem.SetNamespace(i.Namespace)
i.cfg.Releases = storage.Init(mem)
} else if !i.ClientOnly && len(i.APIVersions) > 0 {
} else if interactWithServer(i.DryRunStrategy) && len(i.APIVersions) > 0 {
slog.Debug("API Version list given outside of client only mode, this list will be ignored")
}
@ -333,7 +327,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
}
// special case for helm template --is-upgrade
isUpgrade := i.IsUpgrade && i.isDryRun()
isUpgrade := i.IsUpgrade && isDryRun(i.DryRunStrategy)
options := common.ReleaseOptions{
Name: i.ReleaseName,
Namespace: i.Namespace,
@ -353,7 +347,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
rel := i.createRelease(chrt, vals, i.Labels)
var manifestDoc *bytes.Buffer
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithRemote, i.EnableDNS, i.HideSecret)
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret)
// Even for errors, attach this if available
if manifestDoc != nil {
rel.Manifest = manifestDoc.String()
@ -386,7 +380,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
// we'll end up in a state where we will delete those resources upon
// deleting the release because the manifest will be pointing at that
// resource
if !i.ClientOnly && !isUpgrade && len(resources) > 0 {
if interactWithServer(i.DryRunStrategy) && !isUpgrade && len(resources) > 0 {
if i.TakeOwnership {
toBeAdopted, err = requireAdoption(resources)
} else {
@ -398,7 +392,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
}
// Bail out here if it is a dry run
if i.isDryRun() {
if isDryRun(i.DryRunStrategy) {
rel.Info.Description = "Dry run complete"
return rel, nil
}
@ -480,14 +474,6 @@ func (i *Install) getGoroutineCount() int32 {
return i.goroutineCount.Load()
}
// isDryRun returns true if Upgrade is set to run as a DryRun
func (i *Install) isDryRun() bool {
if i.DryRun || i.DryRunOption == "client" || i.DryRunOption == "server" || i.DryRunOption == "true" {
return true
}
return false
}
func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, error) {
var err error
// pre-install hooks
@ -590,7 +576,7 @@ func (i *Install) availableName() error {
return fmt.Errorf("release name %q: %w", start, err)
}
// On dry run, bail here
if i.isDryRun() {
if isDryRun(i.DryRunStrategy) {
return nil
}

@ -249,16 +249,6 @@ func TestInstallReleaseWithValues(t *testing.T) {
is.Equal(expectedUserValues, rel.Config)
}
func TestInstallReleaseClientOnly(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.ClientOnly = true
instAction.Run(buildChart(), nil) // disregard output
is.Equal(instAction.cfg.Capabilities, common.DefaultCapabilities)
is.Equal(instAction.cfg.KubeClient, &kubefake.PrintingKubeClient{Out: io.Discard})
}
func TestInstallRelease_NoName(t *testing.T) {
instAction := installAction(t)
instAction.ReleaseName = ""
@ -355,27 +345,30 @@ func TestInstallRelease_WithChartAndDependencyAllNotes(t *testing.T) {
is.Equal(rel.Info.Description, "Install complete")
}
func TestInstallRelease_DryRun(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.DryRun = true
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(withSampleTemplates()), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
func TestInstallRelease_DryRunClient(t *testing.T) {
for _, dryRunStrategy := range []DryRunStrategy{DryRunClient, DryRunServer} {
is := assert.New(t)
instAction := installAction(t)
instAction.DryRunStrategy = dryRunStrategy
is.Contains(res.Manifest, "---\n# Source: hello/templates/hello\nhello: world")
is.Contains(res.Manifest, "---\n# Source: hello/templates/goodbye\ngoodbye: world")
is.Contains(res.Manifest, "hello: Earth")
is.NotContains(res.Manifest, "hello: {{ template \"_planet\" . }}")
is.NotContains(res.Manifest, "empty")
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(withSampleTemplates()), vals)
if err != nil {
t.Fatalf("Failed install: %s", err)
}
_, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err)
is.Len(res.Hooks, 1)
is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "expect hook to not be marked as run")
is.Equal(res.Info.Description, "Dry run complete")
is.Contains(res.Manifest, "---\n# Source: hello/templates/hello\nhello: world")
is.Contains(res.Manifest, "---\n# Source: hello/templates/goodbye\ngoodbye: world")
is.Contains(res.Manifest, "hello: Earth")
is.NotContains(res.Manifest, "hello: {{ template \"_planet\" . }}")
is.NotContains(res.Manifest, "empty")
_, err = instAction.cfg.Releases.Get(res.Name, res.Version)
is.Error(err)
is.Len(res.Hooks, 1)
is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "expect hook to not be marked as run")
is.Equal(res.Info.Description, "Dry run complete")
}
}
func TestInstallRelease_DryRunHiddenSecret(t *testing.T) {
@ -383,7 +376,7 @@ func TestInstallRelease_DryRunHiddenSecret(t *testing.T) {
instAction := installAction(t)
// First perform a normal dry-run with the secret and confirm its presence.
instAction.DryRun = true
instAction.DryRunStrategy = DryRunClient
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals)
if err != nil {
@ -410,7 +403,7 @@ func TestInstallRelease_DryRunHiddenSecret(t *testing.T) {
is.Equal(res2.Info.Description, "Dry run complete")
// Ensure there is an error when HideSecret True but not in a dry-run mode
instAction.DryRun = false
instAction.DryRunStrategy = DryRunNone
vals = map[string]interface{}{}
_, err = instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals)
if err == nil {
@ -422,7 +415,7 @@ func TestInstallRelease_DryRunHiddenSecret(t *testing.T) {
func TestInstallRelease_DryRun_Lookup(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.DryRun = true
instAction.DryRunStrategy = DryRunNone
vals := map[string]interface{}{}
mockChart := buildChart(withSampleTemplates())
@ -442,7 +435,7 @@ func TestInstallRelease_DryRun_Lookup(t *testing.T) {
func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) {
is := assert.New(t)
instAction := installAction(t)
instAction.DryRun = true
instAction.DryRunStrategy = DryRunNone
vals := map[string]interface{}{}
_, err := instAction.Run(buildChart(withSampleIncludingIncorrectTemplates()), vals)
expectedErr := `hello/templates/incorrect:1:10
@ -1001,7 +994,6 @@ func TestInstallRun_UnreachableKubeClient(t *testing.T) {
config.KubeClient = &failingKubeClient
instAction := NewInstall(config)
instAction.ClientOnly = false
ctx, done := context.WithCancel(t.Context())
chrt := buildChart()
res, err := instAction.RunWithContext(ctx, chrt, nil)

@ -39,7 +39,8 @@ type Rollback struct {
WaitStrategy kube.WaitStrategy
WaitForJobs bool
DisableHooks bool
DryRun bool
// DryRunStrategy can be set to prepare, but not execute the operation and whether or not to interact with the remote cluster
DryRunStrategy DryRunStrategy
// ForceReplace will, if set to `true`, ignore certain warnings and perform the rollback anyway.
//
// This should be used with caution.
@ -59,7 +60,8 @@ type Rollback struct {
// NewRollback creates a new Rollback object with the given configuration.
func NewRollback(cfg *Configuration) *Rollback {
return &Rollback{
cfg: cfg,
cfg: cfg,
DryRunStrategy: DryRunNone,
}
}
@ -77,7 +79,7 @@ func (r *Rollback) Run(name string) error {
return err
}
if !r.DryRun {
if !isDryRun(r.DryRunStrategy) {
slog.Debug("creating rolled back release", "name", name)
if err := r.cfg.Releases.Create(targetRelease); err != nil {
return err
@ -89,7 +91,7 @@ func (r *Rollback) Run(name string) error {
return err
}
if !r.DryRun {
if !isDryRun(r.DryRunStrategy) {
slog.Debug("updating status for rolled back release", "name", name)
if err := r.cfg.Releases.Update(targetRelease); err != nil {
return err
@ -175,7 +177,7 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele
}
func (r *Rollback) performRollback(currentRelease, targetRelease *release.Release, serverSideApply bool) (*release.Release, error) {
if r.DryRun {
if isDryRun(r.DryRunStrategy) {
slog.Debug("dry run", "name", targetRelease.Name)
return targetRelease, nil
}

@ -73,10 +73,8 @@ type Upgrade struct {
WaitForJobs bool
// DisableHooks disables hook processing if set to true.
DisableHooks bool
// DryRun controls whether the operation is prepared, but not executed.
DryRun bool
// DryRunOption controls whether the operation is prepared, but not executed with options on whether or not to interact with the remote cluster.
DryRunOption string
// DryRunStrategy can be set to prepare, but not execute the operation and whether or not to interact with the remote cluster
DryRunStrategy DryRunStrategy
// HideSecret can be set to true when DryRun is enabled in order to hide
// Kubernetes Secrets in the output. It cannot be used outside of DryRun.
HideSecret bool
@ -140,6 +138,7 @@ func NewUpgrade(cfg *Configuration) *Upgrade {
up := &Upgrade{
cfg: cfg,
ServerSideApply: "auto",
DryRunStrategy: DryRunNone,
}
up.registryClient = cfg.RegistryClient
@ -198,7 +197,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Char
}
// Do not update for dry runs
if !u.isDryRun() {
if !isDryRun(u.DryRunStrategy) {
slog.Debug("updating status for upgraded release", "name", name)
if err := u.cfg.Releases.Update(upgradedRelease); err != nil {
return res, err
@ -208,14 +207,6 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Char
return res, nil
}
// isDryRun returns true if Upgrade is set to run as a DryRun
func (u *Upgrade) isDryRun() bool {
if u.DryRun || u.DryRunOption == "client" || u.DryRunOption == "server" || u.DryRunOption == "true" {
return true
}
return false
}
// prepareUpgrade builds an upgraded release for an upgrade operation.
func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[string]interface{}) (*release.Release, *release.Release, bool, error) {
if chart == nil {
@ -223,7 +214,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str
}
// HideSecret must be used with dry run. Otherwise, return an error.
if !u.isDryRun() && u.HideSecret {
if !isDryRun(u.DryRunStrategy) && u.HideSecret {
return nil, nil, false, errors.New("hiding Kubernetes secrets requires a dry-run mode")
}
@ -289,13 +280,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str
return nil, nil, false, err
}
// Determine whether or not to interact with remote
var interactWithRemote bool
if !u.isDryRun() || u.DryRunOption == "server" || u.DryRunOption == "none" || u.DryRunOption == "false" {
interactWithRemote = true
}
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithRemote, u.EnableDNS, u.HideSecret)
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithServer(u.DryRunStrategy), u.EnableDNS, u.HideSecret)
if err != nil {
return nil, nil, false, err
}
@ -391,8 +376,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR
return nil
})
// Run if it is a dry run
if u.isDryRun() {
if isDryRun(u.DryRunStrategy) {
slog.Debug("dry run for release", "name", upgradedRelease.Name)
if len(u.Description) > 0 {
upgradedRelease.Info.Description = u.Description

@ -545,7 +545,7 @@ func TestUpgradeRelease_DryRun(t *testing.T) {
rel.Info.Status = release.StatusDeployed
req.NoError(upAction.cfg.Releases.Create(rel))
upAction.DryRun = true
upAction.DryRunStrategy = DryRunClient
vals := map[string]interface{}{}
ctx, done := context.WithCancel(t.Context())
@ -577,7 +577,7 @@ func TestUpgradeRelease_DryRun(t *testing.T) {
is.Equal(1, lastRelease.Version)
// Ensure in a dry run mode when using HideSecret
upAction.DryRun = false
upAction.DryRunStrategy = DryRunNone
vals = map[string]interface{}{}
ctx, done = context.WithCancel(t.Context())

@ -0,0 +1,83 @@
/*
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 (
"fmt"
"log/slog"
"strconv"
"github.com/spf13/cobra"
"helm.sh/helm/v4/pkg/action"
)
func addDryRunFlag(cmd *cobra.Command) {
// --dry-run options with expected outcome:
// - Not set means no dry run and server is contacted.
// - Set with no value, a value of client, or a value of true and the server is not contacted
// - Set with a value of false, none, or false and the server is contacted
// The true/false part is meant to reflect some legacy behavior while none is equal to "".
f := cmd.Flags()
f.String(
"dry-run",
"none",
`simulates the operation without persisting changes. Must be one of: "none" (default), "client", or "server". '--dry-run=none' executes the operation normally and persists changes (no simulation). '--dry-run=client' simulates the operation client-side only and avoids cluster connections. '--dry-run=server' simulates the operation on the server, requiring cluster connectivity.`)
f.Lookup("dry-run").NoOptDefVal = "unset"
}
// Determine the `action.DryRunStrategy` given -dry-run=<value>` flag (or absence of)
// Legacy usage of the flag: boolean values, and `--dry-run` (without value) are supported, and log warnings emitted
func cmdGetDryRunFlagStrategy(cmd *cobra.Command, isTemplate bool) (action.DryRunStrategy, error) {
f := cmd.Flag("dry-run")
v := f.Value.String()
switch v {
case f.NoOptDefVal:
slog.Warn(`--dry-run is deprecated and should be replaced with '--dry-run=client'`)
return action.DryRunClient, nil
case string(action.DryRunClient):
return action.DryRunClient, nil
case string(action.DryRunServer):
return action.DryRunServer, nil
case string(action.DryRunNone):
if isTemplate {
// Special case hack for `helm template`, which is always a dry run
return action.DryRunNone, fmt.Errorf(`invalid dry-run value (%q). Must be "server" or "client"`, v)
}
return action.DryRunNone, nil
}
b, err := strconv.ParseBool(v)
if err != nil {
return action.DryRunNone, fmt.Errorf(`invalid dry-run value (%q). Must be "none", "server", or "client"`, v)
}
if isTemplate && !b {
// Special case for `helm template`, which is always a dry run
return action.DryRunNone, fmt.Errorf(`invalid dry-run value (%q). Must be "server" or "client"`, v)
}
result := action.DryRunNone
if b {
result = action.DryRunClient
}
slog.Warn(fmt.Sprintf(`boolean '--dry-run=%v' flag is deprecated and must be replaced with '--dry-run=%s'`, v, result))
return result, nil
}

@ -18,7 +18,9 @@ package cmd
import (
"bytes"
"encoding/json"
"io"
"log/slog"
"os"
"strings"
"testing"
@ -26,6 +28,8 @@ import (
shellwords "github.com/mattn/go-shellwords"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/internal/test"
"helm.sh/helm/v4/pkg/action"
@ -151,3 +155,155 @@ func resetEnv() func() {
settings = cli.New()
}
}
func TestCmdGetDryRunFlagStrategy(t *testing.T) {
type testCaseExpectedLog struct {
Level string
Msg string
}
testCases := map[string]struct {
DryRunFlagArg string
IsTemplate bool
ExpectedStrategy action.DryRunStrategy
ExpectedError bool
ExpectedLog *testCaseExpectedLog
}{
"unset_value": {
DryRunFlagArg: "--dry-run",
ExpectedStrategy: action.DryRunClient,
ExpectedLog: &testCaseExpectedLog{
Level: "WARN",
Msg: `--dry-run is deprecated and should be replaced with '--dry-run=client'`,
},
},
"unset_special": {
DryRunFlagArg: "--dry-run=unset", // Special value that matches cmd.Flags("dry-run").NoOptDefVal
ExpectedStrategy: action.DryRunClient,
ExpectedLog: &testCaseExpectedLog{
Level: "WARN",
Msg: `--dry-run is deprecated and should be replaced with '--dry-run=client'`,
},
},
"none": {
DryRunFlagArg: "--dry-run=none",
ExpectedStrategy: action.DryRunNone,
},
"client": {
DryRunFlagArg: "--dry-run=client",
ExpectedStrategy: action.DryRunClient,
},
"server": {
DryRunFlagArg: "--dry-run=server",
ExpectedStrategy: action.DryRunServer,
},
"bool_false": {
DryRunFlagArg: "--dry-run=false",
ExpectedStrategy: action.DryRunNone,
ExpectedLog: &testCaseExpectedLog{
Level: "WARN",
Msg: `boolean '--dry-run=false' flag is deprecated and must be replaced with '--dry-run=none'`,
},
},
"bool_true": {
DryRunFlagArg: "--dry-run=true",
ExpectedStrategy: action.DryRunClient,
ExpectedLog: &testCaseExpectedLog{
Level: "WARN",
Msg: `boolean '--dry-run=true' flag is deprecated and must be replaced with '--dry-run=client'`,
},
},
"bool_0": {
DryRunFlagArg: "--dry-run=0",
ExpectedStrategy: action.DryRunNone,
ExpectedLog: &testCaseExpectedLog{
Level: "WARN",
Msg: `boolean '--dry-run=0' flag is deprecated and must be replaced with '--dry-run=none'`,
},
},
"bool_1": {
DryRunFlagArg: "--dry-run=1",
ExpectedStrategy: action.DryRunClient,
ExpectedLog: &testCaseExpectedLog{
Level: "WARN",
Msg: `boolean '--dry-run=1' flag is deprecated and must be replaced with '--dry-run=client'`,
},
},
"invalid": {
DryRunFlagArg: "--dry-run=invalid",
ExpectedError: true,
},
"template_unset_value": {
DryRunFlagArg: "--dry-run",
IsTemplate: true,
ExpectedStrategy: action.DryRunClient,
ExpectedLog: &testCaseExpectedLog{
Level: "WARN",
Msg: `--dry-run is deprecated and should be replaced with '--dry-run=client'`,
},
},
"template_bool_false": {
DryRunFlagArg: "--dry-run=false",
IsTemplate: true,
ExpectedError: true,
},
"template_bool_template_true": {
DryRunFlagArg: "--dry-run=true",
IsTemplate: true,
ExpectedStrategy: action.DryRunClient,
ExpectedLog: &testCaseExpectedLog{
Level: "WARN",
Msg: `boolean '--dry-run=true' flag is deprecated and must be replaced with '--dry-run=client'`,
},
},
"template_none": {
DryRunFlagArg: "--dry-run=none",
IsTemplate: true,
ExpectedError: true,
},
"template_client": {
DryRunFlagArg: "--dry-run=client",
IsTemplate: true,
ExpectedStrategy: action.DryRunClient,
},
"template_server": {
DryRunFlagArg: "--dry-run=server",
IsTemplate: true,
ExpectedStrategy: action.DryRunServer,
},
}
for name, tc := range testCases {
logBuf := new(bytes.Buffer)
logger := slog.New(slog.NewJSONHandler(logBuf, nil))
slog.SetDefault(logger)
cmd := &cobra.Command{
Use: "helm",
}
addDryRunFlag(cmd)
cmd.Flags().Parse([]string{"helm", tc.DryRunFlagArg})
t.Run(name, func(t *testing.T) {
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, tc.IsTemplate)
if tc.ExpectedError {
assert.Error(t, err)
} else {
assert.Nil(t, err)
assert.Equal(t, tc.ExpectedStrategy, dryRunStrategy)
}
if tc.ExpectedLog != nil {
logResult := map[string]string{}
err = json.Unmarshal(logBuf.Bytes(), &logResult)
require.Nil(t, err)
assert.Equal(t, tc.ExpectedLog.Level, logResult["level"])
assert.Equal(t, tc.ExpectedLog.Msg, logResult["msg"])
} else {
assert.Equal(t, 0, logBuf.Len())
}
})
}
}

@ -18,14 +18,12 @@ package cmd
import (
"context"
"errors"
"fmt"
"io"
"log"
"log/slog"
"os"
"os/signal"
"slices"
"syscall"
"time"
@ -144,7 +142,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compInstall(args, toComplete, client)
},
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password)
if err != nil {
@ -152,12 +150,12 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
client.SetRegistryClient(registryClient)
// This is for the case where "" is specifically passed in as a
// value. When there is no value passed in NoOptDefVal will be used
// and it is set to client. See addInstallFlags.
if client.DryRunOption == "" {
client.DryRunOption = "none"
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false)
if err != nil {
return err
}
client.DryRunStrategy = dryRunStrategy
rel, err := runInstall(args, client, valueOpts, out)
if err != nil {
return fmt.Errorf("INSTALLATION FAILED: %w", err)
@ -173,11 +171,12 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
addInstallFlags(cmd, cmd.Flags(), client, valueOpts)
f := cmd.Flags()
addInstallFlags(cmd, f, client, valueOpts)
// hide-secret is not available in all places the install flags are used so
// it is added separately
f := cmd.Flags()
f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag")
addDryRunFlag(cmd)
bindOutputFlag(cmd, &outfmt)
bindPostRenderFlag(cmd, &client.PostRenderer, settings)
@ -186,13 +185,6 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) {
f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present")
// --dry-run options with expected outcome:
// - Not set means no dry run and server is contacted.
// - Set with no value, a value of client, or a value of true and the server is not contacted
// - Set with a value of false, none, or false and the server is contacted
// The true/false part is meant to reflect some legacy behavior while none is equal to "".
f.StringVar(&client.DryRunOption, "dry-run", "", "simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections.")
f.Lookup("dry-run").NoOptDefVal = "client"
f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement")
f.BoolVar(&client.ForceReplace, "force", false, "deprecated")
f.MarkDeprecated("force", "use --force-replace instead")
@ -316,11 +308,6 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
client.Namespace = settings.Namespace()
// Validate DryRunOption member is one of the allowed values
if err := validateDryRunOptionFlag(client.DryRunOption); err != nil {
return nil, err
}
// Create context and prepare the handle of SIGTERM
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
@ -363,13 +350,3 @@ func compInstall(args []string, toComplete string, client *action.Install) ([]st
}
return nil, cobra.ShellCompDirectiveNoFileComp
}
func validateDryRunOptionFlag(dryRunOptionFlagValue string) error {
// Validate dry-run flag value with a set of allowed value
allowedDryRunValues := []string{"false", "true", "none", "client", "server"}
isAllowed := slices.Contains(allowedDryRunValues, dryRunOptionFlagValue)
if !isAllowed {
return errors.New("invalid dry-run flag. Flag must one of the following: false, true, none, client, server")
}
return nil
}

@ -57,7 +57,7 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return noMoreArgsComp()
},
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 1 {
ver, err := strconv.Atoi(args[1])
if err != nil {
@ -66,6 +66,12 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client.Version = ver
}
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false)
if err != nil {
return err
}
client.DryRunStrategy = dryRunStrategy
if err := client.Run(args[0]); err != nil {
return err
}
@ -76,7 +82,6 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
f := cmd.Flags()
f.BoolVar(&client.DryRun, "dry-run", false, "simulate a rollback")
f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement")
f.BoolVar(&client.ForceReplace, "force", false, "deprecated")
f.MarkDeprecated("force", "use --force-replace instead")
@ -87,6 +92,7 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&client.WaitForJobs, "wait-for-jobs", false, "if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout")
f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this rollback when rollback fails")
f.IntVar(&client.MaxHistory, "history-max", settings.MaxHistory, "limit the maximum number of revisions saved per release. Use 0 for no limit")
addDryRunFlag(cmd)
AddWaitFlag(cmd, &client.WaitStrategy)
cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts")
cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts")

@ -67,7 +67,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compInstall(args, toComplete, client)
},
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
if kubeVersion != "" {
parsedKubeVersion, err := common.ParseKubeVersion(kubeVersion)
if err != nil {
@ -83,16 +83,17 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
client.SetRegistryClient(registryClient)
// This is for the case where "" is specifically passed in as a
// value. When there is no value passed in NoOptDefVal will be used
// and it is set to client. See addInstallFlags.
if client.DryRunOption == "" {
client.DryRunOption = "true"
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, true)
if err != nil {
return err
}
if validate {
// Mimic deprecated --validate flag behavior by enabling server dry run
dryRunStrategy = action.DryRunServer
}
client.DryRun = true
client.DryRunStrategy = dryRunStrategy
client.ReleaseName = "release-name"
client.Replace = true // Skip the name check
client.ClientOnly = !validate
client.APIVersions = common.VersionSet(extraAPIs)
client.IncludeCRDs = includeCrds
rel, err := runInstall(args, client, valueOpts, out)
@ -196,14 +197,21 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
addInstallFlags(cmd, f, client, valueOpts)
f.StringArrayVarP(&showFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates")
f.StringVar(&client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout")
f.BoolVar(&validate, "validate", false, "validate your manifests against the Kubernetes cluster you are currently pointing at. This is the same validation performed on an install")
f.BoolVar(&validate, "validate", false, "deprecated")
f.MarkDeprecated("validate", "use '--dry-run=server' instead")
f.BoolVar(&includeCrds, "include-crds", false, "include CRDs in the templated output")
f.BoolVar(&skipTests, "skip-tests", false, "skip tests from templated output")
f.BoolVar(&client.IsUpgrade, "is-upgrade", false, "set .Release.IsUpgrade instead of .Release.IsInstall")
f.StringVar(&kubeVersion, "kube-version", "", "Kubernetes version used for Capabilities.KubeVersion")
f.StringSliceVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions (multiple can be specified)")
f.BoolVar(&client.UseReleaseName, "release-name", false, "use release name in the output-dir path.")
f.String(
"dry-run",
"client",
`simulates the operation either client-side or server-side. Must be either: "client", or "server". '--dry-run=client simulates the operation client-side only and avoids cluster connections. '--dry-run=server' simulates/validates the operation on the server, requiring cluster connectivity.`)
f.Lookup("dry-run").NoOptDefVal = "unset"
bindPostRenderFlag(cmd, &client.PostRenderer, settings)
cmd.MarkFlagsMutuallyExclusive("validate", "dry-run")
return cmd
}

@ -100,7 +100,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
return noMoreArgsComp()
},
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
client.Namespace = settings.Namespace()
registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile,
@ -110,12 +110,12 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
}
client.SetRegistryClient(registryClient)
// This is for the case where "" is specifically passed in as a
// value. When there is no value passed in NoOptDefVal will be used
// and it is set to client. See addInstallFlags.
if client.DryRunOption == "" {
client.DryRunOption = "none"
dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false)
if err != nil {
return err
}
client.DryRunStrategy = dryRunStrategy
// Fixes #7002 - Support reading values from STDIN for `upgrade` command
// Must load values AFTER determining if we have to call install so that values loaded from stdin are not read twice
if client.Install {
@ -132,8 +132,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
instClient.CreateNamespace = createNamespace
instClient.ChartPathOptions = client.ChartPathOptions
instClient.ForceReplace = client.ForceReplace
instClient.DryRun = client.DryRun
instClient.DryRunOption = client.DryRunOption
instClient.DryRunStrategy = client.DryRunStrategy
instClient.DisableHooks = client.DisableHooks
instClient.SkipCRDs = client.SkipCRDs
instClient.Timeout = client.Timeout
@ -183,10 +182,6 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
if err != nil {
return err
}
// Validate dry-run flag value is one of the allowed values
if err := validateDryRunOptionFlag(client.DryRunOption); err != nil {
return err
}
p := getter.All(settings)
vals, err := valueOpts.MergeValues(p)
@ -274,9 +269,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&createNamespace, "create-namespace", false, "if --install is set, create the release namespace if not present")
f.BoolVarP(&client.Install, "install", "i", false, "if a release by this name doesn't already exist, run an install")
f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored")
f.StringVar(&client.DryRunOption, "dry-run", "", "simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections.")
f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag")
f.Lookup("dry-run").NoOptDefVal = "client"
f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement")
f.BoolVar(&client.ForceReplace, "force", false, "deprecated")
f.MarkDeprecated("force", "use --force-replace instead")
@ -303,6 +296,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "update dependencies if they are missing before installing the chart")
f.BoolVar(&client.EnableDNS, "enable-dns", false, "enable DNS lookups when rendering templates")
f.BoolVar(&client.TakeOwnership, "take-ownership", false, "if set, upgrade will ignore the check for helm annotations and take ownership of the existing resources")
addDryRunFlag(cmd)
addChartPathOptionsFlags(f, &client.ChartPathOptions)
addValueOptionsFlags(f, valueOpts)
bindOutputFlag(cmd, &outfmt)

Loading…
Cancel
Save