From 972bb3cade0f95700a4412411396d934a6c95cc4 Mon Sep 17 00:00:00 2001 From: Fuxin Hao Date: Tue, 18 Sep 2018 18:48:57 +0800 Subject: [PATCH] (feat)helm: Allow dynamic dependency override (#2205) Add the --requirements flag to install and upgrade commands Signed-off-by: Fuxin Hao --- cmd/helm/install.go | 27 ++++++++++++++- cmd/helm/upgrade.go | 63 +++++++++++++++++++++++++++++------ docs/helm/helm_install.md | 3 +- docs/helm/helm_upgrade.md | 3 +- pkg/chartutil/requirements.go | 47 ++++++++++++++++++++++++-- pkg/downloader/manager.go | 23 ++++++++----- 6 files changed, 143 insertions(+), 23 deletions(-) diff --git a/cmd/helm/install.go b/cmd/helm/install.go index c5c6b9a49..6b7df0363 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -114,6 +114,7 @@ type installCmd struct { name string namespace string valueFiles valueFiles + reqFile string chartPath string dryRun bool disableHooks bool @@ -195,6 +196,7 @@ func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { f := cmd.Flags() settings.AddFlagsTLS(f) f.VarP(&inst.valueFiles, "values", "f", "specify values in a YAML file or a URL(can specify multiple)") + f.StringVarP(&inst.reqFile, "requirements", "r", "", "specify a yaml file to override dependencies(specify 'none' to remove all dependencies)") f.StringVarP(&inst.name, "name", "n", "", "release name. If unspecified, it will autogenerate one for you") f.StringVar(&inst.namespace, "namespace", "", "namespace to install the release into. Defaults to the current kube config namespace.") f.BoolVar(&inst.dryRun, "dry-run", false, "simulate an install") @@ -254,6 +256,18 @@ func (i *installCmd) run() error { return prettyError(err) } + // Override the requirements + if i.reqFile != "" { + if err := chartutil.SetRequirements(chartRequested, i.reqFile); err != nil { + return prettyError(err) + } + + // Run `helm dependency update` to update dependencies + i.depUp = true + // Ignore the existing dependencies + chartRequested.Dependencies = nil + } + if req, err := chartutil.LoadRequirements(chartRequested); err == nil { // If checkDependencies returns an error, we have unfulfilled dependencies. // As of Helm 2.4.0, this is treated as a stopping condition: @@ -268,6 +282,9 @@ func (i *installCmd) run() error { SkipUpdate: false, Getters: getter.All(settings), } + if i.reqFile != "" { + man.Requirements = req + } if err := man.Update(); err != nil { return prettyError(err) } @@ -277,10 +294,18 @@ func (i *installCmd) run() error { if err != nil { return prettyError(err) } + + if i.reqFile != "" { + if err := chartutil.SetRequirements(chartRequested, i.reqFile); err != nil { + return prettyError(err) + } + if err := chartutil.RemoveDependenciesNotPresent(chartRequested); err != nil { + return prettyError(err) + } + } } else { return prettyError(err) } - } } else if err != chartutil.ErrRequirementsNotFound { return fmt.Errorf("cannot load requirements: %v", err) diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index d05987b8a..113f8ffd0 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -24,6 +24,8 @@ import ( "github.com/spf13/cobra" "k8s.io/helm/pkg/chartutil" + "k8s.io/helm/pkg/downloader" + "k8s.io/helm/pkg/getter" "k8s.io/helm/pkg/helm" "k8s.io/helm/pkg/renderutil" storageerrors "k8s.io/helm/pkg/storage/errors" @@ -96,6 +98,7 @@ type upgradeCmd struct { values []string stringValues []string fileValues []string + reqFile string verify bool keyring string install bool @@ -149,6 +152,7 @@ func newUpgradeCmd(client helm.Interface, out io.Writer) *cobra.Command { f := cmd.Flags() settings.AddFlagsTLS(f) f.VarP(&upgrade.valueFiles, "values", "f", "specify values in a YAML file or a URL(can specify multiple)") + f.StringVarP(&upgrade.reqFile, "requirements", "r", "", "specify a yaml file to override dependencies(specify 'none' to remove all dependencies)") f.BoolVar(&upgrade.dryRun, "dry-run", false, "simulate an upgrade") f.BoolVar(&upgrade.recreate, "recreate-pods", false, "performs pods restart for the resource if applicable") f.BoolVar(&upgrade.force, "force", false, "force resource update through delete/recreate if needed") @@ -219,6 +223,7 @@ func (u *upgradeCmd) run() error { out: u.out, name: u.release, valueFiles: u.valueFiles, + reqFile: u.reqFile, dryRun: u.dryRun, verify: u.verify, disableHooks: u.disableHooks, @@ -240,22 +245,60 @@ func (u *upgradeCmd) run() error { return err } + ch, err := chartutil.Load(chartPath) + if err != nil { + return prettyError(err) + } + + // Override requirements + if u.reqFile != "" { + if err = chartutil.SetRequirements(ch, u.reqFile); err != nil { + return prettyError(err) + } + + // Ignore existing dependencies + ch.Dependencies = nil + } + // Check chart requirements to make sure all dependencies are present in /charts - if ch, err := chartutil.Load(chartPath); err == nil { - if req, err := chartutil.LoadRequirements(ch); err == nil { - if err := renderutil.CheckDependencies(ch, req); err != nil { - return err + if req, err := chartutil.LoadRequirements(ch); err == nil { + if err := renderutil.CheckDependencies(ch, req); err != nil { + if u.reqFile != "" { + man := &downloader.Manager{ + Out: u.out, + ChartPath: chartPath, + HelmHome: settings.Home, + Keyring: defaultKeyring(), + SkipUpdate: false, + Getters: getter.All(settings), + Requirements: req, + } + if err := man.Update(); err != nil { + return prettyError(err) + } + + // Update all dependencies which are present in /charts. + ch, err = chartutil.Load(chartPath) + if err != nil { + return prettyError(err) + } + if err := chartutil.SetRequirements(ch, u.reqFile); err != nil { + return prettyError(err) + } + if err := chartutil.RemoveDependenciesNotPresent(ch); err != nil { + return prettyError(err) + } + } else { + return prettyError(err) } - } else if err != chartutil.ErrRequirementsNotFound { - return fmt.Errorf("cannot load requirements: %v", err) } - } else { - return prettyError(err) + } else if err != chartutil.ErrRequirementsNotFound { + return fmt.Errorf("cannot load requirements: %v", err) } - resp, err := u.client.UpdateRelease( + resp, err := u.client.UpdateReleaseFromChart( u.release, - chartPath, + ch, helm.UpdateValueOverrides(rawVals), helm.UpgradeDryRun(u.dryRun), helm.UpgradeRecreate(u.recreate), diff --git a/docs/helm/helm_install.md b/docs/helm/helm_install.md index 05cdf1e4a..6fecc37f1 100644 --- a/docs/helm/helm_install.md +++ b/docs/helm/helm_install.md @@ -95,6 +95,7 @@ helm install [CHART] [flags] --password string chart repository password where to locate the requested chart --replace re-use the given name, even if that name is already used. This is unsafe in production --repo string chart repository url where to locate the requested chart + -r, --requirements string specify a yaml file to override dependencies(specify 'none' to remove all dependencies) --set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) --set-file stringArray set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) --set-string stringArray set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) @@ -128,4 +129,4 @@ helm install [CHART] [flags] * [helm](helm.md) - The Helm package manager for Kubernetes. -###### Auto generated by spf13/cobra on 10-Aug-2018 +###### Auto generated by spf13/cobra on 16-Oct-2018 diff --git a/docs/helm/helm_upgrade.md b/docs/helm/helm_upgrade.md index f18bcf6a7..0f71d3a68 100644 --- a/docs/helm/helm_upgrade.md +++ b/docs/helm/helm_upgrade.md @@ -80,6 +80,7 @@ helm upgrade [RELEASE] [CHART] [flags] --password string chart repository password where to locate the requested chart --recreate-pods performs pods restart for the resource if applicable --repo string chart repository url where to locate the requested chart + -r, --requirements string specify a yaml file to override dependencies(specify 'none' to remove all dependencies) --reset-values when upgrading, reset the values to the ones built into the chart --reuse-values when upgrading, reuse the last release's values and merge in any overrides from the command line via --set and -f. If '--reset-values' is specified, this is ignored. --set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) @@ -115,4 +116,4 @@ helm upgrade [RELEASE] [CHART] [flags] * [helm](helm.md) - The Helm package manager for Kubernetes. -###### Auto generated by spf13/cobra on 24-Aug-2018 +###### Auto generated by spf13/cobra on 16-Oct-2018 diff --git a/pkg/chartutil/requirements.go b/pkg/chartutil/requirements.go index 566123122..618f9b43b 100644 --- a/pkg/chartutil/requirements.go +++ b/pkg/chartutil/requirements.go @@ -17,6 +17,8 @@ package chartutil import ( "errors" + "fmt" + "io/ioutil" "log" "strings" "time" @@ -27,8 +29,9 @@ import ( ) const ( - requirementsName = "requirements.yaml" - lockfileName = "requirements.lock" + requirementsName = "requirements.yaml" + lockfileName = "requirements.lock" + removeRequirementsName = "none" ) var ( @@ -123,6 +126,46 @@ func LoadRequirementsLock(c *chart.Chart) (*RequirementsLock, error) { return r, yaml.Unmarshal(data, r) } +// SetRequirements sets a requirements file from an in-memory chart. +func SetRequirements(c *chart.Chart, filename string) error { + var data []byte + + if filename == removeRequirementsName { + data = []byte("dependencies:") + } else { + var err error + data, err = ioutil.ReadFile(filename) + if err != nil { + return fmt.Errorf("error reading %s: %s", filename, err) + } + } + for _, f := range c.Files { + if f.TypeUrl == requirementsName { + f.Value = data + } + } + + return nil +} + +// RemoveDependenciesNotPresent removes the charts not present in requirements +func RemoveDependenciesNotPresent(c *chart.Chart) error { + var deps []*chart.Chart + req, err := LoadRequirements(c) + if err != nil { + return err + } + for _, depInRequestments := range req.Dependencies { + for _, dep := range c.Dependencies { + if depInRequestments.Name == dep.Metadata.Name { + deps = append(deps, dep) + } + } + } + c.Dependencies = deps + return nil +} + // ProcessRequirementsConditions disables charts based on condition path value in values func ProcessRequirementsConditions(reqs *Requirements, cvals Values) { var cond string diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index 67f9dc7bf..ea2231356 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -57,6 +57,8 @@ type Manager struct { SkipUpdate bool // Getter collection for the operation Getters []getter.Provider + // Requirements is the requirements override + Requirements *chartutil.Requirements } // Build rebuilds a local charts directory from a lockfile. @@ -113,15 +115,20 @@ func (m *Manager) Update() error { return err } - // If no requirements file is found, we consider this a successful - // completion. - req, err := chartutil.LoadRequirements(c) - if err != nil { - if err == chartutil.ErrRequirementsNotFound { - fmt.Fprintf(m.Out, "No requirements found in %s/charts.\n", m.ChartPath) - return nil + var req *chartutil.Requirements + if m.Requirements == nil { + // If no requirements file is found, we consider this a successful + // completion. + req, err = chartutil.LoadRequirements(c) + if err != nil { + if err == chartutil.ErrRequirementsNotFound { + fmt.Fprintf(m.Out, "No requirements found in %s/charts.\n", m.ChartPath) + return nil + } + return err } - return err + } else { + req = m.Requirements } // Hash requirements.yaml