From f9d8645c173af224742eac3c21153504d91ef65f Mon Sep 17 00:00:00 2001 From: Matheus Hunsche Date: Fri, 2 Oct 2020 01:58:48 -0300 Subject: [PATCH] add include file Signed-off-by: Matheus Hunsche --- cmd/helm/flags.go | 6 ++ cmd/helm/helm.go | 5 ++ cmd/helm/install.go | 51 +++++++++++ cmd/helm/install_test.go | 18 ++++ cmd/helm/template_test.go | 15 ++++ cmd/helm/testdata/files/external.1.conf | 1 + cmd/helm/testdata/files/external.2.conf | 1 + cmd/helm/testdata/files/external.txt | 1 + .../output/install-with-external-files.txt | 6 ++ .../output/template-with-external-dir.txt | 22 +++++ .../output/template-with-external-file.txt | 20 +++++ .../output/template-with-external-glob.txt | 21 +++++ .../testdata/testcharts/external/Chart.yaml | 8 ++ .../testdata/testcharts/external/external.txt | 1 + .../external/templates/config-map.yaml | 23 +++++ .../testdata/testcharts/external/values.yaml | 4 + cmd/helm/upgrade.go | 7 ++ cmd/helm/upgrade_test.go | 69 +++++++++++++++ pkg/action/install.go | 2 + pkg/action/upgrade.go | 2 + pkg/chart/loader/local.go | 85 +++++++++++++++++++ pkg/chart/loader/local_test.go | 46 ++++++++++ pkg/cli/files/files.go | 20 +++++ pkg/cli/files/parser.go | 66 ++++++++++++++ pkg/cli/files/parser_test.go | 76 +++++++++++++++++ pkg/cli/files/testdata/foo/bar.txt | 1 + pkg/cli/files/testdata/foo/foo.txt | 1 + 27 files changed, 578 insertions(+) create mode 100644 cmd/helm/testdata/files/external.1.conf create mode 100644 cmd/helm/testdata/files/external.2.conf create mode 100644 cmd/helm/testdata/files/external.txt create mode 100644 cmd/helm/testdata/output/install-with-external-files.txt create mode 100644 cmd/helm/testdata/output/template-with-external-dir.txt create mode 100644 cmd/helm/testdata/output/template-with-external-file.txt create mode 100644 cmd/helm/testdata/output/template-with-external-glob.txt create mode 100644 cmd/helm/testdata/testcharts/external/Chart.yaml create mode 100644 cmd/helm/testdata/testcharts/external/external.txt create mode 100644 cmd/helm/testdata/testcharts/external/templates/config-map.yaml create mode 100644 cmd/helm/testdata/testcharts/external/values.yaml create mode 100644 pkg/chart/loader/local.go create mode 100644 pkg/chart/loader/local_test.go create mode 100644 pkg/cli/files/files.go create mode 100644 pkg/cli/files/parser.go create mode 100644 pkg/cli/files/parser_test.go create mode 100644 pkg/cli/files/testdata/foo/bar.txt create mode 100644 pkg/cli/files/testdata/foo/foo.txt diff --git a/cmd/helm/flags.go b/cmd/helm/flags.go index 76d6e0476..6453e523d 100644 --- a/cmd/helm/flags.go +++ b/cmd/helm/flags.go @@ -29,6 +29,7 @@ import ( "k8s.io/klog/v2" "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/cli/files" "helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v3/pkg/cli/values" "helm.sh/helm/v3/pkg/helmpath" @@ -64,6 +65,11 @@ func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { f.BoolVar(&c.PassCredentialsAll, "pass-credentials", false, "pass credentials to all domains") } +func addExternalFilesFlags(f *pflag.FlagSet, v *files.ExternalFiles) { + f.StringArrayVar(&v.Files, "include-file", []string{}, "paths to local files to add during chart installation") + f.StringArrayVar(&v.Globs, "include-dir", []string{}, "paths or globs to local dirs to add during chart installation") +} + // bindOutputFlag will add the output flag to the given command and bind the // value to the given format pointer func bindOutputFlag(cmd *cobra.Command, varRef *output.Format) { diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 15b0d5c76..a98af62e4 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -80,6 +80,11 @@ func main() { } }) + if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), os.Getenv("HELM_DRIVER"), debug); err != nil { + debug("%+v", err) + os.Exit(1) + } + if err := cmd.Execute(); err != nil { debug("%+v", err) switch e := err.(type) { diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 044d1f2b5..53da84b86 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -20,9 +20,11 @@ import ( "context" "fmt" "io" + "io/ioutil" "log" "os" "os/signal" + "strings" "syscall" "time" @@ -34,6 +36,7 @@ import ( "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart/loader" + "helm.sh/helm/v3/pkg/cli/files" "helm.sh/helm/v3/pkg/cli/output" "helm.sh/helm/v3/pkg/cli/values" "helm.sh/helm/v3/pkg/downloader" @@ -171,6 +174,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent") addValueOptionsFlags(f, valueOpts) addChartPathOptionsFlags(f, &client.ChartPathOptions) + addExternalFilesFlags(f, &client.ExternalFiles) err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { requiredArgs := 2 @@ -258,6 +262,11 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options } } + err = loadExternalFiles(chartRequested, client.ExternalFiles) + if err != nil { + return nil, err + } + client.Namespace = settings.Namespace() // Create context and prepare the handle of SIGTERM @@ -289,6 +298,48 @@ func checkIfInstallable(ch *chart.Chart) error { return errors.Errorf("%s charts are not installable", ch.Metadata.Type) } +func loadExternalFiles(ch *chart.Chart, exFiles files.ExternalFiles) error { + var errs []string + fs := make(map[string]string) + + for _, s := range exFiles.Files { + if err := files.ParseIntoString(s, fs); err != nil { + debug(fmt.Sprintf("error parsing include-file option %s: %v", s, err)) + errs = append(errs, fmt.Sprintf("%s (parse error)", s)) + } + } + + for _, g := range exFiles.Globs { + if err := files.ParseIntoString(g, fs); err != nil { + debug(fmt.Sprintf("error parsing include-dir option %s: %v", g, err)) + errs = append(errs, fmt.Sprintf("%s (parse error)", g)) + } + } + + for n, p := range fs { + allPaths, err := loader.ExpandLocalPath(n, p) + debug(fmt.Sprintf("%s expanded to: %v", p, allPaths)) + if err != nil { + debug(fmt.Sprintf("error loading external path %s: %v", p, err)) + errs = append(errs, fmt.Sprintf("%s (path not accessible)", p)) + } + + for name, fp := range allPaths { + byt, err := ioutil.ReadFile(fp) + if err != nil { + errs = append(errs, fmt.Sprintf("%s (not readable)", fp)) + } else { + ch.Files = append(ch.Files, &chart.File{Name: name, Data: byt}) + } + } + } + + if len(errs) > 0 { + return errors.New(fmt.Sprint("Failed to load external files: ", strings.Join(errs, "; "))) + } + return nil +} + // Provide dynamic auto-completion for the install and template commands func compInstall(args []string, toComplete string, client *action.Install) ([]string, cobra.ShellCompDirective) { requiredArgs := 1 diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go index b34d1455c..a1a97445d 100644 --- a/cmd/helm/install_test.go +++ b/cmd/helm/install_test.go @@ -77,6 +77,24 @@ func TestInstall(t *testing.T) { cmd: "install virgil testdata/testcharts/alpine -f testdata/testcharts/alpine/extra_values.yaml", golden: "output/install-with-values-file.txt", }, + // Install, external file + { + name: "install with external files", + cmd: "install virgil testdata/testcharts/external --include-file external.txt=testdata/files/external.txt", + golden: "output/install-with-external-files.txt", + }, + // Install, external dir + { + name: "install with external dir", + cmd: "install virgil testdata/testcharts/external --set glob.enabled=true --include-dir glob=testdata/files/", + golden: "output/install-with-external-files.txt", + }, + // Install, external glob files + { + name: "install with external globbed files", + cmd: "install virgil testdata/testcharts/external --set glob.enabled=true --include-dir glob=testdata/files/external.*.conf", + golden: "output/install-with-external-files.txt", + }, // Install, no hooks { name: "install without hooks", diff --git a/cmd/helm/template_test.go b/cmd/helm/template_test.go index d1f17fe98..91a68acec 100644 --- a/cmd/helm/template_test.go +++ b/cmd/helm/template_test.go @@ -131,6 +131,21 @@ func TestTemplateCmd(t *testing.T) { cmd: fmt.Sprintf(`template '%s' --skip-tests`, chartPath), golden: "output/template-skip-tests.txt", }, + { + name: "chart with template with external file", + cmd: fmt.Sprintf("template '%s' --set external=external.txt --include-file external.txt=testdata/files/external.txt", "testdata/testcharts/external"), + golden: "output/template-with-external-file.txt", + }, + { + name: "chart with template with external dir", + cmd: fmt.Sprintf("template '%s' --set glob.enabled=true --include-dir glob=testdata/files/", "testdata/testcharts/external"), + golden: "output/template-with-external-dir.txt", + }, + { + name: "chart with template with external globbed files", + cmd: fmt.Sprintf("template '%s' --set glob.enabled=true --include-dir glob=testdata/files/external.*.conf", "testdata/testcharts/external"), + golden: "output/template-with-external-glob.txt", + }, } runTestCmd(t, tests) } diff --git a/cmd/helm/testdata/files/external.1.conf b/cmd/helm/testdata/files/external.1.conf new file mode 100644 index 000000000..065cf1bd9 --- /dev/null +++ b/cmd/helm/testdata/files/external.1.conf @@ -0,0 +1 @@ +glob-external-1 diff --git a/cmd/helm/testdata/files/external.2.conf b/cmd/helm/testdata/files/external.2.conf new file mode 100644 index 000000000..92c587e84 --- /dev/null +++ b/cmd/helm/testdata/files/external.2.conf @@ -0,0 +1 @@ +glob-external-2 diff --git a/cmd/helm/testdata/files/external.txt b/cmd/helm/testdata/files/external.txt new file mode 100644 index 000000000..068c4db58 --- /dev/null +++ b/cmd/helm/testdata/files/external.txt @@ -0,0 +1 @@ +out-of-chart-dir diff --git a/cmd/helm/testdata/output/install-with-external-files.txt b/cmd/helm/testdata/output/install-with-external-files.txt new file mode 100644 index 000000000..406e522a9 --- /dev/null +++ b/cmd/helm/testdata/output/install-with-external-files.txt @@ -0,0 +1,6 @@ +NAME: virgil +LAST DEPLOYED: Fri Sep 2 22:04:05 1977 +NAMESPACE: default +STATUS: deployed +REVISION: 1 +TEST SUITE: None diff --git a/cmd/helm/testdata/output/template-with-external-dir.txt b/cmd/helm/testdata/output/template-with-external-dir.txt new file mode 100644 index 000000000..34645fde8 --- /dev/null +++ b/cmd/helm/testdata/output/template-with-external-dir.txt @@ -0,0 +1,22 @@ +--- +# Source: configmap/templates/config-map.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: "RELEASE-NAME-" + labels: + # The "app.kubernetes.io/managed-by" label is used to track which tool + # deployed a given chart. It is useful for admins who want to see what + # releases a particular tool is responsible for. + app.kubernetes.io/managed-by: "Helm" + # The "app.kubernetes.io/instance" convention makes it easy to tie a release + # to all of the Kubernetes resources that were created as part of that + # release. + app.kubernetes.io/instance: "RELEASE-NAME" + app.kubernetes.io/version: 1.0 + # This makes it easy to audit chart usage. + helm.sh/chart: "configmap-0.1.0" +data: + external.1.conf: glob-external-1 + external.2.conf: glob-external-2 + external.txt: out-of-chart-dir diff --git a/cmd/helm/testdata/output/template-with-external-file.txt b/cmd/helm/testdata/output/template-with-external-file.txt new file mode 100644 index 000000000..9550768c0 --- /dev/null +++ b/cmd/helm/testdata/output/template-with-external-file.txt @@ -0,0 +1,20 @@ +--- +# Source: configmap/templates/config-map.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: "RELEASE-NAME-" + labels: + # The "app.kubernetes.io/managed-by" label is used to track which tool + # deployed a given chart. It is useful for admins who want to see what + # releases a particular tool is responsible for. + app.kubernetes.io/managed-by: "Helm" + # The "app.kubernetes.io/instance" convention makes it easy to tie a release + # to all of the Kubernetes resources that were created as part of that + # release. + app.kubernetes.io/instance: "RELEASE-NAME" + app.kubernetes.io/version: 1.0 + # This makes it easy to audit chart usage. + helm.sh/chart: "configmap-0.1.0" +data: + external.txt: out-of-chart-dir diff --git a/cmd/helm/testdata/output/template-with-external-glob.txt b/cmd/helm/testdata/output/template-with-external-glob.txt new file mode 100644 index 000000000..7796e1c7e --- /dev/null +++ b/cmd/helm/testdata/output/template-with-external-glob.txt @@ -0,0 +1,21 @@ +--- +# Source: configmap/templates/config-map.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: "RELEASE-NAME-" + labels: + # The "app.kubernetes.io/managed-by" label is used to track which tool + # deployed a given chart. It is useful for admins who want to see what + # releases a particular tool is responsible for. + app.kubernetes.io/managed-by: "Helm" + # The "app.kubernetes.io/instance" convention makes it easy to tie a release + # to all of the Kubernetes resources that were created as part of that + # release. + app.kubernetes.io/instance: "RELEASE-NAME" + app.kubernetes.io/version: 1.0 + # This makes it easy to audit chart usage. + helm.sh/chart: "configmap-0.1.0" +data: + external.1.conf: glob-external-1 + external.2.conf: glob-external-2 diff --git a/cmd/helm/testdata/testcharts/external/Chart.yaml b/cmd/helm/testdata/testcharts/external/Chart.yaml new file mode 100644 index 000000000..0b070f43e --- /dev/null +++ b/cmd/helm/testdata/testcharts/external/Chart.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +appVersion: "1.0" +description: Deploy a basic Config Map from an external file +home: https://helm.sh/helm +name: configmap +sources: +- https://github.com/helm/helm +version: 0.1.0 diff --git a/cmd/helm/testdata/testcharts/external/external.txt b/cmd/helm/testdata/testcharts/external/external.txt new file mode 100644 index 000000000..d8e8e4de4 --- /dev/null +++ b/cmd/helm/testdata/testcharts/external/external.txt @@ -0,0 +1 @@ +in-chart diff --git a/cmd/helm/testdata/testcharts/external/templates/config-map.yaml b/cmd/helm/testdata/testcharts/external/templates/config-map.yaml new file mode 100644 index 000000000..f644c307e --- /dev/null +++ b/cmd/helm/testdata/testcharts/external/templates/config-map.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: "{{.Release.Name}}-{{.Values.Name}}" + labels: + # The "app.kubernetes.io/managed-by" label is used to track which tool + # deployed a given chart. It is useful for admins who want to see what + # releases a particular tool is responsible for. + app.kubernetes.io/managed-by: {{.Release.Service | quote }} + # The "app.kubernetes.io/instance" convention makes it easy to tie a release + # to all of the Kubernetes resources that were created as part of that + # release. + app.kubernetes.io/instance: {{.Release.Name | quote }} + app.kubernetes.io/version: {{ .Chart.AppVersion }} + # This makes it easy to audit chart usage. + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" +data: +{{- if .Values.external }} +{{ (.Files.Glob .Values.external).AsConfig | indent 2 }} +{{- end }} +{{- if .Values.glob.enabled }} +{{ (.Files.Glob .Values.glob.path).AsConfig | indent 2 }} +{{- end }} diff --git a/cmd/helm/testdata/testcharts/external/values.yaml b/cmd/helm/testdata/testcharts/external/values.yaml new file mode 100644 index 000000000..ee4f44aed --- /dev/null +++ b/cmd/helm/testdata/testcharts/external/values.yaml @@ -0,0 +1,4 @@ +external: false +glob: + enabled: false + path: "glob/*" diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 2ceb31a81..45a35c679 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -116,6 +116,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient.PostRenderer = client.PostRenderer instClient.DisableOpenAPIValidation = client.DisableOpenAPIValidation instClient.SubNotes = client.SubNotes + instClient.ExternalFiles = client.ExternalFiles instClient.Description = client.Description instClient.DependencyUpdate = client.DependencyUpdate @@ -181,6 +182,11 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { warning("This chart is deprecated") } + err = loadExternalFiles(ch, client.ExternalFiles) + if err != nil { + return err + } + // Create context and prepare the handle of SIGTERM ctx := context.Background() ctx, cancel := context.WithCancel(ctx) @@ -235,6 +241,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { addValueOptionsFlags(f, valueOpts) bindOutputFlag(cmd, &outfmt) bindPostRenderFlag(cmd, &client.PostRenderer) + addExternalFilesFlags(f, &client.ExternalFiles) err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) != 2 { diff --git a/cmd/helm/upgrade_test.go b/cmd/helm/upgrade_test.go index 8afcb139b..26a459a79 100644 --- a/cmd/helm/upgrade_test.go +++ b/cmd/helm/upgrade_test.go @@ -357,6 +357,38 @@ func TestUpgradeInstallWithValuesFromStdin(t *testing.T) { } +func TestUpgradeWithExternalFile(t *testing.T) { + + releaseName := "funny-bunny-v7" + + exFiles := []*chart.File{ + {Name: "external.txt", Data: []byte("from-external-file")}, + } + + relMock, ch, chartPath := prepareMockReleaseWithExternal(releaseName, exFiles, t) + + defer resetEnv()() + + store := storageFixture() + + store.Create(relMock(releaseName, 3, ch)) + + cmd := fmt.Sprintf("upgrade %s --set glob.enabled=false --set external=external.txt '%s'", releaseName, chartPath) + _, _, err := executeActionCommandC(store, cmd) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + updatedRel, err := store.Get(releaseName, 4) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + + if !strings.Contains(updatedRel.Manifest, "from-external-file") { + t.Errorf("The value is not set correctly. manifest: %s", updatedRel.Manifest) + } +} + func prepareMockRelease(releaseName string, t *testing.T) (func(n string, v int, ch *chart.Chart) *release.Release, *chart.Chart, string) { tmpChart := ensure.TempDir(t) configmapData, err := ioutil.ReadFile("testdata/testcharts/upgradetest/templates/configmap.yaml") @@ -392,6 +424,43 @@ func prepareMockRelease(releaseName string, t *testing.T) (func(n string, v int, return relMock, ch, chartPath } +func prepareMockReleaseWithExternal(releaseName string, exFiles []*chart.File, t *testing.T) (func(n string, v int, ch *chart.Chart) *release.Release, *chart.Chart, string) { + tmpChart := ensure.TempDir(t) + configmapData, err := ioutil.ReadFile("testdata/testcharts/external/templates/config-map.yaml") + + if err != nil { + t.Fatalf("Error loading template yaml %v", err) + } + cfile := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "testUpgradeChart", + Description: "A Helm chart for Kubernetes", + Version: "0.1.0", + }, + Templates: []*chart.File{{Name: "templates/configmap.yaml", Data: configmapData}}, + Files: exFiles, + } + chartPath := filepath.Join(tmpChart, cfile.Metadata.Name) + if err := chartutil.SaveDir(cfile, tmpChart); err != nil { + t.Fatalf("Error creating chart for upgrade: %v", err) + } + ch, err := loader.Load(chartPath) + if err != nil { + t.Fatalf("Error loading chart: %v", err) + } + _ = release.Mock(&release.MockReleaseOptions{ + Name: releaseName, + Chart: ch, + }) + + relMock := func(n string, v int, ch *chart.Chart) *release.Release { + return release.Mock(&release.MockReleaseOptions{Name: n, Version: v, Chart: ch}) + } + + return relMock, ch, chartPath +} + func TestUpgradeOutputCompletion(t *testing.T) { outputFlagCompletionTest(t, "upgrade") } diff --git a/pkg/action/install.go b/pkg/action/install.go index fa5508234..92e14ce9e 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -41,6 +41,7 @@ import ( "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chartutil" "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/cli/files" "helm.sh/helm/v3/pkg/downloader" "helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/kube" @@ -100,6 +101,7 @@ type Install struct { // OutputDir/ UseReleaseName bool PostRenderer postrender.PostRenderer + ExternalFiles files.ExternalFiles // Lock to control raceconditions when the process receives a SIGTERM Lock sync.Mutex } diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 690397d4a..e2befe2a1 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -30,6 +30,7 @@ import ( "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chartutil" + "helm.sh/helm/v3/pkg/cli/files" "helm.sh/helm/v3/pkg/kube" "helm.sh/helm/v3/pkg/postrender" "helm.sh/helm/v3/pkg/release" @@ -101,6 +102,7 @@ type Upgrade struct { DisableOpenAPIValidation bool // Get missing dependencies DependencyUpdate bool + ExternalFiles files.ExternalFiles // Lock to control raceconditions when the process receives a SIGTERM Lock sync.Mutex } diff --git a/pkg/chart/loader/local.go b/pkg/chart/loader/local.go new file mode 100644 index 000000000..9537c2742 --- /dev/null +++ b/pkg/chart/loader/local.go @@ -0,0 +1,85 @@ +/* +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 loader + +import ( + "errors" + "os" + "path/filepath" + "strings" +) + +// ExpandLocalPath expands a local file, dir or glob path to a list of files +func ExpandLocalPath(name string, path string) (map[string]string, error) { + if strings.Contains(path, "*") { + // if this is a glob, we expand it and return a list of files + return expandGlob(name, path) + } + + fi, err := os.Stat(path) + if err != nil { + return nil, err + } + + if fi.IsDir() { + // if this is a valid dir, we return all files within + return expandDir(name, path) + } + + // finally, this is a file, so we return it + return map[string]string{name: path}, nil +} + +func expandGlob(name string, path string) (map[string]string, error) { + fmap := make(map[string]string) + paths, err := filepath.Glob(path) + if err != nil { + return nil, err + } + if len(paths) == 0 { + return nil, errors.New("empty glob") + } + + namePrefix := strings.TrimRight(name, "/") + "/" + for _, p := range paths { + key := namePrefix + filepath.Base(p) + fmap[key] = p + } + + return fmap, nil +} + +func expandDir(name string, path string) (map[string]string, error) { + fmap := make(map[string]string) + + f, err := os.Open(path) + + if err != nil { + return nil, err + } + defer f.Close() + + files, err := f.Readdir(-1) + if err != nil { + return nil, err + } + + localDirName := strings.TrimRight(path, "/") + "/" + namePrefix := strings.TrimRight(name, "/") + "/" + for _, file := range files { + key := namePrefix + file.Name() + fmap[key] = localDirName + file.Name() + } + return fmap, nil +} diff --git a/pkg/chart/loader/local_test.go b/pkg/chart/loader/local_test.go new file mode 100644 index 000000000..79b81f9fe --- /dev/null +++ b/pkg/chart/loader/local_test.go @@ -0,0 +1,46 @@ +/* +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 loader + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExpandLocalPath(t *testing.T) { + need := require.New(t) + is := assert.New(t) + + glob, err := ExpandLocalPath("glob", "testdata/frobnitz/*.yaml") + need.NoError(err) + need.Contains(glob, "glob/Chart.yaml") + need.Contains(glob, "glob/values.yaml") + is.Equal("testdata/frobnitz/Chart.yaml", glob["glob/Chart.yaml"]) + is.Equal("testdata/frobnitz/values.yaml", glob["glob/values.yaml"]) + + dir, err := ExpandLocalPath("dir", "testdata/albatross/") + need.NoError(err) + need.Contains(dir, "dir/Chart.yaml") + need.Contains(dir, "dir/values.yaml") + is.Equal("testdata/albatross/Chart.yaml", dir["dir/Chart.yaml"]) + is.Equal("testdata/albatross/values.yaml", dir["dir/values.yaml"]) + + file, err := ExpandLocalPath("file", "testdata/albatross/Chart.yaml") + need.NoError(err) + need.Contains(file, "file") + is.Equal("testdata/albatross/Chart.yaml", file["file"]) + +} diff --git a/pkg/cli/files/files.go b/pkg/cli/files/files.go new file mode 100644 index 000000000..f8f781742 --- /dev/null +++ b/pkg/cli/files/files.go @@ -0,0 +1,20 @@ +/* +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 files + +// ExternalFiles holds the list of external files or globs +type ExternalFiles struct { + Files []string + Globs []string +} diff --git a/pkg/cli/files/parser.go b/pkg/cli/files/parser.go new file mode 100644 index 000000000..d3dd68345 --- /dev/null +++ b/pkg/cli/files/parser.go @@ -0,0 +1,66 @@ +/* +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 files + +import ( + "errors" + "fmt" + "path/filepath" + "strings" +) + +// ParseIntoString parses a include-file line and merges the result into dest. +func ParseIntoString(s string, dest map[string]string) error { + for _, val := range strings.Split(s, ",") { + val = strings.TrimSpace(val) + splt := strings.SplitN(val, "=", 2) + + if len(splt) != 2 { + return errors.New("Could not parse line") + } + + name := strings.TrimSpace(splt[0]) + path := strings.TrimSpace(splt[1]) + dest[name] = path + } + + return nil +} + +//ParseGlobIntoString parses an include-dir file line and merges all files found into dest. +func ParseGlobIntoString(g string, dest map[string]string) error { + globs := make(map[string]string) + err := ParseIntoString(g, globs) + if err != nil { + return err + } + for k, g := range globs { + if !strings.Contains(g, "*") { + // force glob style on simple directories + g = strings.TrimRight(g, "/") + "/*" + } + + paths, err := filepath.Glob(g) + if err != nil { + return err + } + + k = strings.TrimRight(k, "/") + for _, path := range paths { + dest[fmt.Sprintf("%s/%s", k, filepath.Base(path))] = path + } + } + + return nil +} diff --git a/pkg/cli/files/parser_test.go b/pkg/cli/files/parser_test.go new file mode 100644 index 000000000..ac3a9d46f --- /dev/null +++ b/pkg/cli/files/parser_test.go @@ -0,0 +1,76 @@ +/* +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 files + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseIntoString(t *testing.T) { + need := require.New(t) + is := assert.New(t) + + dest := make(map[string]string) + goodFlag := "foo.txt=../foo.txt" + anotherFlag := " bar.txt=~/bar.txt, baz.txt=/path/to/baz.txt" + + err := ParseIntoString(goodFlag, dest) + need.NoError(err) + + err = ParseIntoString(anotherFlag, dest) + need.NoError(err) + + is.Contains(dest, "foo.txt") + is.Contains(dest, "bar.txt") + is.Contains(dest, "baz.txt") + + is.Equal(dest["foo.txt"], "../foo.txt", "foo.txt not mapped properly") + is.Equal(dest["bar.txt"], "~/bar.txt", "bar.txt not mapped properly") + is.Equal(dest["baz.txt"], "/path/to/baz.txt", "baz.txt not mapped properly") + + overwriteFlag := "foo.txt=../new_foo.txt" + err = ParseIntoString(overwriteFlag, dest) + need.NoError(err) + + is.Equal(dest["foo.txt"], "../new_foo.txt") + + badFlag := "empty.txt" + err = ParseIntoString(badFlag, dest) + is.NotNil(err) +} + +func TestParseGlobIntoString(t *testing.T) { + need := require.New(t) + is := assert.New(t) + + dest := make(map[string]string) + globFlagSlash := "glob/=testdata/foo/foo.*" + dirFlagNoSlash := "dir=testdata/foo/" + + err := ParseGlobIntoString(globFlagSlash, dest) + need.NoError(err) + need.Contains(dest, "glob/foo.txt") + is.Equal("testdata/foo/foo.txt", dest["glob/foo.txt"]) + + err = ParseGlobIntoString(dirFlagNoSlash, dest) + need.NoError(err) + need.Contains(dest, "dir/foo.txt") + need.Contains(dest, "dir/bar.txt") + is.Equal("testdata/foo/foo.txt", dest["dir/foo.txt"]) + is.Equal("testdata/foo/bar.txt", dest["dir/bar.txt"]) +} diff --git a/pkg/cli/files/testdata/foo/bar.txt b/pkg/cli/files/testdata/foo/bar.txt new file mode 100644 index 000000000..5716ca598 --- /dev/null +++ b/pkg/cli/files/testdata/foo/bar.txt @@ -0,0 +1 @@ +bar diff --git a/pkg/cli/files/testdata/foo/foo.txt b/pkg/cli/files/testdata/foo/foo.txt new file mode 100644 index 000000000..257cc5642 --- /dev/null +++ b/pkg/cli/files/testdata/foo/foo.txt @@ -0,0 +1 @@ +foo