diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 5ef9ba25d..da6e13b14 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -18,26 +18,20 @@ package main import ( "io" - "io/ioutil" - "net/url" - "os" - "strings" "time" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" - "sigs.k8s.io/yaml" "helm.sh/helm/cmd/helm/require" "helm.sh/helm/pkg/action" "helm.sh/helm/pkg/chart" "helm.sh/helm/pkg/chart/loader" - "helm.sh/helm/pkg/cli" + "helm.sh/helm/pkg/cli/values" "helm.sh/helm/pkg/downloader" "helm.sh/helm/pkg/getter" "helm.sh/helm/pkg/release" - "helm.sh/helm/pkg/strvals" ) const installDesc = ` @@ -104,7 +98,7 @@ charts in a repository, use 'helm search'. func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client := action.NewInstall(cfg) - valueOpts := &ValueOptions{} + valueOpts := &values.Options{} cmd := &cobra.Command{ Use: "install [NAME] [CHART]", @@ -126,7 +120,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return cmd } -func addInstallFlags(f *pflag.FlagSet, client *action.Install, valueOpts *ValueOptions) { +func addInstallFlags(f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) { f.BoolVar(&client.DryRun, "dry-run", false, "simulate an 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, even if that name is already used. This is unsafe in production") @@ -141,7 +135,7 @@ func addInstallFlags(f *pflag.FlagSet, client *action.Install, valueOpts *ValueO addChartPathOptionsFlags(f, &client.ChartPathOptions) } -func addValueOptionsFlags(f *pflag.FlagSet, v *ValueOptions) { +func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) { f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL(can specify multiple)") f.StringArrayVar(&v.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") f.StringArrayVar(&v.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") @@ -159,7 +153,7 @@ func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") } -func runInstall(args []string, client *action.Install, valueOpts *ValueOptions, out io.Writer) (*release.Release, error) { +func runInstall(args []string, client *action.Install, valueOpts *values.Options, out io.Writer) (*release.Release, error) { debug("Original chart version: %q", client.Version) if client.Version == "" && client.Devel { debug("setting version to >0.0.0-0") @@ -232,89 +226,3 @@ func isChartInstallable(ch *chart.Chart) (bool, error) { } return false, errors.Errorf("%s charts are not installable", ch.Metadata.Type) } - -type ValueOptions struct { - ValueFiles []string - StringValues []string - Values []string -} - -// MergeValues merges values from files specified via -f/--values and -// directly via --set or --set-string, marshaling them to YAML -func (v *ValueOptions) MergeValues(settings cli.EnvSettings) (map[string]interface{}, error) { - base := map[string]interface{}{} - - // User specified a values files via -f/--values - for _, filePath := range v.ValueFiles { - currentMap := map[string]interface{}{} - - bytes, err := readFile(filePath, settings) - if err != nil { - return nil, err - } - - if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { - return nil, errors.Wrapf(err, "failed to parse %s", filePath) - } - // Merge with the previous map - base = mergeMaps(base, currentMap) - } - - // User specified a value via --set - for _, value := range v.Values { - if err := strvals.ParseInto(value, base); err != nil { - return nil, errors.Wrap(err, "failed parsing --set data") - } - } - - // User specified a value via --set-string - for _, value := range v.StringValues { - if err := strvals.ParseIntoString(value, base); err != nil { - return nil, errors.Wrap(err, "failed parsing --set-string data") - } - } - - return base, nil -} - -func mergeMaps(a, b map[string]interface{}) map[string]interface{} { - out := make(map[string]interface{}, len(a)) - for k, v := range a { - out[k] = v - } - for k, v := range b { - if v, ok := v.(map[string]interface{}); ok { - if bv, ok := out[k]; ok { - if bv, ok := bv.(map[string]interface{}); ok { - out[k] = mergeMaps(bv, v) - continue - } - } - } - out[k] = v - } - return out -} - -// readFile load a file from stdin, the local directory, or a remote file with a url. -func readFile(filePath string, settings cli.EnvSettings) ([]byte, error) { - if strings.TrimSpace(filePath) == "-" { - return ioutil.ReadAll(os.Stdin) - } - u, _ := url.Parse(filePath) - p := getter.All(settings) - - // FIXME: maybe someone handle other protocols like ftp. - getterConstructor, err := p.ByScheme(u.Scheme) - - if err != nil { - return ioutil.ReadFile(filePath) - } - - getter, err := getterConstructor(getter.WithURL(filePath)) - if err != nil { - return []byte{}, err - } - data, err := getter.Get(filePath) - return data.Bytes(), err -} diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go index cf8802668..bab1ebb7c 100644 --- a/cmd/helm/lint.go +++ b/cmd/helm/lint.go @@ -25,6 +25,7 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/pkg/action" + "helm.sh/helm/pkg/cli/values" ) var longLintHelp = ` @@ -38,7 +39,7 @@ or recommendation, it will emit [WARNING] messages. func newLintCmd(out io.Writer) *cobra.Command { client := action.NewLint() - valueOpts := &ValueOptions{} + valueOpts := &values.Options{} cmd := &cobra.Command{ Use: "lint PATH", diff --git a/cmd/helm/package.go b/cmd/helm/package.go index f73ee9916..48fcace1f 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -26,6 +26,7 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/pkg/action" + "helm.sh/helm/pkg/cli/values" "helm.sh/helm/pkg/downloader" "helm.sh/helm/pkg/getter" ) @@ -43,7 +44,7 @@ Versioned chart archives are used by Helm package repositories. func newPackageCmd(out io.Writer) *cobra.Command { client := action.NewPackage() - valueOpts := &ValueOptions{} + valueOpts := &values.Options{} cmd := &cobra.Command{ Use: "package [CHART_PATH] [...]", diff --git a/cmd/helm/template.go b/cmd/helm/template.go index 47462e28a..babfe0eac 100644 --- a/cmd/helm/template.go +++ b/cmd/helm/template.go @@ -25,6 +25,7 @@ import ( "helm.sh/helm/cmd/helm/require" "helm.sh/helm/pkg/action" + "helm.sh/helm/pkg/cli/values" ) const templateDesc = ` @@ -39,7 +40,7 @@ is done. func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { var validate bool client := action.NewInstall(cfg) - valueOpts := &ValueOptions{} + valueOpts := &values.Options{} cmd := &cobra.Command{ Use: "template [NAME] [CHART]", diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 9256a34f6..87d279950 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -27,6 +27,7 @@ import ( "helm.sh/helm/cmd/helm/require" "helm.sh/helm/pkg/action" "helm.sh/helm/pkg/chart/loader" + "helm.sh/helm/pkg/cli/values" "helm.sh/helm/pkg/storage/driver" ) @@ -57,7 +58,7 @@ set for a key called 'foo', the 'newbar' value would take precedence: func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client := action.NewUpgrade(cfg) - valueOpts := &ValueOptions{} + valueOpts := &values.Options{} cmd := &cobra.Command{ Use: "upgrade [RELEASE] [CHART]", diff --git a/pkg/cli/values/options.go b/pkg/cli/values/options.go new file mode 100644 index 000000000..86e83ab76 --- /dev/null +++ b/pkg/cli/values/options.go @@ -0,0 +1,117 @@ +/* +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 values + +import ( + "io/ioutil" + "net/url" + "os" + "strings" + + "github.com/pkg/errors" + "sigs.k8s.io/yaml" + + "helm.sh/helm/pkg/cli" + "helm.sh/helm/pkg/getter" + "helm.sh/helm/pkg/strvals" +) + +type Options struct { + ValueFiles []string + StringValues []string + Values []string +} + +// MergeValues merges values from files specified via -f/--values and +// directly via --set or --set-string, marshaling them to YAML +func (opts *Options) MergeValues(settings cli.EnvSettings) (map[string]interface{}, error) { + base := map[string]interface{}{} + + // User specified a values files via -f/--values + for _, filePath := range opts.ValueFiles { + currentMap := map[string]interface{}{} + + bytes, err := readFile(filePath, settings) + if err != nil { + return nil, err + } + + if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { + return nil, errors.Wrapf(err, "failed to parse %s", filePath) + } + // Merge with the previous map + base = mergeMaps(base, currentMap) + } + + // User specified a value via --set + for _, value := range opts.Values { + if err := strvals.ParseInto(value, base); err != nil { + return nil, errors.Wrap(err, "failed parsing --set data") + } + } + + // User specified a value via --set-string + for _, value := range opts.StringValues { + if err := strvals.ParseIntoString(value, base); err != nil { + return nil, errors.Wrap(err, "failed parsing --set-string data") + } + } + + return base, nil +} + +func mergeMaps(a, b map[string]interface{}) map[string]interface{} { + out := make(map[string]interface{}, len(a)) + for k, v := range a { + out[k] = v + } + for k, v := range b { + if v, ok := v.(map[string]interface{}); ok { + if bv, ok := out[k]; ok { + if bv, ok := bv.(map[string]interface{}); ok { + out[k] = mergeMaps(bv, v) + continue + } + } + } + out[k] = v + } + return out +} + +// readFile load a file from stdin, the local directory, or a remote file with a url. +func readFile(filePath string, settings cli.EnvSettings) ([]byte, error) { + if strings.TrimSpace(filePath) == "-" { + return ioutil.ReadAll(os.Stdin) + } + u, _ := url.Parse(filePath) + p := getter.All(settings) + + // FIXME: maybe someone handle other protocols like ftp. + getterConstructor, err := p.ByScheme(u.Scheme) + + if err != nil { + return ioutil.ReadFile(filePath) + } + + getter, err := getterConstructor(getter.WithURL(filePath)) + if err != nil { + return []byte{}, err + } + data, err := getter.Get(filePath) + return data.Bytes(), err +}