diff --git a/cmd/helm/template.go b/cmd/helm/template.go index 5c1ba33d1..320718344 100644 --- a/cmd/helm/template.go +++ b/cmd/helm/template.go @@ -132,6 +132,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVar(&client.IsUpgrade, "is-upgrade", false, "set .Release.IsUpgrade instead of .Release.IsInstall") f.StringArrayVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions") f.BoolVar(&client.UseReleaseName, "release-name", false, "use release name in the output-dir path.") + bindPostRenderFlag(cmd, &client.PostRenderer) return cmd } diff --git a/pkg/postrender/exec.go b/pkg/postrender/exec.go index dde30f6e3..bcf3ef64d 100644 --- a/pkg/postrender/exec.go +++ b/pkg/postrender/exec.go @@ -73,18 +73,27 @@ func (p *execRender) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer, error) // getFullPath returns the full filepath to the binary to execute. If the path // does not contain any separators, it will search first in the plugins -// directory, then in $PATH, otherwise it will resolve any relative paths to a -// fully qualified path +// directory (or directories if multiple are specified. In which case, it will +// return the first result), then in $PATH, otherwise it will resolve any +// relative paths to a fully qualified path func getFullPath(binaryPath string) (string, error) { // Manually check the plugin dir first if !strings.Contains(binaryPath, string(filepath.Separator)) { // First check the plugin dir - pluginDir := helmpath.DataPath("plugins") - _, err := os.Stat(filepath.Join(pluginDir, binaryPath)) - if err != nil && !os.IsNotExist(err) { - return "", err - } else if err == nil { - binaryPath = filepath.Join(pluginDir, binaryPath) + pluginDir := helmpath.DataPath("plugins") // Default location + // If location for plugins is explicitly set, check there + if v, ok := os.LookupEnv("HELM_PLUGINS"); ok { + pluginDir = v + } + // The plugins variable can actually contain multple paths, so loop through those + for _, p := range filepath.SplitList(pluginDir) { + _, err := os.Stat(filepath.Join(p, binaryPath)) + if err != nil && !os.IsNotExist(err) { + return "", err + } else if err == nil { + binaryPath = filepath.Join(p, binaryPath) + break + } } } diff --git a/pkg/postrender/exec_test.go b/pkg/postrender/exec_test.go new file mode 100644 index 000000000..684a0642b --- /dev/null +++ b/pkg/postrender/exec_test.go @@ -0,0 +1,152 @@ +/* +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 postrender + +import ( + "bytes" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "helm.sh/helm/v3/internal/test/ensure" +) + +const testingScript = `#!/bin/sh +sed s/FOOTEST/BARTEST/g <&0 +` + +func TestGetFullPath(t *testing.T) { + is := assert.New(t) + t.Run("full path resolves correctly", func(t *testing.T) { + testpath, cleanup := setupTestingScript(t) + defer cleanup() + + fullPath, err := getFullPath(testpath) + is.NoError(err) + is.Equal(testpath, fullPath) + }) + + t.Run("relative path resolves correctly", func(t *testing.T) { + testpath, cleanup := setupTestingScript(t) + defer cleanup() + + currentDir, err := os.Getwd() + require.NoError(t, err) + relative, err := filepath.Rel(currentDir, testpath) + require.NoError(t, err) + fullPath, err := getFullPath(relative) + is.NoError(err) + is.Equal(testpath, fullPath) + }) + + t.Run("binary in PATH resolves correctly", func(t *testing.T) { + testpath, cleanup := setupTestingScript(t) + defer cleanup() + + realPath := os.Getenv("PATH") + os.Setenv("PATH", filepath.Dir(testpath)) + defer func() { + os.Setenv("PATH", realPath) + }() + + fullPath, err := getFullPath(filepath.Base(testpath)) + is.NoError(err) + is.Equal(testpath, fullPath) + }) + + t.Run("binary in plugin path resolves correctly", func(t *testing.T) { + testpath, cleanup := setupTestingScript(t) + defer cleanup() + + realPath := os.Getenv("HELM_PLUGINS") + os.Setenv("HELM_PLUGINS", filepath.Dir(testpath)) + defer func() { + os.Setenv("HELM_PLUGINS", realPath) + }() + + fullPath, err := getFullPath(filepath.Base(testpath)) + is.NoError(err) + is.Equal(testpath, fullPath) + }) + + t.Run("binary in multiple plugin paths resolves correctly", func(t *testing.T) { + testpath, cleanup := setupTestingScript(t) + defer cleanup() + + realPath := os.Getenv("HELM_PLUGINS") + os.Setenv("HELM_PLUGINS", filepath.Dir(testpath)+string(os.PathListSeparator)+"/another/dir") + defer func() { + os.Setenv("HELM_PLUGINS", realPath) + }() + + fullPath, err := getFullPath(filepath.Base(testpath)) + is.NoError(err) + is.Equal(testpath, fullPath) + }) +} + +func TestExecRun(t *testing.T) { + if runtime.GOOS == "windows" { + // the actual Run test uses a basic sed example, so skip this test on windows + t.Skip("skipping on windows") + } + is := assert.New(t) + testpath, cleanup := setupTestingScript(t) + defer cleanup() + + renderer, err := NewExec(testpath) + require.NoError(t, err) + + output, err := renderer.Run(bytes.NewBufferString("FOOTEST")) + is.NoError(err) + is.Contains(output.String(), "BARTEST") +} + +func setupTestingScript(t *testing.T) (filepath string, cleanup func()) { + t.Helper() + + tempdir := ensure.TempDir(t) + + f, err := ioutil.TempFile(tempdir, "post-render-test.sh") + if err != nil { + t.Fatalf("unable to create tempfile for testing: %s", err) + } + + _, err = f.WriteString(testingScript) + if err != nil { + t.Fatalf("unable to write tempfile for testing: %s", err) + } + + err = f.Chmod(0755) + if err != nil { + t.Fatalf("unable to make tempfile executable for testing: %s", err) + } + + err = f.Close() + if err != nil { + t.Fatalf("unable to close tempfile after writing: %s", err) + } + + return f.Name(), func() { + os.RemoveAll(tempdir) + } +}