fix(v3): Add `--client-only` and fix `--validate` for `helm template`

A.k.a "allow `helm template` to optionally skip api versions check while keeping existing use-cases and features as-is"

This introduces a new flag `--client-only` to `helm template`, so that (1)we can optionally disable the validation at all, (2)while keeping the ability to switch what are validated.

(1) is achieved by changing the semantics of the existing flag `--validate=[true|false]` new in v3 and (2) is achieved by adding `--client-only=[true|false]` that mostly the same as the former `--validate`.

This should address all the three use-cases listed in https://github.com/helm/helm/pull/6729#issuecomment-547249311 after fixing other related bugs.

Closes #6505
Replaces #6729

---

Even though the semantics of the existing flag `--validate` has changed, the existing and default behavior of `helm template` is maintained. That is, `helm template` without any flags means `do client-only validation` before/after this change.

Regarding changes in the `--validate`, as you can see from the existing code-base and the description of the flag, unlike it's name, it had been used for toggling the client-only mode of the validation. For instance, `helm template [--validate=false]` had been left api versions check running even though, `--validate=false` or omitting the flag would had disabled the whole validation according to it's name.

This change adds `--client-only` so that we can toggle the validation and the client-only mode separately, which allows more granular control, and more specifically, support the `do not validate at all` scenario.

Signed-off-by: Yusuke Kuoka <ykuoka@gmail.com>
pull/6811/head
Yusuke Kuoka 6 years ago
parent 82a100b0b7
commit 1f951c842d

@ -44,6 +44,7 @@ faked locally. Additionally, none of the server-side testing of chart validity
func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
var validate bool var validate bool
var clientOnly bool
client := action.NewInstall(cfg) client := action.NewInstall(cfg)
valueOpts := &values.Options{} valueOpts := &values.Options{}
var extraAPIs []string var extraAPIs []string
@ -58,7 +59,8 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client.DryRun = true client.DryRun = true
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.NoValidate = !validate
client.ClientOnly = clientOnly
client.APIVersions = chartutil.VersionSet(extraAPIs) client.APIVersions = chartutil.VersionSet(extraAPIs)
rel, err := runInstall(args, client, valueOpts, out) rel, err := runInstall(args, client, valueOpts, out)
if err != nil { if err != nil {
@ -119,7 +121,8 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
addInstallFlags(f, client, valueOpts) addInstallFlags(f, client, valueOpts)
f.StringArrayVarP(&showFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates") 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.StringVar(&client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout")
f.BoolVar(&validate, "validate", false, "establish a connection to Kubernetes for schema validation") f.BoolVar(&validate, "validate", true, "Runs schema validation")
f.BoolVar(&clientOnly, "client-only", true, "Prevents establishing a connection to Kubernetes for schema validation")
f.StringArrayVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions") f.StringArrayVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions")
return cmd return cmd

@ -68,6 +68,7 @@ type Install struct {
ChartPathOptions ChartPathOptions
ClientOnly bool ClientOnly bool
NoValidate bool
DryRun bool DryRun bool
DisableHooks bool DisableHooks bool
Replace bool Replace bool
@ -101,6 +102,10 @@ type ChartPathOptions struct {
Version string // --version Version string // --version
} }
type RenderOptions struct {
NoValidate bool
}
// NewInstall creates a new Install object with the given configuration. // NewInstall creates a new Install object with the given configuration.
func NewInstall(cfg *Configuration) *Install { func NewInstall(cfg *Configuration) *Install {
return &Install{ return &Install{
@ -151,8 +156,10 @@ func (i *Install) installCRDs(crds []*chart.File) error {
// //
// If DryRun is set to true, this will prepare the release, but not install it // If DryRun is set to true, this will prepare the release, but not install it
func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
validate := !i.NoValidate
// Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`) // Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`)
if !i.ClientOnly { if validate && !i.ClientOnly {
if err := i.cfg.KubeClient.IsReachable(); err != nil { if err := i.cfg.KubeClient.IsReachable(); err != nil {
return nil, err return nil, err
} }
@ -164,7 +171,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
// 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.CRDs(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 { if crds := chrt.CRDs(); validate && !i.SkipCRDs && len(crds) > 0 {
// On dry run, bail here // On dry run, bail here
if i.DryRun { 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.")
@ -211,7 +218,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
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.OutputDir, i.SubNotes) rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.OutputDir, i.SubNotes, RenderOptions{NoValidate: i.NoValidate})
// 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()
@ -237,7 +244,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.
// we'll end up in a state where we will delete those resources upon // 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 // deleting the release because the manifest will be pointing at that
// resource // resource
if !i.ClientOnly { if validate && !i.ClientOnly {
if err := existingResourceConflict(resources); err != nil { if err := existingResourceConflict(resources); err != nil {
return nil, errors.Wrap(err, "rendered manifests contain a resource that already exists. Unable to continue with install") return nil, errors.Wrap(err, "rendered manifests contain a resource that already exists. Unable to continue with install")
} }
@ -409,7 +416,7 @@ func (i *Install) replaceRelease(rel *release.Release) error {
} }
// renderResources renders the templates in a chart // renderResources renders the templates in a chart
func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, outputDir string, subNotes bool) ([]*release.Hook, *bytes.Buffer, string, error) { func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, outputDir string, subNotes bool, opts RenderOptions) ([]*release.Hook, *bytes.Buffer, string, error) {
hs := []*release.Hook{} hs := []*release.Hook{}
b := bytes.NewBuffer(nil) b := bytes.NewBuffer(nil)
@ -452,7 +459,7 @@ func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values
// Sort hooks, manifests, and partials. Only hooks and manifests are returned, // Sort hooks, manifests, and partials. Only hooks and manifests are returned,
// as partials are not used after renderer.Render. Empty manifests are also // as partials are not used after renderer.Render. Empty manifests are also
// removed here. // removed here.
hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder) hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder, releaseutil.SortOptions{NoValidate: opts.NoValidate})
if err != nil { if err != nil {
// By catching parse errors here, we can prevent bogus releases from going // By catching parse errors here, we can prevent bogus releases from going
// to Kubernetes. // to Kubernetes.

@ -160,7 +160,7 @@ 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) hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", u.SubNotes, RenderOptions{})
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

@ -38,9 +38,10 @@ type Manifest struct {
// manifestFile represents a file that contains a manifest. // manifestFile represents a file that contains a manifest.
type manifestFile struct { type manifestFile struct {
entries map[string]string entries map[string]string
path string path string
apis chartutil.VersionSet apis chartutil.VersionSet
validate bool
} }
// result is an intermediate structure used during sorting. // result is an intermediate structure used during sorting.
@ -65,6 +66,10 @@ var events = map[string]release.HookEvent{
"test-success": release.HookTest, "test-success": release.HookTest,
} }
type SortOptions struct {
NoValidate bool
}
// SortManifests takes a map of filename/YAML contents, splits the file // SortManifests takes a map of filename/YAML contents, splits the file
// by manifest entries, and sorts the entries into hook types. // by manifest entries, and sorts the entries into hook types.
// //
@ -74,7 +79,12 @@ var events = map[string]release.HookEvent{
// //
// Files that do not parse into the expected format are simply placed into a map and // Files that do not parse into the expected format are simply placed into a map and
// returned. // returned.
func SortManifests(files map[string]string, apis chartutil.VersionSet, sort KindSortOrder) ([]*release.Hook, []Manifest, error) { func SortManifests(files map[string]string, apis chartutil.VersionSet, sort KindSortOrder, opts ...SortOptions) ([]*release.Hook, []Manifest, error) {
var opt SortOptions
if len(opts) > 0 {
opt = opts[0]
}
result := &result{} result := &result{}
for filePath, c := range files { for filePath, c := range files {
@ -90,9 +100,10 @@ func SortManifests(files map[string]string, apis chartutil.VersionSet, sort Kind
} }
manifestFile := &manifestFile{ manifestFile := &manifestFile{
entries: SplitManifests(c), entries: SplitManifests(c),
path: filePath, path: filePath,
apis: apis, apis: apis,
validate: !opt.NoValidate,
} }
if err := manifestFile.sort(result); err != nil { if err := manifestFile.sort(result); err != nil {
@ -129,7 +140,7 @@ func (file *manifestFile) sort(result *result) error {
return errors.Wrapf(err, "YAML parse error on %s", file.path) return errors.Wrapf(err, "YAML parse error on %s", file.path)
} }
if entry.Version != "" && !file.apis.Has(entry.Version) { if file.validate && entry.Version != "" && !file.apis.Has(entry.Version) {
return errors.Errorf("apiVersion %q in %s is not available", entry.Version, file.path) return errors.Errorf("apiVersion %q in %s is not available", entry.Version, file.path)
} }

Loading…
Cancel
Save