Repository: helm/helm
Pull request #6750 (2019-10-22 20:53:46)

Support cross device rename for helm v3
This PR includes changes on #6738 for Helm v3

/cc @thomastaylor312 


Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 601d98661..b391a3e18 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -22,6 +22,6 @@ linters-settings:
   gofmt:
     simplify: true
   goimports:
-    local-prefixes: helm.sh/helm/v3
+    local-prefixes: helm.sh/helm
   dupl:
     threshold: 400
diff --git a/internal/third_party/dep/fs/fs.go b/internal/third_party/dep/fs/fs.go
new file mode 100644
index 000000000..832592197
--- /dev/null
+++ b/internal/third_party/dep/fs/fs.go
@@ -0,0 +1,373 @@
+/*
+Copyright (c) for portions of fs.go are held by The Go Authors, 2016 and are provided under
+the BSD license.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+package fs
+
+import (
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"runtime"
+	"syscall"
+
+	"github.com/pkg/errors"
+)
+
+// fs contains a copy of a few functions from dep tool code to avoid a dependency on golang/dep.
+// This code is copied from https://github.com/golang/dep/blob/37d6c560cdf407be7b6cd035b23dba89df9275cf/internal/fs/fs.go
+// No changes to the code were made other than removing some unused functions
+
+// RenameWithFallback attempts to rename a file or directory, but falls back to
+// copying in the event of a cross-device link error. If the fallback copy
+// succeeds, src is still removed, emulating normal rename behavior.
+func RenameWithFallback(src, dst string) error {
+	_, err := os.Stat(src)
+	if err != nil {
+		return errors.Wrapf(err, "cannot stat %s", src)
+	}
+
+	err = os.Rename(src, dst)
+	if err == nil {
+		return nil
+	}
+
+	return renameFallback(err, src, dst)
+}
+
+// renameByCopy attempts to rename a file or directory by copying it to the
+// destination and then removing the src thus emulating the rename behavior.
+func renameByCopy(src, dst string) error {
+	var cerr error
+	if dir, _ := IsDir(src); dir {
+		cerr = CopyDir(src, dst)
+		if cerr != nil {
+			cerr = errors.Wrap(cerr, "copying directory failed")
+		}
+	} else {
+		cerr = copyFile(src, dst)
+		if cerr != nil {
+			cerr = errors.Wrap(cerr, "copying file failed")
+		}
+	}
+
+	if cerr != nil {
+		return errors.Wrapf(cerr, "rename fallback failed: cannot rename %s to %s", src, dst)
+	}
+
+	return errors.Wrapf(os.RemoveAll(src), "cannot delete %s", src)
+}
+
+var (
+	errSrcNotDir = errors.New("source is not a directory")
+	errDstExist  = errors.New("destination already exists")
+)
+
+// CopyDir recursively copies a directory tree, attempting to preserve permissions.
+// Source directory must exist, destination directory must *not* exist.
+func CopyDir(src, dst string) error {
+	src = filepath.Clean(src)
+	dst = filepath.Clean(dst)
+
+	// We use os.Lstat() here to ensure we don't fall in a loop where a symlink
+	// actually links to a one of its parent directories.
+	fi, err := os.Lstat(src)
+	if err != nil {
+		return err
+	}
+	if !fi.IsDir() {
+		return errSrcNotDir
+	}
+
+	_, err = os.Stat(dst)
+	if err != nil && !os.IsNotExist(err) {
+		return err
+	}
+	if err == nil {
+		return errDstExist
+	}
+
+	if err = os.MkdirAll(dst, fi.Mode()); err != nil {
+		return errors.Wrapf(err, "cannot mkdir %s", dst)
+	}
+
+	entries, err := ioutil.ReadDir(src)
+	if err != nil {
+		return errors.Wrapf(err, "cannot read directory %s", dst)
+	}
+
+	for _, entry := range entries {
+		srcPath := filepath.Join(src, entry.Name())
+		dstPath := filepath.Join(dst, entry.Name())
+
+		if entry.IsDir() {
+			if err = CopyDir(srcPath, dstPath); err != nil {
+				return errors.Wrap(err, "copying directory failed")
+			}
+		} else {
+			// This will include symlinks, which is what we want when
+			// copying things.
+			if err = copyFile(srcPath, dstPath); err != nil {
+				return errors.Wrap(err, "copying file failed")
+			}
+		}
+	}
+
+	return nil
+}
+
+// copyFile copies the contents of the file named src to the file named
+// by dst. The file will be created if it does not already exist. If the
+// destination file exists, all its contents will be replaced by the contents
+// of the source file. The file mode will be copied from the source.
+func copyFile(src, dst string) (err error) {
+	if sym, err := IsSymlink(src); err != nil {
+		return errors.Wrap(err, "symlink check failed")
+	} else if sym {
+		if err := cloneSymlink(src, dst); err != nil {
+			if runtime.GOOS == "windows" {
+				// If cloning the symlink fails on Windows because the user
+				// does not have the required privileges, ignore the error and
+				// fall back to copying the file contents.
+				//
+				// ERROR_PRIVILEGE_NOT_HELD is 1314 (0x522):
+				// https://msdn.microsoft.com/en-us/library/windows/desktop/ms681385(v=vs.85).aspx
+				if lerr, ok := err.(*os.LinkError); ok && lerr.Err != syscall.Errno(1314) {
+					return err
+				}
+			} else {
+				return err
+			}
+		} else {
+			return nil
+		}
+	}
+
+	in, err := os.Open(src)
+	if err != nil {
+		return
+	}
+	defer in.Close()
+
+	out, err := os.Create(dst)
+	if err != nil {
+		return
+	}
+
+	if _, err = io.Copy(out, in); err != nil {
+		out.Close()
+		return
+	}
+
+	// Check for write errors on Close
+	if err = out.Close(); err != nil {
+		return
+	}
+
+	si, err := os.Stat(src)
+	if err != nil {
+		return
+	}
+
+	// Temporary fix for Go < 1.9
+	//
+	// See: https://github.com/golang/dep/issues/774
+	// and https://github.com/golang/go/issues/20829
+	if runtime.GOOS == "windows" {
+		dst = fixLongPath(dst)
+	}
+	err = os.Chmod(dst, si.Mode())
+
+	return
+}
+
+// cloneSymlink will create a new symlink that points to the resolved path of sl.
+// If sl is a relative symlink, dst will also be a relative symlink.
+func cloneSymlink(sl, dst string) error {
+	resolved, err := os.Readlink(sl)
+	if err != nil {
+		return err
+	}
+
+	return os.Symlink(resolved, dst)
+}
+
+// IsDir determines is the path given is a directory or not.
+func IsDir(name string) (bool, error) {
+	fi, err := os.Stat(name)
+	if err != nil {
+		return false, err
+	}
+	if !fi.IsDir() {
+		return false, errors.Errorf("%q is not a directory", name)
+	}
+	return true, nil
+}
+
+// IsSymlink determines if the given path is a symbolic link.
+func IsSymlink(path string) (bool, error) {
+	l, err := os.Lstat(path)
+	if err != nil {
+		return false, err
+	}
+
+	return l.Mode()&os.ModeSymlink == os.ModeSymlink, nil
+}
+
+// fixLongPath returns the extended-length (\\?\-prefixed) form of
+// path when needed, in order to avoid the default 260 character file
+// path limit imposed by Windows. If path is not easily converted to
+// the extended-length form (for example, if path is a relative path
+// or contains .. elements), or is short enough, fixLongPath returns
+// path unmodified.
+//
+// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath
+func fixLongPath(path string) string {
+	// Do nothing (and don't allocate) if the path is "short".
+	// Empirically (at least on the Windows Server 2013 builder),
+	// the kernel is arbitrarily okay with < 248 bytes. That
+	// matches what the docs above say:
+	// "When using an API to create a directory, the specified
+	// path cannot be so long that you cannot append an 8.3 file
+	// name (that is, the directory name cannot exceed MAX_PATH
+	// minus 12)." Since MAX_PATH is 260, 260 - 12 = 248.
+	//
+	// The MSDN docs appear to say that a normal path that is 248 bytes long
+	// will work; empirically the path must be less then 248 bytes long.
+	if len(path) < 248 {
+		// Don't fix. (This is how Go 1.7 and earlier worked,
+		// not automatically generating the \\?\ form)
+		return path
+	}
+
+	// The extended form begins with \\?\, as in
+	// \\?\c:\windows\foo.txt or \\?\UNC\server\share\foo.txt.
+	// The extended form disables evaluation of . and .. path
+	// elements and disables the interpretation of / as equivalent
+	// to \. The conversion here rewrites / to \ and elides
+	// . elements as well as trailing or duplicate separators. For
+	// simplicity it avoids the conversion entirely for relative
+	// paths or paths containing .. elements. For now,
+	// \\server\share paths are not converted to
+	// \\?\UNC\server\share paths because the rules for doing so
+	// are less well-specified.
+	if len(path) >= 2 && path[:2] == `\\` {
+		// Don't canonicalize UNC paths.
+		return path
+	}
+	if !isAbs(path) {
+		// Relative path
+		return path
+	}
+
+	const prefix = `\\?`
+
+	pathbuf := make([]byte, len(prefix)+len(path)+len(`\`))
+	copy(pathbuf, prefix)
+	n := len(path)
+	r, w := 0, len(prefix)
+	for r < n {
+		switch {
+		case os.IsPathSeparator(path[r]):
+			// empty block
+			r++
+		case path[r] == '.' && (r+1 == n || os.IsPathSeparator(path[r+1])):
+			// /./
+			r++
+		case r+1 < n && path[r] == '.' && path[r+1] == '.' && (r+2 == n || os.IsPathSeparator(path[r+2])):
+			// /../ is currently unhandled
+			return path
+		default:
+			pathbuf[w] = '\\'
+			w++
+			for ; r < n && !os.IsPathSeparator(path[r]); r++ {
+				pathbuf[w] = path[r]
+				w++
+			}
+		}
+	}
+	// A drive's root directory needs a trailing \
+	if w == len(`\\?\c:`) {
+		pathbuf[w] = '\\'
+		w++
+	}
+	return string(pathbuf[:w])
+}
+
+func isAbs(path string) (b bool) {
+	v := volumeName(path)
+	if v == "" {
+		return false
+	}
+	path = path[len(v):]
+	if path == "" {
+		return false
+	}
+	return os.IsPathSeparator(path[0])
+}
+
+func volumeName(path string) (v string) {
+	if len(path) < 2 {
+		return ""
+	}
+	// with drive letter
+	c := path[0]
+	if path[1] == ':' &&
+		('0' <= c && c <= '9' || 'a' <= c && c <= 'z' ||
+			'A' <= c && c <= 'Z') {
+		return path[:2]
+	}
+	// is it UNC
+	if l := len(path); l >= 5 && os.IsPathSeparator(path[0]) && os.IsPathSeparator(path[1]) &&
+		!os.IsPathSeparator(path[2]) && path[2] != '.' {
+		// first, leading `\\` and next shouldn't be `\`. its server name.
+		for n := 3; n < l-1; n++ {
+			// second, next '\' shouldn't be repeated.
+			if os.IsPathSeparator(path[n]) {
+				n++
+				// third, following something characters. its share name.
+				if !os.IsPathSeparator(path[n]) {
+					if path[n] == '.' {
+						break
+					}
+					for ; n < l; n++ {
+						if os.IsPathSeparator(path[n]) {
+							break
+						}
+					}
+					return path[:n]
+				}
+				break
+			}
+		}
+	}
+	return ""
+}
diff --git a/internal/third_party/dep/fs/rename.go b/internal/third_party/dep/fs/rename.go
new file mode 100644
index 000000000..0bb600949
--- /dev/null
+++ b/internal/third_party/dep/fs/rename.go
@@ -0,0 +1,58 @@
+// +build !windows
+
+/*
+Copyright (c) for portions of rename.go are held by The Go Authors, 2016 and are provided under
+the BSD license.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+package fs
+
+import (
+	"os"
+	"syscall"
+
+	"github.com/pkg/errors"
+)
+
+// renameFallback attempts to determine the appropriate fallback to failed rename
+// operation depending on the resulting error.
+func renameFallback(err error, src, dst string) error {
+	// Rename may fail if src and dst are on different devices; fall back to
+	// copy if we detect that case. syscall.EXDEV is the common name for the
+	// cross device link error which has varying output text across different
+	// operating systems.
+	terr, ok := err.(*os.LinkError)
+	if !ok {
+		return err
+	} else if terr.Err != syscall.EXDEV {
+		return errors.Wrapf(terr, "link error: cannot rename %s to %s", src, dst)
+	}
+
+	return renameByCopy(src, dst)
+}
diff --git a/internal/third_party/dep/fs/rename_windows.go b/internal/third_party/dep/fs/rename_windows.go
new file mode 100644
index 000000000..14f017d09
--- /dev/null
+++ b/internal/third_party/dep/fs/rename_windows.go
@@ -0,0 +1,69 @@
+// +build windows
+
+/*
+Copyright (c) for portions of rename_windows.go are held by The Go Authors, 2016 and are provided under
+the BSD license.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+package fs
+
+import (
+	"os"
+	"syscall"
+
+	"github.com/pkg/errors"
+)
+
+// renameFallback attempts to determine the appropriate fallback to failed rename
+// operation depending on the resulting error.
+func renameFallback(err error, src, dst string) error {
+	// Rename may fail if src and dst are on different devices; fall back to
+	// copy if we detect that case. syscall.EXDEV is the common name for the
+	// cross device link error which has varying output text across different
+	// operating systems.
+	terr, ok := err.(*os.LinkError)
+	if !ok {
+		return err
+	}
+
+	if terr.Err != syscall.EXDEV {
+		// In windows it can drop down to an operating system call that
+		// returns an operating system error with a different number and
+		// message. Checking for that as a fall back.
+		noerr, ok := terr.Err.(syscall.Errno)
+
+		// 0x11 (ERROR_NOT_SAME_DEVICE) is the windows error.
+		// See https://msdn.microsoft.com/en-us/library/cc231199.aspx
+		if ok && noerr != 0x11 {
+			return errors.Wrapf(terr, "link error: cannot rename %s to %s", src, dst)
+		}
+	}
+
+	return renameByCopy(src, dst)
+}
diff --git a/pkg/chartutil/values.go b/pkg/chartutil/values.go
index e1cdf4642..d9b94212c 100644
--- a/pkg/chartutil/values.go
+++ b/pkg/chartutil/values.go
@@ -17,6 +17,7 @@ limitations under the License.
 package chartutil
 
 import (
+	"encoding/json"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -105,7 +106,10 @@ func tableLookup(v Values, simple string) (Values, error) {
 
 // ReadValues will parse YAML byte data into a Values.
 func ReadValues(data []byte) (vals Values, err error) {
-	err = yaml.Unmarshal(data, &vals)
+	err = yaml.Unmarshal(data, &vals, func(d *json.Decoder) *json.Decoder {
+		d.UseNumber()
+		return d
+	})
 	if len(vals) == 0 {
 		vals = Values{}
 	}
diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go
index cc3af74db..388ba67f2 100644
--- a/pkg/downloader/manager.go
+++ b/pkg/downloader/manager.go
@@ -31,6 +31,7 @@ import (
 	"sigs.k8s.io/yaml"
 
 	"helm.sh/helm/v3/internal/resolver"
+	"helm.sh/helm/v3/internal/third_party/dep/fs"
 	"helm.sh/helm/v3/internal/urlutil"
 	"helm.sh/helm/v3/pkg/chart"
 	"helm.sh/helm/v3/pkg/chart/loader"
@@ -192,7 +193,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
 		return errors.Errorf("%q is not a directory", destPath)
 	}
 
-	if err := os.Rename(destPath, tmpPath); err != nil {
+	if err := fs.RenameWithFallback(destPath, tmpPath); err != nil {
 		return errors.Wrap(err, "unable to move current charts to tmp dir")
 	}
 
@@ -296,7 +297,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency) error {
 		if err := os.RemoveAll(destPath); err != nil {
 			return errors.Wrapf(err, "failed to remove %v", destPath)
 		}
-		if err := os.Rename(tmpPath, destPath); err != nil {
+		if err := fs.RenameWithFallback(tmpPath, destPath); err != nil {
 			return errors.Wrap(err, "unable to move current charts to tmp dir")
 		}
 		return saveError
@@ -672,7 +673,7 @@ func move(tmpPath, destPath string) error {
 		filename := file.Name()
 		tmpfile := filepath.Join(tmpPath, filename)
 		destfile := filepath.Join(destPath, filename)
-		if err := os.Rename(tmpfile, destfile); err != nil {
+		if err := fs.RenameWithFallback(tmpfile, destfile); err != nil {
 			return errors.Wrap(err, "unable to move local charts to charts dir")
 		}
 	}
diff --git a/scripts/coverage.sh b/scripts/coverage.sh
index bdbfaa991..62d495769 100755
--- a/scripts/coverage.sh
+++ b/scripts/coverage.sh
@@ -20,12 +20,11 @@ covermode=${COVERMODE:-atomic}
 coverdir=$(mktemp -d /tmp/coverage.XXXXXXXXXX)
 profile="${coverdir}/cover.out"
 
-pushd /
 hash goveralls 2>/dev/null || go get github.com/mattn/goveralls
-popd
+hash godir 2>/dev/null || go get github.com/Masterminds/godir
 
 generate_cover_data() {
-  for d in $(go list ./...) ; do
+  for d in $(godir) ; do
     (
       local output="${coverdir}/${d//\//-}.cover"
       go test -coverprofile="${output}" -covermode="$covermode" "$d"


Test patch:
diff --git a/internal/third_party/dep/fs/fs_test.go b/internal/third_party/dep/fs/fs_test.go
new file mode 100644
index 000000000..bf4b803f8
--- /dev/null
+++ b/internal/third_party/dep/fs/fs_test.go
@@ -0,0 +1,719 @@
+/*
+Copyright (c) for portions of fs_test.go are held by The Go Authors, 2016 and are provided under
+the BSD license.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+   * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+   * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+   * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+package fs
+
+import (
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"os/user"
+	"path/filepath"
+	"runtime"
+	"sync"
+	"testing"
+)
+
+var (
+	mu sync.Mutex
+)
+
+func TestRenameWithFallback(t *testing.T) {
+	dir, err := ioutil.TempDir("", "helm-tmp")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(dir)
+
+	if err = RenameWithFallback(filepath.Join(dir, "does_not_exists"), filepath.Join(dir, "dst")); err == nil {
+		t.Fatal("expected an error for non existing file, but got nil")
+	}
+
+	srcpath := filepath.Join(dir, "src")
+
+	if srcf, err := os.Create(srcpath); err != nil {
+		t.Fatal(err)
+	} else {
+		srcf.Close()
+	}
+
+	if err = RenameWithFallback(srcpath, filepath.Join(dir, "dst")); err != nil {
+		t.Fatal(err)
+	}
+
+	srcpath = filepath.Join(dir, "a")
+	if err = os.MkdirAll(srcpath, 0777); err != nil {
+		t.Fatal(err)
+	}
+
+	dstpath := filepath.Join(dir, "b")
+	if err = os.MkdirAll(dstpath, 0777); err != nil {
+		t.Fatal(err)
+	}
+
+	if err = RenameWithFallback(srcpath, dstpath); err == nil {
+		t.Fatal("expected an error if dst is an existing directory, but got nil")
+	}
+}
+
+func TestCopyDir(t *testing.T) {
+	dir, err := ioutil.TempDir("", "helm-tmp")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(dir)
+
+	srcdir := filepath.Join(dir, "src")
+	if err := os.MkdirAll(srcdir, 0755); err != nil {
+		t.Fatal(err)
+	}
+
+	files := []struct {
+		path     string
+		contents string
+		fi       os.FileInfo
+	}{
+		{path: "myfile", contents: "hello world"},
+		{path: filepath.Join("subdir", "file"), contents: "subdir file"},
+	}
+
+	// Create structure indicated in 'files'
+	for i, file := range files {
+		fn := filepath.Join(srcdir, file.path)
+		dn := filepath.Dir(fn)
+		if err = os.MkdirAll(dn, 0755); err != nil {
+			t.Fatal(err)
+		}
+
+		fh, err := os.Create(fn)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if _, err = fh.Write([]byte(file.contents)); err != nil {
+			t.Fatal(err)
+		}
+		fh.Close()
+
+		files[i].fi, err = os.Stat(fn)
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+
+	destdir := filepath.Join(dir, "dest")
+	if err := CopyDir(srcdir, destdir); err != nil {
+		t.Fatal(err)
+	}
+
+	// Compare copy against structure indicated in 'files'
+	for _, file := range files {
+		fn := filepath.Join(srcdir, file.path)
+		dn := filepath.Dir(fn)
+		dirOK, err := IsDir(dn)
+		if err != nil {
+			t.Fatal(err)
+		}
+		if !dirOK {
+			t.Fatalf("expected %s to be a directory", dn)
+		}
+
+		got, err := ioutil.ReadFile(fn)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if file.contents != string(got) {
+			t.Fatalf("expected: %s, got: %s", file.contents, string(got))
+		}
+
+		gotinfo, err := os.Stat(fn)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if file.fi.Mode() != gotinfo.Mode() {
+			t.Fatalf("expected %s: %#v\n to be the same mode as %s: %#v",
+				file.path, file.fi.Mode(), fn, gotinfo.Mode())
+		}
+	}
+}
+
+func TestCopyDirFail_SrcInaccessible(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		// XXX: setting permissions works differently in
+		// Microsoft Windows. Skipping this this until a
+		// compatible implementation is provided.
+		t.Skip("skipping on windows")
+	}
+
+	var currentUser, err = user.Current()
+
+	if err != nil {
+		t.Fatalf("Failed to get name of current user: %s", err)
+	}
+
+	if currentUser.Name == "root" {
+		// Skipping if root, because all files are accessible
+		t.Skip("Skipping for root user")
+	}
+
+	var srcdir, dstdir string
+
+	cleanup := setupInaccessibleDir(t, func(dir string) error {
+		srcdir = filepath.Join(dir, "src")
+		return os.MkdirAll(srcdir, 0755)
+	})
+	defer cleanup()
+
+	dir, err := ioutil.TempDir("", "helm-tmp")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(dir)
+
+	dstdir = filepath.Join(dir, "dst")
+	if err = CopyDir(srcdir, dstdir); err == nil {
+		t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir)
+	}
+}
+
+func TestCopyDirFail_DstInaccessible(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		// XXX: setting permissions works differently in
+		// Microsoft Windows. Skipping this this until a
+		// compatible implementation is provided.
+		t.Skip("skipping on windows")
+	}
+
+	var currentUser, err = user.Current()
+
+	if err != nil {
+		t.Fatalf("Failed to get name of current user: %s", err)
+	}
+
+	if currentUser.Name == "root" {
+		// Skipping if root, because all files are accessible
+		t.Skip("Skipping for root user")
+	}
+
+	var srcdir, dstdir string
+
+	dir, err := ioutil.TempDir("", "helm-tmp")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(dir)
+
+	srcdir = filepath.Join(dir, "src")
+	if err = os.MkdirAll(srcdir, 0755); err != nil {
+		t.Fatal(err)
+	}
+
+	cleanup := setupInaccessibleDir(t, func(dir string) error {
+		dstdir = filepath.Join(dir, "dst")
+		return nil
+	})
+	defer cleanup()
+
+	if err := CopyDir(srcdir, dstdir); err == nil {
+		t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir)
+	}
+}
+
+func TestCopyDirFail_SrcIsNotDir(t *testing.T) {
+	var srcdir, dstdir string
+
+	dir, err := ioutil.TempDir("", "helm-tmp")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(dir)
+
+	srcdir = filepath.Join(dir, "src")
+	if _, err = os.Create(srcdir); err != nil {
+		t.Fatal(err)
+	}
+
+	dstdir = filepath.Join(dir, "dst")
+
+	if err = CopyDir(srcdir, dstdir); err == nil {
+		t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir)
+	}
+
+	if err != errSrcNotDir {
+		t.Fatalf("expected %v error for CopyDir(%s, %s), got %s", errSrcNotDir, srcdir, dstdir, err)
+	}
+
+}
+
+func TestCopyDirFail_DstExists(t *testing.T) {
+	var srcdir, dstdir string
+
+	dir, err := ioutil.TempDir("", "helm-tmp")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(dir)
+
+	srcdir = filepath.Join(dir, "src")
+	if err = os.MkdirAll(srcdir, 0755); err != nil {
+		t.Fatal(err)
+	}
+
+	dstdir = filepath.Join(dir, "dst")
+	if err = os.MkdirAll(dstdir, 0755); err != nil {
+		t.Fatal(err)
+	}
+
+	if err = CopyDir(srcdir, dstdir); err == nil {
+		t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir)
+	}
+
+	if err != errDstExist {
+		t.Fatalf("expected %v error for CopyDir(%s, %s), got %s", errDstExist, srcdir, dstdir, err)
+	}
+}
+
+func TestCopyDirFailOpen(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		// XXX: setting permissions works differently in
+		// Microsoft Windows. os.Chmod(..., 0222) below is not
+		// enough for the file to be readonly, and os.Chmod(...,
+		// 0000) returns an invalid argument error. Skipping
+		// this this until a compatible implementation is
+		// provided.
+		t.Skip("skipping on windows")
+	}
+
+	var currentUser, err = user.Current()
+
+	if err != nil {
+		t.Fatalf("Failed to get name of current user: %s", err)
+	}
+
+	if currentUser.Name == "root" {
+		// Skipping if root, because all files are accessible
+		t.Skip("Skipping for root user")
+	}
+
+	var srcdir, dstdir string
+
+	dir, err := ioutil.TempDir("", "helm-tmp")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(dir)
+
+	srcdir = filepath.Join(dir, "src")
+	if err = os.MkdirAll(srcdir, 0755); err != nil {
+		t.Fatal(err)
+	}
+
+	srcfn := filepath.Join(srcdir, "file")
+	srcf, err := os.Create(srcfn)
+	if err != nil {
+		t.Fatal(err)
+	}
+	srcf.Close()
+
+	// setup source file so that it cannot be read
+	if err = os.Chmod(srcfn, 0222); err != nil {
+		t.Fatal(err)
+	}
+
+	dstdir = filepath.Join(dir, "dst")
+
+	if err = CopyDir(srcdir, dstdir); err == nil {
+		t.Fatalf("expected error for CopyDir(%s, %s), got none", srcdir, dstdir)
+	}
+}
+
+func TestCopyFile(t *testing.T) {
+	dir, err := ioutil.TempDir("", "helm-tmp")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(dir)
+
+	srcf, err := os.Create(filepath.Join(dir, "srcfile"))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := "hello world"
+	if _, err := srcf.Write([]byte(want)); err != nil {
+		t.Fatal(err)
+	}
+	srcf.Close()
+
+	destf := filepath.Join(dir, "destf")
+	if err := copyFile(srcf.Name(), destf); err != nil {
+		t.Fatal(err)
+	}
+
+	got, err := ioutil.ReadFile(destf)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if want != string(got) {
+		t.Fatalf("expected: %s, got: %s", want, string(got))
+	}
+
+	wantinfo, err := os.Stat(srcf.Name())
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	gotinfo, err := os.Stat(destf)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	if wantinfo.Mode() != gotinfo.Mode() {
+		t.Fatalf("expected %s: %#v\n to be the same mode as %s: %#v", srcf.Name(), wantinfo.Mode(), destf, gotinfo.Mode())
+	}
+}
+
+func cleanUpDir(dir string) {
+	// NOTE(mattn): It seems that sometimes git.exe is not dead
+	// when cleanUpDir() is called. But we do not know any way to wait for it.
+	if runtime.GOOS == "windows" {
+		mu.Lock()
+		exec.Command(`taskkill`, `/F`, `/IM`, `git.exe`).Run()
+		mu.Unlock()
+	}
+	if dir != "" {
+		os.RemoveAll(dir)
+	}
+}
+
+func TestCopyFileSymlink(t *testing.T) {
+	var tempdir, err = ioutil.TempDir("", "gotest")
+
+	if err != nil {
+		t.Fatalf("failed to create directory: %s", err)
+	}
+
+	defer cleanUpDir(tempdir)
+
+	testcases := map[string]string{
+		filepath.Join("./testdata/symlinks/file-symlink"):         filepath.Join(tempdir, "dst-file"),
+		filepath.Join("./testdata/symlinks/windows-file-symlink"): filepath.Join(tempdir, "windows-dst-file"),
+		filepath.Join("./testdata/symlinks/invalid-symlink"):      filepath.Join(tempdir, "invalid-symlink"),
+	}
+
+	for symlink, dst := range testcases {
+		t.Run(symlink, func(t *testing.T) {
+			var err error
+			if err = copyFile(symlink, dst); err != nil {
+				t.Fatalf("failed to copy symlink: %s", err)
+			}
+
+			var want, got string
+
+			if runtime.GOOS == "windows" {
+				// Creating symlinks on Windows require an additional permission
+				// regular users aren't granted usually. So we copy the file
+				// content as a fall back instead of creating a real symlink.
+				srcb, err := ioutil.ReadFile(symlink)
+				if err != nil {
+					t.Fatalf("%+v", err)
+				}
+				dstb, err := ioutil.ReadFile(dst)
+				if err != nil {
+					t.Fatalf("%+v", err)
+				}
+
+				want = string(srcb)
+				got = string(dstb)
+			} else {
+				want, err = os.Readlink(symlink)
+				if err != nil {
+					t.Fatalf("%+v", err)
+				}
+
+				got, err = os.Readlink(dst)
+				if err != nil {
+					t.Fatalf("could not resolve symlink: %s", err)
+				}
+			}
+
+			if want != got {
+				t.Fatalf("resolved path is incorrect. expected %s, got %s", want, got)
+			}
+		})
+	}
+}
+
+func TestCopyFileFail(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		// XXX: setting permissions works differently in
+		// Microsoft Windows. Skipping this this until a
+		// compatible implementation is provided.
+		t.Skip("skipping on windows")
+	}
+
+	var currentUser, err = user.Current()
+
+	if err != nil {
+		t.Fatalf("Failed to get name of current user: %s", err)
+	}
+
+	if currentUser.Name == "root" {
+		// Skipping if root, because all files are accessible
+		t.Skip("Skipping for root user")
+	}
+
+	dir, err := ioutil.TempDir("", "helm-tmp")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(dir)
+
+	srcf, err := os.Create(filepath.Join(dir, "srcfile"))
+	if err != nil {
+		t.Fatal(err)
+	}
+	srcf.Close()
+
+	var dstdir string
+
+	cleanup := setupInaccessibleDir(t, func(dir string) error {
+		dstdir = filepath.Join(dir, "dir")
+		return os.Mkdir(dstdir, 0777)
+	})
+	defer cleanup()
+
+	fn := filepath.Join(dstdir, "file")
+	if err := copyFile(srcf.Name(), fn); err == nil {
+		t.Fatalf("expected error for %s, got none", fn)
+	}
+}
+
+// setupInaccessibleDir creates a temporary location with a single
+// directory in it, in such a way that that directory is not accessible
+// after this function returns.
+//
+// op is called with the directory as argument, so that it can create
+// files or other test artifacts.
+//
+// If setupInaccessibleDir fails in its preparation, or op fails, t.Fatal
+// will be invoked.
+//
+// This function returns a cleanup function that removes all the temporary
+// files this function creates. It is the caller's responsibility to call
+// this function before the test is done running, whether there's an error or not.
+func setupInaccessibleDir(t *testing.T, op func(dir string) error) func() {
+	dir, err := ioutil.TempDir("", "helm-tmp")
+	if err != nil {
+		t.Fatal(err)
+		return nil // keep compiler happy
+	}
+
+	subdir := filepath.Join(dir, "dir")
+
+	cleanup := func() {
+		if err := os.Chmod(subdir, 0777); err != nil {
+			t.Error(err)
+		}
+		if err := os.RemoveAll(dir); err != nil {
+			t.Error(err)
+		}
+	}
+
+	if err := os.Mkdir(subdir, 0777); err != nil {
+		cleanup()
+		t.Fatal(err)
+		return nil
+	}
+
+	if err := op(subdir); err != nil {
+		cleanup()
+		t.Fatal(err)
+		return nil
+	}
+
+	if err := os.Chmod(subdir, 0666); err != nil {
+		cleanup()
+		t.Fatal(err)
+		return nil
+	}
+
+	return cleanup
+}
+
+func TestIsDir(t *testing.T) {
+
+	var currentUser, err = user.Current()
+
+	if err != nil {
+		t.Fatalf("Failed to get name of current user: %s", err)
+	}
+
+	if currentUser.Name == "root" {
+		// Skipping if root, because all files are accessible
+		t.Skip("Skipping for root user")
+	}
+
+	wd, err := os.Getwd()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	var dn string
+
+	cleanup := setupInaccessibleDir(t, func(dir string) error {
+		dn = filepath.Join(dir, "dir")
+		return os.Mkdir(dn, 0777)
+	})
+	defer cleanup()
+
+	tests := map[string]struct {
+		exists bool
+		err    bool
+	}{
+		wd:                            {true, false},
+		filepath.Join(wd, "testdata"): {true, false},
+		filepath.Join(wd, "main.go"):  {false, true},
+		filepath.Join(wd, "this_file_does_not_exist.thing"): {false, true},
+		dn: {false, true},
+	}
+
+	if runtime.GOOS == "windows" {
+		// This test doesn't work on Microsoft Windows because
+		// of the differences in how file permissions are
+		// implemented. For this to work, the directory where
+		// the directory exists should be inaccessible.
+		delete(tests, dn)
+	}
+
+	for f, want := range tests {
+		got, err := IsDir(f)
+		if err != nil && !want.err {
+			t.Fatalf("expected no error, got %v", err)
+		}
+
+		if got != want.exists {
+			t.Fatalf("expected %t for %s, got %t", want.exists, f, got)
+		}
+	}
+}
+
+func TestIsSymlink(t *testing.T) {
+
+	var currentUser, err = user.Current()
+
+	if err != nil {
+		t.Fatalf("Failed to get name of current user: %s", err)
+	}
+
+	if currentUser.Name == "root" {
+		// Skipping if root, because all files are accessible
+		t.Skip("Skipping for root user")
+	}
+
+	dir, err := ioutil.TempDir("", "helm-tmp")
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer os.RemoveAll(dir)
+
+	dirPath := filepath.Join(dir, "directory")
+	if err = os.MkdirAll(dirPath, 0777); err != nil {
+		t.Fatal(err)
+	}
+
+	filePath := filepath.Join(dir, "file")
+	f, err := os.Create(filePath)
+	if err != nil {
+		t.Fatal(err)
+	}
+	f.Close()
+
+	dirSymlink := filepath.Join(dir, "dirSymlink")
+	fileSymlink := filepath.Join(dir, "fileSymlink")
+
+	if err = os.Symlink(dirPath, dirSymlink); err != nil {
+		t.Fatal(err)
+	}
+	if err = os.Symlink(filePath, fileSymlink); err != nil {
+		t.Fatal(err)
+	}
+
+	var (
+		inaccessibleFile    string
+		inaccessibleSymlink string
+	)
+
+	cleanup := setupInaccessibleDir(t, func(dir string) error {
+		inaccessibleFile = filepath.Join(dir, "file")
+		if fh, err := os.Create(inaccessibleFile); err != nil {
+			return err
+		} else if err = fh.Close(); err != nil {
+			return err
+		}
+
+		inaccessibleSymlink = filepath.Join(dir, "symlink")
+		return os.Symlink(inaccessibleFile, inaccessibleSymlink)
+	})
+	defer cleanup()
+
+	tests := map[string]struct{ expected, err bool }{
+		dirPath:             {false, false},
+		filePath:            {false, false},
+		dirSymlink:          {true, false},
+		fileSymlink:         {true, false},
+		inaccessibleFile:    {false, true},
+		inaccessibleSymlink: {false, true},
+	}
+
+	if runtime.GOOS == "windows" {
+		// XXX: setting permissions works differently in Windows. Skipping
+		// these cases until a compatible implementation is provided.
+		delete(tests, inaccessibleFile)
+		delete(tests, inaccessibleSymlink)
+	}
+
+	for path, want := range tests {
+		got, err := IsSymlink(path)
+		if err != nil {
+			if !want.err {
+				t.Errorf("expected no error, got %v", err)
+			}
+		}
+
+		if got != want.expected {
+			t.Errorf("expected %t for %s, got %t", want.expected, path, got)
+		}
+	}
+}
diff --git a/internal/third_party/dep/fs/testdata/symlinks/file-symlink b/internal/third_party/dep/fs/testdata/symlinks/file-symlink
new file mode 120000
index 000000000..4c52274de
--- /dev/null
+++ b/internal/third_party/dep/fs/testdata/symlinks/file-symlink
@@ -0,0 +1,1 @@
+../test.file
\ No newline at end of file
diff --git a/internal/third_party/dep/fs/testdata/symlinks/invalid-symlink b/internal/third_party/dep/fs/testdata/symlinks/invalid-symlink
new file mode 120000
index 000000000..0edf4f301
--- /dev/null
+++ b/internal/third_party/dep/fs/testdata/symlinks/invalid-symlink
@@ -0,0 +1,1 @@
+/non/existing/file
\ No newline at end of file
diff --git a/internal/third_party/dep/fs/testdata/symlinks/windows-file-symlink b/internal/third_party/dep/fs/testdata/symlinks/windows-file-symlink
new file mode 120000
index 000000000..af1d6c8f5
--- /dev/null
+++ b/internal/third_party/dep/fs/testdata/symlinks/windows-file-symlink
@@ -0,0 +1,1 @@
+C:/Users/ibrahim/go/src/github.com/golang/dep/internal/fs/testdata/test.file
\ No newline at end of file
diff --git a/internal/third_party/dep/fs/testdata/test.file b/internal/third_party/dep/fs/testdata/test.file
new file mode 100644
index 000000000..e69de29bb
diff --git a/pkg/chartutil/testdata/coleridge.yaml b/pkg/chartutil/testdata/coleridge.yaml
index b6579628b..15535988b 100644
--- a/pkg/chartutil/testdata/coleridge.yaml
+++ b/pkg/chartutil/testdata/coleridge.yaml
@@ -10,3 +10,4 @@ water:
   water:
     where: "everywhere"
     nor: "any drop to drink"
+    temperature: 1234567890
diff --git a/pkg/chartutil/values_test.go b/pkg/chartutil/values_test.go
index a3fe7aeac..2cb328d5d 100644
--- a/pkg/chartutil/values_test.go
+++ b/pkg/chartutil/values_test.go
@@ -45,6 +45,7 @@ water:
   water:
     where: "everywhere"
     nor: "any drop to drink"
+    temperature: 1234567890
 `
 
 	data, err := ReadValues([]byte(doc))
@@ -245,6 +246,12 @@ func matchValues(t *testing.T, data map[string]interface{}) {
 	} else if o != "everywhere" {
 		t.Errorf("Expected water water everywhere")
 	}
+
+	if o, err := ttpl("{{.water.water.temperature}}", data); err != nil {
+		t.Errorf(".water.water.temperature: %s", err)
+	} else if o != "1234567890" {
+		t.Errorf("Expected water water temperature: 1234567890, got: %s", o)
+	}
 }
 
 func ttpl(tpl string, v map[string]interface{}) (string, error) {