feat(helm): add ability for a dry-run to evaluate lookup functions

When a helm command is run with the --dry-run-option=server flag, it will try to connect to the cluster
to be able to render lookup functions.
Closes #8137

Signed-off-by: Tapas Kapadia <tapaskapadia10@gmail.com>
pull/9426/head
Tapas Kapadia 3 years ago
parent be99ebe8af
commit ddb33580db

@ -154,8 +154,9 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) { 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") f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present")
f.StringVar(&client.DryRun, "dry-run", "none", "simulate an install. If --dry-run is set with no option being specified or as 'client', it will not attempt cluster connections. Setting option as 'server' allows attempting cluster connections.") f.BoolVar(&client.DryRun, "dry-run", false, "simulate an install")
f.Lookup("dry-run").NoOptDefVal = "client" f.StringVar(&client.DryRunOption, "dry-run-option", "none", "simulate an install. If --dry-run is set with no option being specified or as 'client', it will not attempt cluster connections. Setting option as 'server' allows attempting cluster connections.")
f.Lookup("dry-run-option").NoOptDefVal = "client"
f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy") f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install") f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install")
f.BoolVar(&client.Replace, "replace", false, "re-use the given name, only if that name is a deleted release which remains in the history. This is unsafe in production") f.BoolVar(&client.Replace, "replace", false, "re-use the given name, only if that name is a deleted release which remains in the history. This is unsafe in production")
@ -174,6 +175,8 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal
addValueOptionsFlags(f, valueOpts) addValueOptionsFlags(f, valueOpts)
addChartPathOptionsFlags(f, &client.ChartPathOptions) addChartPathOptionsFlags(f, &client.ChartPathOptions)
cmd.MarkFlagsMutuallyExclusive("dry-run", "dry-run-option")
err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
requiredArgs := 2 requiredArgs := 2
if client.GenerateName { if client.GenerateName {
@ -263,7 +266,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
client.Namespace = settings.Namespace() client.Namespace = settings.Namespace()
// validate dry-run flag value is one of the allowed values // validate dry-run flag value is one of the allowed values
if err := validateDryRunFlag(client.DryRun); err != nil { if err := validateDryRunOptionFlag(client.DryRunOption); err != nil {
return nil, err return nil, err
} }
@ -308,12 +311,12 @@ func compInstall(args []string, toComplete string, client *action.Install) ([]st
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp
} }
func validateDryRunFlag(dryRunFlagValue string) error { func validateDryRunOptionFlag(dryRunOptionFlagValue string) error {
// validate dry-run flag value with set of allowed value // validate dry-run flag value with set of allowed value
allowedDryRunValues := []string{"false", "true", "none", "client", "server"} allowedDryRunValues := []string{"false", "true", "none", "client", "server"}
isAllowed := false isAllowed := false
for _, v := range allowedDryRunValues { for _, v := range allowedDryRunValues {
if dryRunFlagValue == v { if dryRunOptionFlagValue == v {
isAllowed = true isAllowed = true
break break
} }

@ -73,7 +73,6 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client.KubeVersion = parsedKubeVersion client.KubeVersion = parsedKubeVersion
} }
client.DryRun = "client"
client.ReleaseName = "release-name" client.ReleaseName = "release-name"
client.Replace = true // Skip the name check client.Replace = true // Skip the name check
client.ClientOnly = !validate client.ClientOnly = !validate

@ -106,6 +106,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
instClient.ChartPathOptions = client.ChartPathOptions instClient.ChartPathOptions = client.ChartPathOptions
instClient.Force = client.Force instClient.Force = client.Force
instClient.DryRun = client.DryRun instClient.DryRun = client.DryRun
instClient.DryRunOption = client.DryRunOption
instClient.DisableHooks = client.DisableHooks instClient.DisableHooks = client.DisableHooks
instClient.SkipCRDs = client.SkipCRDs instClient.SkipCRDs = client.SkipCRDs
instClient.Timeout = client.Timeout instClient.Timeout = client.Timeout
@ -119,7 +120,6 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
instClient.SubNotes = client.SubNotes instClient.SubNotes = client.SubNotes
instClient.Description = client.Description instClient.Description = client.Description
instClient.DependencyUpdate = client.DependencyUpdate instClient.DependencyUpdate = client.DependencyUpdate
rel, err := runInstall(args, instClient, valueOpts, out) rel, err := runInstall(args, instClient, valueOpts, out)
if err != nil { if err != nil {
return err return err
@ -140,7 +140,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
return err return err
} }
// validate dry-run flag value is one of the allowed values // validate dry-run flag value is one of the allowed values
if err := validateDryRunFlag(client.DryRun); err != nil { if err := validateDryRunOptionFlag(client.DryRunOption); err != nil {
return err return err
} }
@ -218,8 +218,9 @@ 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.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.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.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.DryRun, "dry-run", "none", "simulate an install. If --dry-run is set with no option being specified or as 'client', it will not attempt cluster connections. Setting option as 'server' allows attempting cluster connections.") f.BoolVar(&client.DryRun, "dry-run", false, "simulate an upgrade")
f.Lookup("dry-run").NoOptDefVal = "client" f.StringVar(&client.DryRunOption, "dry-run-option", "none", "simulate an install. If --dry-run is set with no option being specified or as 'client', it will not attempt cluster connections. Setting option as 'server' allows attempting cluster connections.")
f.Lookup("dry-run-option").NoOptDefVal = "client"
f.BoolVar(&client.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable") f.BoolVar(&client.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable")
f.MarkDeprecated("recreate-pods", "functionality will no longer be updated. Consult the documentation for other methods to recreate pods") f.MarkDeprecated("recreate-pods", "functionality will no longer be updated. Consult the documentation for other methods to recreate pods")
f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy") f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy")
@ -242,6 +243,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
bindOutputFlag(cmd, &outfmt) bindOutputFlag(cmd, &outfmt)
bindPostRenderFlag(cmd, &client.PostRenderer) bindPostRenderFlag(cmd, &client.PostRenderer)
cmd.MarkFlagsMutuallyExclusive("dry-run", "dry-run-option")
err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 2 { if len(args) != 2 {
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp

@ -102,7 +102,7 @@ type Configuration struct {
// TODO: This function is badly in need of a refactor. // TODO: This function is badly in need of a refactor.
// TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed // TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed
// This code has to do with writing files to disk. // This code has to do with writing files to disk.
func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, dryRun string) ([]*release.Hook, *bytes.Buffer, string, error) { func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, interactWithRemote bool) ([]*release.Hook, *bytes.Buffer, string, error) {
hs := []*release.Hook{} hs := []*release.Hook{}
b := bytes.NewBuffer(nil) b := bytes.NewBuffer(nil)
@ -121,11 +121,11 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu
var err2 error var err2 error
// A `helm template` should not talk to the remote cluster. However, commands // A `helm template` should not talk to the remote cluster. However, commands
// with `--dry-run` with the value of false, none, or sever should try to connect to the cluster. // with the flag `--dry-run-option` with the value of false, none, or sever
// This enables the ability to render 'lookup' functions. // or with the flag `--dry-run` with the value of false should try to interact with the cluster.
// It may break in interesting and exotic ways because other data (e.g. discovery) // It may break in interesting and exotic ways because other data (e.g. discovery)
// is mocked. // is mocked.
if (dryRun == "server" || dryRun == "none" || dryRun == "false") && cfg.RESTClientGetter != nil { if interactWithRemote && cfg.RESTClientGetter != nil {
restConfig, err := cfg.RESTClientGetter.ToRESTConfig() restConfig, err := cfg.RESTClientGetter.ToRESTConfig()
if err != nil { if err != nil {
return hs, b, "", err return hs, b, "", err

@ -71,7 +71,8 @@ type Install struct {
ClientOnly bool ClientOnly bool
Force bool Force bool
CreateNamespace bool CreateNamespace bool
DryRun string DryRun bool
DryRunOption string
DisableHooks bool DisableHooks bool
Replace bool Replace bool
Wait bool Wait bool
@ -128,8 +129,6 @@ type ChartPathOptions struct {
func NewInstall(cfg *Configuration) *Install { func NewInstall(cfg *Configuration) *Install {
in := &Install{ in := &Install{
cfg: cfg, cfg: cfg,
// Set default value of DryRun for before flags are binded (tests)
DryRun: "none",
} }
in.ChartPathOptions.registryClient = cfg.RegistryClient in.ChartPathOptions.registryClient = cfg.RegistryClient
@ -205,11 +204,21 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
return nil, err return nil, err
} }
// determine dry run behavior
if i.DryRun || i.DryRunOption == "client" || i.DryRunOption == "server" || i.DryRunOption == "true" {
i.DryRun = true
}
var interactWithRemote bool
if !i.DryRun || i.DryRunOption == "server" {
interactWithRemote = true
}
// Pre-install anything in the crd/ directory. We do this before Helm // Pre-install anything in the crd/ directory. We do this before Helm
// contacts the upstream server and builds the capabilities object. // contacts the upstream server and builds the capabilities object.
if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 { if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 {
// On dry run, bail here // On dry run, bail here
if i.DryRun != "none" && i.DryRun != "false" { if i.DryRun {
i.cfg.Log("WARNING: This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.") i.cfg.Log("WARNING: This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.")
} else if err := i.installCRDs(crds); err != nil { } else if err := i.installCRDs(crds); err != nil {
return nil, err return nil, err
@ -243,7 +252,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
} }
// special case for helm template --is-upgrade // special case for helm template --is-upgrade
isUpgrade := i.IsUpgrade && (i.DryRun != "none" && i.DryRun != "false") isUpgrade := i.IsUpgrade && i.DryRun
options := chartutil.ReleaseOptions{ options := chartutil.ReleaseOptions{
Name: i.ReleaseName, Name: i.ReleaseName,
Namespace: i.Namespace, Namespace: i.Namespace,
@ -259,8 +268,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
rel := i.createRelease(chrt, vals) rel := i.createRelease(chrt, vals)
var manifestDoc *bytes.Buffer 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)
rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, i.DryRun)
// Even for errors, attach this if available // Even for errors, attach this if available
if manifestDoc != nil { if manifestDoc != nil {
rel.Manifest = manifestDoc.String() rel.Manifest = manifestDoc.String()
@ -301,7 +309,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma
} }
// Bail out here if it is a dry run // Bail out here if it is a dry run
if i.DryRun != "none" && i.DryRun != "false" { if i.DryRun {
rel.Info.Description = "Dry run complete" rel.Info.Description = "Dry run complete"
return rel, nil return rel, nil
} }
@ -471,7 +479,7 @@ func (i *Install) availableName() error {
return errors.Wrapf(err, "release name %q", start) return errors.Wrapf(err, "release name %q", start)
} }
// On dry run, bail here // On dry run, bail here
if i.DryRun != "none" && i.DryRun != "false" { if i.DryRun {
return nil return nil
} }

@ -234,7 +234,7 @@ func TestInstallRelease_WithChartAndDependencyAllNotes(t *testing.T) {
func TestInstallRelease_DryRun(t *testing.T) { func TestInstallRelease_DryRun(t *testing.T) {
is := assert.New(t) is := assert.New(t)
instAction := installAction(t) instAction := installAction(t)
instAction.DryRun = "true" instAction.DryRun = true
vals := map[string]interface{}{} vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(withSampleTemplates()), vals) res, err := instAction.Run(buildChart(withSampleTemplates()), vals)
if err != nil { if err != nil {
@ -258,7 +258,7 @@ func TestInstallRelease_DryRun(t *testing.T) {
func TestInstallRelease_DryRun_Lookup(t *testing.T) { func TestInstallRelease_DryRun_Lookup(t *testing.T) {
is := assert.New(t) is := assert.New(t)
instAction := installAction(t) instAction := installAction(t)
instAction.DryRun = "true" instAction.DryRun = true
vals := map[string]interface{}{} vals := map[string]interface{}{}
mockChart := buildChart(withSampleTemplates()) mockChart := buildChart(withSampleTemplates())
@ -278,7 +278,7 @@ func TestInstallRelease_DryRun_Lookup(t *testing.T) {
func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) { func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) {
is := assert.New(t) is := assert.New(t)
instAction := installAction(t) instAction := installAction(t)
instAction.DryRun = "true" instAction.DryRun = true
vals := map[string]interface{}{} vals := map[string]interface{}{}
_, err := instAction.Run(buildChart(withSampleIncludingIncorrectTemplates()), vals) _, err := instAction.Run(buildChart(withSampleIncludingIncorrectTemplates()), vals)
expectedErr := "\"hello/templates/incorrect\" at <.Values.bad.doh>: nil pointer evaluating interface {}.doh" expectedErr := "\"hello/templates/incorrect\" at <.Values.bad.doh>: nil pointer evaluating interface {}.doh"

@ -70,7 +70,9 @@ type Upgrade struct {
// DisableHooks disables hook processing if set to true. // DisableHooks disables hook processing if set to true.
DisableHooks bool DisableHooks bool
// DryRun controls whether the operation is prepared, but not executed. // DryRun controls whether the operation is prepared, but not executed.
DryRun string 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
// Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway. // Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway.
// //
// This should be used with caution. // This should be used with caution.
@ -113,8 +115,6 @@ type resultMessage struct {
func NewUpgrade(cfg *Configuration) *Upgrade { func NewUpgrade(cfg *Configuration) *Upgrade {
up := &Upgrade{ up := &Upgrade{
cfg: cfg, cfg: cfg,
// Set default value of DryRun for before flags are binded (tests)
DryRun: "none",
} }
up.ChartPathOptions.registryClient = cfg.RegistryClient up.ChartPathOptions.registryClient = cfg.RegistryClient
@ -140,6 +140,12 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
if err := chartutil.ValidateReleaseName(name); err != nil { if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("release name is invalid: %s", name) return nil, errors.Errorf("release name is invalid: %s", name)
} }
// determine dry run behavior
if u.DryRun || u.DryRunOption == "client" || u.DryRunOption == "server" || u.DryRunOption == "true" {
u.DryRun = true
}
u.cfg.Log("preparing upgrade for %s", name) u.cfg.Log("preparing upgrade for %s", name)
currentRelease, upgradedRelease, err := u.prepareUpgrade(name, chart, vals) currentRelease, upgradedRelease, err := u.prepareUpgrade(name, chart, vals)
if err != nil { if err != nil {
@ -153,8 +159,9 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.
if err != nil { if err != nil {
return res, err return res, err
} }
// Do not update for dry runs // Do not update for dry runs
if u.DryRun == "none" || u.DryRun == "false" { if !u.DryRun {
u.cfg.Log("updating status for upgraded release for %s", name) u.cfg.Log("updating status for upgraded release for %s", name)
if err := u.cfg.Releases.Update(upgradedRelease); err != nil { if err := u.cfg.Releases.Update(upgradedRelease); err != nil {
return res, err return res, err
@ -232,7 +239,13 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin
return nil, nil, err return nil, nil, err
} }
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, u.DryRun) // determine whether or not to interact with remote
var interactWithRemote bool
if !u.DryRun || u.DryRunOption == "server" {
interactWithRemote = true
}
hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithRemote)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -311,7 +324,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR
}) })
// Run if it is a dry run // Run if it is a dry run
if u.DryRun != "none" && u.DryRun != "false" { if u.DryRun {
u.cfg.Log("dry run for %s", upgradedRelease.Name) u.cfg.Log("dry run for %s", upgradedRelease.Name)
if len(u.Description) > 0 { if len(u.Description) > 0 {
upgradedRelease.Info.Description = u.Description upgradedRelease.Info.Description = u.Description

Loading…
Cancel
Save