Repository: helm/helm
Pull request #8752 (2020-09-17 17:23:51)

improve the HTTP detection for tar archives
Signed-off-by: Matt Butcher <matt.butcher@microsoft.com>

<!--  Thanks for sending a pull request!  Here are some tips for you:
1. Make sure to read the Contributing Guide before submitting your PR: https://github.com/helm/helm/blob/master/CONTRIBUTING.md
2. If this PR closes another issue, add 'closes #<issue number>' somewhere in the PR summary. GitHub will automatically close that issue when this PR gets merged. Alternatively, adding 'refs #<issue number>' will not close the issue, but help provide the reviewer more context.-->

**What this PR does / why we need it**:

This replaces extension scanning with media type queries

**Special notes for your reviewer**:

**If applicable**:
- [ ] this PR contains documentation
- [x] this PR contains unit tests
- [x] this PR has been tested for backwards compatibility



Patch:
diff --git a/cmd/helm/root.go b/cmd/helm/root.go
index 91542bb7e..82c87bd7d 100644
--- a/cmd/helm/root.go
+++ b/cmd/helm/root.go
@@ -204,8 +204,5 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
 	// Find and add plugins
 	loadPlugins(cmd, out)
 
-	// Check permissions on critical files
-	checkPerms(out)
-
 	return cmd, nil
 }
diff --git a/cmd/helm/root_unix.go b/cmd/helm/root_unix.go
deleted file mode 100644
index 210842b35..000000000
--- a/cmd/helm/root_unix.go
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
-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 main
-
-import (
-	"fmt"
-	"io"
-	"os"
-	"os/user"
-	"path/filepath"
-)
-
-func checkPerms(out io.Writer) {
-	// This function MUST NOT FAIL, as it is just a check for a common permissions problem.
-	// If for some reason the function hits a stopping condition, it may panic. But only if
-	// we can be sure that it is panicing because Helm cannot proceed.
-
-	kc := settings.KubeConfig
-	if kc == "" {
-		kc = os.Getenv("KUBECONFIG")
-	}
-	if kc == "" {
-		u, err := user.Current()
-		if err != nil {
-			// No idea where to find KubeConfig, so return silently. Many helm commands
-			// can proceed happily without a KUBECONFIG, so this is not a fatal error.
-			return
-		}
-		kc = filepath.Join(u.HomeDir, ".kube", "config")
-	}
-	fi, err := os.Stat(kc)
-	if err != nil {
-		// DO NOT error if no KubeConfig is found. Not all commands require one.
-		return
-	}
-
-	perm := fi.Mode().Perm()
-	if perm&0040 > 0 {
-		fmt.Fprintf(out, "WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: %s\n", kc)
-	}
-	if perm&0004 > 0 {
-		fmt.Fprintf(out, "WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: %s\n", kc)
-	}
-}
diff --git a/cmd/helm/root_windows.go b/cmd/helm/root_windows.go
deleted file mode 100644
index 243780d40..000000000
--- a/cmd/helm/root_windows.go
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
-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 main
-
-import "io"
-
-func checkPerms(out io.Writer) {
-	// Not yet implemented on Windows. If you know how to do a comprehensive perms
-	// check on Windows, contributions welcomed!
-}
diff --git a/pkg/action/action.go b/pkg/action/action.go
index 79bb4f638..071db709b 100644
--- a/pkg/action/action.go
+++ b/pkg/action/action.go
@@ -58,15 +58,14 @@ var (
 	errMissingRelease = errors.New("no release provided")
 	// errInvalidRevision indicates that an invalid release revision number was provided.
 	errInvalidRevision = errors.New("invalid release revision")
+	// errInvalidName indicates that an invalid release name was provided
+	errInvalidName = errors.New("invalid release name, must match regex ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$ and the length must not longer than 53")
 	// errPending indicates that another instance of Helm is already applying an operation on a release.
 	errPending = errors.New("another operation (install/upgrade/rollback) is in progress")
 )
 
 // ValidName is a regular expression for resource names.
 //
-// DEPRECATED: This will be removed in Helm 4, and is no longer used here. See
-// pkg/chartutil.ValidateName for the replacement.
-//
 // According to the Kubernetes help text, the regular expression it uses is:
 //
 //	[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
@@ -295,7 +294,7 @@ func (c *Configuration) Now() time.Time {
 }
 
 func (c *Configuration) releaseContent(name string, version int) (*release.Release, error) {
-	if err := chartutil.ValidateReleaseName(name); err != nil {
+	if err := validateReleaseName(name); err != nil {
 		return nil, errors.Errorf("releaseContent: Release name is invalid: %s", name)
 	}
 
diff --git a/pkg/action/dependency.go b/pkg/action/dependency.go
index 4c80d0159..4a4b8ebad 100644
--- a/pkg/action/dependency.go
+++ b/pkg/action/dependency.go
@@ -21,7 +21,6 @@ import (
 	"io"
 	"os"
 	"path/filepath"
-	"strings"
 
 	"github.com/Masterminds/semver/v3"
 	"github.com/gosuri/uitable"
@@ -62,7 +61,6 @@ func (d *Dependency) List(chartpath string, out io.Writer) error {
 	return nil
 }
 
-// dependecyStatus returns a string describing the status of a dependency viz a viz the parent chart.
 func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, parent *chart.Chart) string {
 	filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*")
 
@@ -77,40 +75,35 @@ func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, p
 	case err != nil:
 		return "bad pattern"
 	case len(archives) > 1:
-		// See if the second part is a SemVer
-		found := []string{}
-		for _, arc := range archives {
-			// we need to trip the prefix dirs and the extension off.
-			filename = strings.TrimSuffix(filepath.Base(arc), ".tgz")
-			maybeVersion := strings.TrimPrefix(filename, fmt.Sprintf("%s-", dep.Name))
-
-			if _, err := semver.StrictNewVersion(maybeVersion); err == nil {
-				// If the version parsed without an error, it is possibly a valid
-				// version.
-				found = append(found, arc)
+		return "too many matches"
+	case len(archives) == 1:
+		archive := archives[0]
+		if _, err := os.Stat(archive); err == nil {
+			c, err := loader.Load(archive)
+			if err != nil {
+				return "corrupt"
 			}
-		}
-
-		if l := len(found); l == 1 {
-			// If we get here, we do the same thing as in len(archives) == 1.
-			if r := statArchiveForStatus(found[0], dep); r != "" {
-				return r
+			if c.Name() != dep.Name {
+				return "misnamed"
 			}
 
-			// Fall through and look for directories
-		} else if l > 1 {
-			return "too many matches"
-		}
+			if c.Metadata.Version != dep.Version {
+				constraint, err := semver.NewConstraint(dep.Version)
+				if err != nil {
+					return "invalid version"
+				}
 
-		// The sanest thing to do here is to fall through and see if we have any directory
-		// matches.
+				v, err := semver.NewVersion(c.Metadata.Version)
+				if err != nil {
+					return "invalid version"
+				}
 
-	case len(archives) == 1:
-		archive := archives[0]
-		if r := statArchiveForStatus(archive, dep); r != "" {
-			return r
+				if !constraint.Check(v) {
+					return "wrong version"
+				}
+			}
+			return "ok"
 		}
-
 	}
 	// End unnecessary code.
 
@@ -144,40 +137,6 @@ func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, p
 	return "unpacked"
 }
 
-// stat an archive and return a message if the stat is successful
-//
-// This is a refactor of the code originally in dependencyStatus. It is here to
-// support legacy behavior, and should be removed in Helm 4.
-func statArchiveForStatus(archive string, dep *chart.Dependency) string {
-	if _, err := os.Stat(archive); err == nil {
-		c, err := loader.Load(archive)
-		if err != nil {
-			return "corrupt"
-		}
-		if c.Name() != dep.Name {
-			return "misnamed"
-		}
-
-		if c.Metadata.Version != dep.Version {
-			constraint, err := semver.NewConstraint(dep.Version)
-			if err != nil {
-				return "invalid version"
-			}
-
-			v, err := semver.NewVersion(c.Metadata.Version)
-			if err != nil {
-				return "invalid version"
-			}
-
-			if !constraint.Check(v) {
-				return "wrong version"
-			}
-		}
-		return "ok"
-	}
-	return ""
-}
-
 // printDependencies prints all of the dependencies in the yaml file.
 func (d *Dependency) printDependencies(chartpath string, out io.Writer, c *chart.Chart) {
 	table := uitable.New()
diff --git a/pkg/action/history.go b/pkg/action/history.go
index f4043609c..a592745e9 100644
--- a/pkg/action/history.go
+++ b/pkg/action/history.go
@@ -19,7 +19,6 @@ package action
 import (
 	"github.com/pkg/errors"
 
-	"helm.sh/helm/v3/pkg/chartutil"
 	"helm.sh/helm/v3/pkg/release"
 )
 
@@ -46,7 +45,7 @@ func (h *History) Run(name string) ([]*release.Release, error) {
 		return nil, err
 	}
 
-	if err := chartutil.ValidateReleaseName(name); err != nil {
+	if err := validateReleaseName(name); err != nil {
 		return nil, errors.Errorf("release name is invalid: %s", name)
 	}
 
diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go
index 542acefae..8773b6271 100644
--- a/pkg/action/rollback.go
+++ b/pkg/action/rollback.go
@@ -24,7 +24,6 @@ import (
 
 	"github.com/pkg/errors"
 
-	"helm.sh/helm/v3/pkg/chartutil"
 	"helm.sh/helm/v3/pkg/release"
 	helmtime "helm.sh/helm/v3/pkg/time"
 )
@@ -91,7 +90,7 @@ func (r *Rollback) Run(name string) error {
 // prepareRollback finds the previous release and prepares a new release object with
 // the previous release's configuration
 func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Release, error) {
-	if err := chartutil.ValidateReleaseName(name); err != nil {
+	if err := validateReleaseName(name); err != nil {
 		return nil, nil, errors.Errorf("prepareRollback: Release name is invalid: %s", name)
 	}
 
diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go
index c466c6ee2..a51a283d6 100644
--- a/pkg/action/uninstall.go
+++ b/pkg/action/uninstall.go
@@ -22,7 +22,6 @@ import (
 
 	"github.com/pkg/errors"
 
-	"helm.sh/helm/v3/pkg/chartutil"
 	"helm.sh/helm/v3/pkg/release"
 	"helm.sh/helm/v3/pkg/releaseutil"
 	helmtime "helm.sh/helm/v3/pkg/time"
@@ -63,7 +62,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
 		return &release.UninstallReleaseResponse{Release: r}, nil
 	}
 
-	if err := chartutil.ValidateReleaseName(name); err != nil {
+	if err := validateReleaseName(name); err != nil {
 		return nil, errors.Errorf("uninstall: Release name is invalid: %s", name)
 	}
 
diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go
index c439af79d..b707e7e69 100644
--- a/pkg/action/upgrade.go
+++ b/pkg/action/upgrade.go
@@ -115,7 +115,7 @@ func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface
 	// the user doesn't have to specify both
 	u.Wait = u.Wait || u.Atomic
 
-	if err := chartutil.ValidateReleaseName(name); err != nil {
+	if err := validateReleaseName(name); err != nil {
 		return nil, errors.Errorf("release name is invalid: %s", name)
 	}
 	u.cfg.Log("preparing upgrade for %s", name)
@@ -142,6 +142,19 @@ func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface
 	return res, nil
 }
 
+func validateReleaseName(releaseName string) error {
+	if releaseName == "" {
+		return errMissingRelease
+	}
+
+	// Check length first, since that is a less expensive operation.
+	if len(releaseName) > releaseNameMaxLen || !ValidName.MatchString(releaseName) {
+		return errInvalidName
+	}
+
+	return nil
+}
+
 // prepareUpgrade builds an upgraded release for an upgrade operation.
 func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, *release.Release, error) {
 	if chart == nil {
diff --git a/pkg/chartutil/create.go b/pkg/chartutil/create.go
index d4b65e9b8..6e382b961 100644
--- a/pkg/chartutil/create.go
+++ b/pkg/chartutil/create.go
@@ -21,7 +21,6 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
-	"regexp"
 	"strings"
 
 	"github.com/pkg/errors"
@@ -31,12 +30,6 @@ import (
 	"helm.sh/helm/v3/pkg/chart/loader"
 )
 
-// chartName is a regular expression for testing the supplied name of a chart.
-// This regular expression is probably stricter than it needs to be. We can relax it
-// somewhat. Newline characters, as well as $, quotes, +, parens, and % are known to be
-// problematic.
-var chartName = regexp.MustCompile("^[a-zA-Z0-9._-]+$")
-
 const (
 	// ChartfileName is the default Chart file name.
 	ChartfileName = "Chart.yaml"
@@ -70,10 +63,6 @@ const (
 	TestConnectionName = TemplatesTestsDir + sep + "test-connection.yaml"
 )
 
-// maxChartNameLength is lower than the limits we know of with certain file systems,
-// and with certain Kubernetes fields.
-const maxChartNameLength = 250
-
 const sep = string(filepath.Separator)
 
 const defaultChartfile = `apiVersion: v2
@@ -533,12 +522,6 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
 // error. In such a case, this will attempt to clean up by removing the
 // new chart directory.
 func Create(name, dir string) (string, error) {
-
-	// Sanity-check the name of a chart so user doesn't create one that causes problems.
-	if err := validateChartName(name); err != nil {
-		return "", err
-	}
-
 	path, err := filepath.Abs(dir)
 	if err != nil {
 		return path, err
@@ -644,13 +627,3 @@ func writeFile(name string, content []byte) error {
 	}
 	return ioutil.WriteFile(name, content, 0644)
 }
-
-func validateChartName(name string) error {
-	if name == "" || len(name) > maxChartNameLength {
-		return fmt.Errorf("chart name must be between 1 and %d characters", maxChartNameLength)
-	}
-	if !chartName.MatchString(name) {
-		return fmt.Errorf("chart name must match the regular expression %q", chartName.String())
-	}
-	return nil
-}
diff --git a/pkg/chartutil/validate_name.go b/pkg/chartutil/validate_name.go
deleted file mode 100644
index 22132c80e..000000000
--- a/pkg/chartutil/validate_name.go
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
-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 chartutil
-
-import (
-	"regexp"
-
-	"github.com/pkg/errors"
-)
-
-// validName is a regular expression for resource names.
-//
-// According to the Kubernetes help text, the regular expression it uses is:
-//
-//	[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
-//
-// This follows the above regular expression (but requires a full string match, not partial).
-//
-// The Kubernetes documentation is here, though it is not entirely correct:
-// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
-var validName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
-
-var (
-	// errMissingName indicates that a release (name) was not provided.
-	errMissingName = errors.New("no name provided")
-
-	// errInvalidName indicates that an invalid release name was provided
-	errInvalidName = errors.New("invalid release name, must match regex ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$ and the length must not longer than 53")
-
-	// errInvalidKubernetesName indicates that the name does not meet the Kubernetes
-	// restrictions on metadata names.
-	errInvalidKubernetesName = errors.New("invalid metadata name, must match regex ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$ and the length must not longer than 253")
-)
-
-const (
-	// maxNameLen is the maximum length Helm allows for a release name
-	maxReleaseNameLen = 53
-	// maxMetadataNameLen is the maximum length Kubernetes allows for any name.
-	maxMetadataNameLen = 253
-)
-
-// ValidateReleaseName performs checks for an entry for a Helm release name
-//
-// For Helm to allow a name, it must be below a certain character count (53) and also match
-// a reguar expression.
-//
-// According to the Kubernetes help text, the regular expression it uses is:
-//
-//	[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
-//
-// This follows the above regular expression (but requires a full string match, not partial).
-//
-// The Kubernetes documentation is here, though it is not entirely correct:
-// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
-func ValidateReleaseName(name string) error {
-	// This case is preserved for backwards compatibility
-	if name == "" {
-		return errMissingName
-
-	}
-	if len(name) > maxReleaseNameLen || !validName.MatchString(name) {
-		return errInvalidName
-	}
-	return nil
-}
-
-// ValidateMetadataName validates the name field of a Kubernetes metadata object.
-//
-// Empty strings, strings longer than 253 chars, or strings that don't match the regexp
-// will fail.
-//
-// According to the Kubernetes help text, the regular expression it uses is:
-//
-//	[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
-//
-// This follows the above regular expression (but requires a full string match, not partial).
-//
-// The Kubernetes documentation is here, though it is not entirely correct:
-// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
-func ValidateMetadataName(name string) error {
-	if name == "" || len(name) > maxMetadataNameLen || !validName.MatchString(name) {
-		return errInvalidKubernetesName
-	}
-	return nil
-}
diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go
index 5de0819c4..73d645264 100644
--- a/pkg/lint/rules/template.go
+++ b/pkg/lint/rules/template.go
@@ -40,6 +40,14 @@ var (
 	releaseTimeSearch = regexp.MustCompile(`\.Release\.Time`)
 )
 
+// validName is a regular expression for names.
+//
+// This is different than action.ValidName. It conforms to the regular expression
+// `kubectl` says it uses, plus it disallows empty names.
+//
+// For details, see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+var validName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
+
 // Templates lints the templates in the Linter.
 func Templates(linter *support.Linter, values map[string]interface{}, namespace string, strict bool) {
 	fpath := "templates/"
@@ -191,10 +199,10 @@ func validateMetadataName(obj *K8sYamlStruct) error {
 	}
 	// This will return an error if the characters do not abide by the standard OR if the
 	// name is left empty.
-	if err := chartutil.ValidateMetadataName(obj.Metadata.Name); err != nil {
-		return errors.Wrapf(err, "object name does not conform to Kubernetes naming requirements: %q", obj.Metadata.Name)
+	if validName.MatchString(obj.Metadata.Name) {
+		return nil
 	}
-	return nil
+	return fmt.Errorf("object name does not conform to Kubernetes naming requirements: %q", obj.Metadata.Name)
 }
 
 func validateNoCRDHooks(manifest []byte) error {
diff --git a/pkg/plugin/installer/http_installer.go b/pkg/plugin/installer/http_installer.go
index 28e50b72b..bcbcbde93 100644
--- a/pkg/plugin/installer/http_installer.go
+++ b/pkg/plugin/installer/http_installer.go
@@ -59,6 +59,18 @@ var Extractors = map[string]Extractor{
 	".tgz":    &TarGzExtractor{},
 }
 
+// Convert a media type to an extractor extension.
+//
+// This should be refactored in Helm 4, combined with the extension-based mechanism.
+func mediaTypeToExtension(mt string) (string, bool) {
+	switch strings.ToLower(mt) {
+	case "application/gzip", "application/x-gzip", "application/x-tgz", "application/x-gtar":
+		return ".tgz", true
+	default:
+		return "", false
+	}
+}
+
 // NewExtractor creates a new extractor matching the source file name
 func NewExtractor(source string) (Extractor, error) {
 	for suffix, extractor := range Extractors {
diff --git a/pkg/plugin/installer/installer.go b/pkg/plugin/installer/installer.go
index 61b49ab3b..6f01494e5 100644
--- a/pkg/plugin/installer/installer.go
+++ b/pkg/plugin/installer/installer.go
@@ -18,6 +18,7 @@ package installer
 import (
 	"fmt"
 	"log"
+	"net/http"
 	"os"
 	"path/filepath"
 	"strings"
@@ -89,10 +90,29 @@ func isLocalReference(source string) bool {
 }
 
 // isRemoteHTTPArchive checks if the source is a http/https url and is an archive
+//
+// It works by checking whether the source looks like a URL and, if it does, running a
+// HEAD operation to see if the remote resource is a file that we understand.
 func isRemoteHTTPArchive(source string) bool {
 	if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") {
+		res, err := http.Head(source)
+		if err != nil {
+			// If we get an error at the network layer, we can't install it. So
+			// we return false.
+			return false
+		}
+
+		// Next, we look for the content type or content disposition headers to see
+		// if they have matching extractors.
+		contentType := res.Header.Get("content-type")
+		foundSuffix, ok := mediaTypeToExtension(contentType)
+		if !ok {
+			// Media type not recognized
+			return false
+		}
+
 		for suffix := range Extractors {
-			if strings.HasSuffix(source, suffix) {
+			if strings.HasSuffix(foundSuffix, suffix) {
 				return true
 			}
 		}


Test patch:
diff --git a/cmd/helm/root_unix_test.go b/cmd/helm/root_unix_test.go
deleted file mode 100644
index 73f18ec28..000000000
--- a/cmd/helm/root_unix_test.go
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
-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 main
-
-import (
-	"bytes"
-	"io/ioutil"
-	"os"
-	"path/filepath"
-	"strings"
-	"testing"
-)
-
-func TestCheckPerms(t *testing.T) {
-	tdir, err := ioutil.TempDir("", "helmtest")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.RemoveAll(tdir)
-	tfile := filepath.Join(tdir, "testconfig")
-	fh, err := os.OpenFile(tfile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0440)
-	if err != nil {
-		t.Errorf("Failed to create temp file: %s", err)
-	}
-
-	tconfig := settings.KubeConfig
-	settings.KubeConfig = tfile
-	defer func() { settings.KubeConfig = tconfig }()
-
-	var b bytes.Buffer
-	checkPerms(&b)
-	expectPrefix := "WARNING: Kubernetes configuration file is group-readable. This is insecure. Location:"
-	if !strings.HasPrefix(b.String(), expectPrefix) {
-		t.Errorf("Expected to get a warning for group perms. Got %q", b.String())
-	}
-
-	if err := fh.Chmod(0404); err != nil {
-		t.Errorf("Could not change mode on file: %s", err)
-	}
-	b.Reset()
-	checkPerms(&b)
-	expectPrefix = "WARNING: Kubernetes configuration file is world-readable. This is insecure. Location:"
-	if !strings.HasPrefix(b.String(), expectPrefix) {
-		t.Errorf("Expected to get a warning for world perms. Got %q", b.String())
-	}
-
-}
diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go
index fedf260fb..c05b4403d 100644
--- a/pkg/action/action_test.go
+++ b/pkg/action/action_test.go
@@ -319,3 +319,40 @@ func TestGetVersionSet(t *testing.T) {
 		t.Error("Non-existent version is reported found.")
 	}
 }
+
+// TestValidName is a regression test for ValidName
+//
+// Kubernetes has strict naming conventions for resource names. This test represents
+// those conventions.
+//
+// See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
+//
+// NOTE: At the time of this writing, the docs above say that names cannot begin with
+// digits. However, `kubectl`'s regular expression explicit allows this, and
+// Kubernetes (at least as of 1.18) also accepts resources whose names begin with digits.
+func TestValidName(t *testing.T) {
+	names := map[string]bool{
+		"":                          false,
+		"foo":                       true,
+		"foo.bar1234baz.seventyone": true,
+		"FOO":                       false,
+		"123baz":                    true,
+		"foo.BAR.baz":               false,
+		"one-two":                   true,
+		"-two":                      false,
+		"one_two":                   false,
+		"a..b":                      false,
+		"%^&#$%*@^*@&#^":            false,
+		"example:com":               false,
+		"example%%com":              false,
+	}
+	for input, expectPass := range names {
+		if ValidName.MatchString(input) != expectPass {
+			st := "fail"
+			if expectPass {
+				st = "succeed"
+			}
+			t.Errorf("Expected %q to %s", input, st)
+		}
+	}
+}
diff --git a/pkg/action/dependency_test.go b/pkg/action/dependency_test.go
index b5032a377..4f3cb69a5 100644
--- a/pkg/action/dependency_test.go
+++ b/pkg/action/dependency_test.go
@@ -18,16 +18,9 @@ package action
 
 import (
 	"bytes"
-	"io/ioutil"
-	"os"
-	"path/filepath"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
-
 	"helm.sh/helm/v3/internal/test"
-	"helm.sh/helm/v3/pkg/chart"
-	"helm.sh/helm/v3/pkg/chartutil"
 )
 
 func TestList(t *testing.T) {
@@ -63,99 +56,3 @@ func TestList(t *testing.T) {
 		test.AssertGoldenBytes(t, buf.Bytes(), tcase.golden)
 	}
 }
-
-// TestDependencyStatus_Dashes is a regression test to make sure that dashes in
-// chart names do not cause resolution problems.
-func TestDependencyStatus_Dashes(t *testing.T) {
-	// Make a temp dir
-	dir, err := ioutil.TempDir("", "helmtest-")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.RemoveAll(dir)
-
-	chartpath := filepath.Join(dir, "charts")
-	if err := os.MkdirAll(chartpath, 0700); err != nil {
-		t.Fatal(err)
-	}
-
-	// Add some fake charts
-	first := buildChart(withName("first-chart"))
-	_, err = chartutil.Save(first, chartpath)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	second := buildChart(withName("first-chart-second-chart"))
-	_, err = chartutil.Save(second, chartpath)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	dep := &chart.Dependency{
-		Name:    "first-chart",
-		Version: "0.1.0",
-	}
-
-	// Now try to get the deps
-	stat := NewDependency().dependencyStatus(dir, dep, first)
-	if stat != "ok" {
-		t.Errorf("Unexpected status: %q", stat)
-	}
-}
-
-func TestStatArchiveForStatus(t *testing.T) {
-	// Make a temp dir
-	dir, err := ioutil.TempDir("", "helmtest-")
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer os.RemoveAll(dir)
-
-	chartpath := filepath.Join(dir, "charts")
-	if err := os.MkdirAll(chartpath, 0700); err != nil {
-		t.Fatal(err)
-	}
-
-	// unsaved chart
-	lilith := buildChart(withName("lilith"))
-
-	// dep referring to chart
-	dep := &chart.Dependency{
-		Name:    "lilith",
-		Version: "1.2.3",
-	}
-
-	is := assert.New(t)
-
-	lilithpath := filepath.Join(chartpath, "lilith-1.2.3.tgz")
-	is.Empty(statArchiveForStatus(lilithpath, dep))
-
-	// save the chart (version 0.1.0, because that is the default)
-	where, err := chartutil.Save(lilith, chartpath)
-	is.NoError(err)
-
-	// Should get "wrong version" because we asked for 1.2.3 and got 0.1.0
-	is.Equal("wrong version", statArchiveForStatus(where, dep))
-
-	// Break version on dep
-	dep = &chart.Dependency{
-		Name:    "lilith",
-		Version: "1.2.3.4.5",
-	}
-	is.Equal("invalid version", statArchiveForStatus(where, dep))
-
-	// Break the name
-	dep = &chart.Dependency{
-		Name:    "lilith2",
-		Version: "1.2.3",
-	}
-	is.Equal("misnamed", statArchiveForStatus(where, dep))
-
-	// Now create the right version
-	dep = &chart.Dependency{
-		Name:    "lilith",
-		Version: "0.1.0",
-	}
-	is.Equal("ok", statArchiveForStatus(where, dep))
-}
diff --git a/pkg/action/release_testing.go b/pkg/action/release_testing.go
index 2f6f5cfce..795c3c747 100644
--- a/pkg/action/release_testing.go
+++ b/pkg/action/release_testing.go
@@ -25,7 +25,6 @@ import (
 	"github.com/pkg/errors"
 	v1 "k8s.io/api/core/v1"
 
-	"helm.sh/helm/v3/pkg/chartutil"
 	"helm.sh/helm/v3/pkg/release"
 )
 
@@ -52,7 +51,7 @@ func (r *ReleaseTesting) Run(name string) (*release.Release, error) {
 		return nil, err
 	}
 
-	if err := chartutil.ValidateReleaseName(name); err != nil {
+	if err := validateReleaseName(name); err != nil {
 		return nil, errors.Errorf("releaseTest: Release name is invalid: %s", name)
 	}
 
diff --git a/pkg/chartutil/create_test.go b/pkg/chartutil/create_test.go
index f68ebbd63..a11c45140 100644
--- a/pkg/chartutil/create_test.go
+++ b/pkg/chartutil/create_test.go
@@ -117,30 +117,3 @@ func TestCreateFrom(t *testing.T) {
 		}
 	}
 }
-
-func TestValidateChartName(t *testing.T) {
-	for name, shouldPass := range map[string]bool{
-		"":                              false,
-		"abcdefghijklmnopqrstuvwxyz-_.": true,
-		"ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": true,
-		"$hello":                        false,
-		"Hellô":                         false,
-		"he%%o":                         false,
-		"he\nllo":                       false,
-
-		"abcdefghijklmnopqrstuvwxyz-_." +
-			"abcdefghijklmnopqrstuvwxyz-_." +
-			"abcdefghijklmnopqrstuvwxyz-_." +
-			"abcdefghijklmnopqrstuvwxyz-_." +
-			"abcdefghijklmnopqrstuvwxyz-_." +
-			"abcdefghijklmnopqrstuvwxyz-_." +
-			"abcdefghijklmnopqrstuvwxyz-_." +
-			"abcdefghijklmnopqrstuvwxyz-_." +
-			"abcdefghijklmnopqrstuvwxyz-_." +
-			"ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": false,
-	} {
-		if err := validateChartName(name); (err != nil) == shouldPass {
-			t.Errorf("test for %q failed", name)
-		}
-	}
-}
diff --git a/pkg/chartutil/validate_name_test.go b/pkg/chartutil/validate_name_test.go
deleted file mode 100644
index 5f0792f94..000000000
--- a/pkg/chartutil/validate_name_test.go
+++ /dev/null
@@ -1,91 +0,0 @@
-/*
-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 chartutil
-
-import "testing"
-
-// TestValidateName is a regression test for ValidateName
-//
-// Kubernetes has strict naming conventions for resource names. This test represents
-// those conventions.
-//
-// See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
-//
-// NOTE: At the time of this writing, the docs above say that names cannot begin with
-// digits. However, `kubectl`'s regular expression explicit allows this, and
-// Kubernetes (at least as of 1.18) also accepts resources whose names begin with digits.
-func TestValidateReleaseName(t *testing.T) {
-	names := map[string]bool{
-		"":                          false,
-		"foo":                       true,
-		"foo.bar1234baz.seventyone": true,
-		"FOO":                       false,
-		"123baz":                    true,
-		"foo.BAR.baz":               false,
-		"one-two":                   true,
-		"-two":                      false,
-		"one_two":                   false,
-		"a..b":                      false,
-		"%^&#$%*@^*@&#^":            false,
-		"example:com":               false,
-		"example%%com":              false,
-		"a1111111111111111111111111111111111111111111111111111111111z": false,
-	}
-	for input, expectPass := range names {
-		if err := ValidateReleaseName(input); (err == nil) != expectPass {
-			st := "fail"
-			if expectPass {
-				st = "succeed"
-			}
-			t.Errorf("Expected %q to %s", input, st)
-		}
-	}
-}
-
-func TestValidateMetadataName(t *testing.T) {
-	names := map[string]bool{
-		"":                          false,
-		"foo":                       true,
-		"foo.bar1234baz.seventyone": true,
-		"FOO":                       false,
-		"123baz":                    true,
-		"foo.BAR.baz":               false,
-		"one-two":                   true,
-		"-two":                      false,
-		"one_two":                   false,
-		"a..b":                      false,
-		"%^&#$%*@^*@&#^":            false,
-		"example:com":               false,
-		"example%%com":              false,
-		"a1111111111111111111111111111111111111111111111111111111111z": true,
-		"a1111111111111111111111111111111111111111111111111111111111z" +
-			"a1111111111111111111111111111111111111111111111111111111111z" +
-			"a1111111111111111111111111111111111111111111111111111111111z" +
-			"a1111111111111111111111111111111111111111111111111111111111z" +
-			"a1111111111111111111111111111111111111111111111111111111111z" +
-			"a1111111111111111111111111111111111111111111111111111111111z": false,
-	}
-	for input, expectPass := range names {
-		if err := ValidateMetadataName(input); (err == nil) != expectPass {
-			st := "fail"
-			if expectPass {
-				st = "succeed"
-			}
-			t.Errorf("Expected %q to %s", input, st)
-		}
-	}
-}
diff --git a/pkg/plugin/installer/http_installer_test.go b/pkg/plugin/installer/http_installer_test.go
index 3eb92ee77..e89fea29d 100644
--- a/pkg/plugin/installer/http_installer_test.go
+++ b/pkg/plugin/installer/http_installer_test.go
@@ -20,9 +20,13 @@ import (
 	"bytes"
 	"compress/gzip"
 	"encoding/base64"
+	"fmt"
 	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
 	"os"
 	"path/filepath"
+	"strings"
 	"syscall"
 	"testing"
 
@@ -63,9 +67,24 @@ func TestStripName(t *testing.T) {
 	}
 }
 
+func mockArchiveServer() *httptest.Server {
+	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		if !strings.HasSuffix(r.URL.Path, ".tar.gz") {
+			w.Header().Add("Content-Type", "text/html")
+			fmt.Fprintln(w, "broken")
+			return
+		}
+		w.Header().Add("Content-Type", "application/gzip")
+		fmt.Fprintln(w, "test")
+	}))
+}
+
 func TestHTTPInstaller(t *testing.T) {
 	defer ensure.HelmHome(t)()
-	source := "https://repo.localdomain/plugins/fake-plugin-0.0.1.tar.gz"
+
+	srv := mockArchiveServer()
+	defer srv.Close()
+	source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
 
 	if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil {
 		t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err)
@@ -111,7 +130,9 @@ func TestHTTPInstaller(t *testing.T) {
 
 func TestHTTPInstallerNonExistentVersion(t *testing.T) {
 	defer ensure.HelmHome(t)()
-	source := "https://repo.localdomain/plugins/fake-plugin-0.0.2.tar.gz"
+	srv := mockArchiveServer()
+	defer srv.Close()
+	source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
 
 	if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil {
 		t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err)
@@ -141,7 +162,9 @@ func TestHTTPInstallerNonExistentVersion(t *testing.T) {
 }
 
 func TestHTTPInstallerUpdate(t *testing.T) {
-	source := "https://repo.localdomain/plugins/fake-plugin-0.0.1.tar.gz"
+	srv := mockArchiveServer()
+	defer srv.Close()
+	source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
 	defer ensure.HelmHome(t)()
 
 	if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil {
@@ -307,3 +330,26 @@ func TestCleanJoin(t *testing.T) {
 	}
 
 }
+
+func TestMediaTypeToExtension(t *testing.T) {
+
+	for mt, shouldPass := range map[string]bool{
+		"":                   false,
+		"application/gzip":   true,
+		"application/x-gzip": true,
+		"application/x-tgz":  true,
+		"application/x-gtar": true,
+		"application/json":   false,
+	} {
+		ext, ok := mediaTypeToExtension(mt)
+		if ok != shouldPass {
+			t.Errorf("Media type %q failed test", mt)
+		}
+		if shouldPass && ext == "" {
+			t.Errorf("Expected an extension but got empty string")
+		}
+		if !shouldPass && len(ext) != 0 {
+			t.Error("Expected extension to be empty for unrecognized type")
+		}
+	}
+}
diff --git a/pkg/plugin/installer/installer_test.go b/pkg/plugin/installer/installer_test.go
new file mode 100644
index 000000000..a11464924
--- /dev/null
+++ b/pkg/plugin/installer/installer_test.go
@@ -0,0 +1,40 @@
+/*
+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 installer
+
+import "testing"
+
+func TestIsRemoteHTTPArchive(t *testing.T) {
+	srv := mockArchiveServer()
+	defer srv.Close()
+	source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
+
+	if isRemoteHTTPArchive("/not/a/URL") {
+		t.Errorf("Expected non-URL to return false")
+	}
+
+	if isRemoteHTTPArchive("https://127.0.0.1:123/fake/plugin-1.2.3.tgz") {
+		t.Errorf("Bad URL should not have succeeded.")
+	}
+
+	if !isRemoteHTTPArchive(source) {
+		t.Errorf("Expected %q to be a valid archive URL", source)
+	}
+
+	if isRemoteHTTPArchive(source + "-not-an-extension") {
+		t.Error("Expected media type match to fail")
+	}
+}