Repository: helm/helm
Pull request #7246 (2019-12-16 20:56:51)

[helm create] Include serviceAccount.annotations value
In order to ease the use of features such as [GKE Workload Identity](https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity#enable_workload_identity_on_a_new_cluster), it is beneficial to allow users to annotate service accounts out of the box.


Patch:
diff --git a/Makefile b/Makefile
index 611222e28..31e1f4374 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
 BINDIR      := $(CURDIR)/bin
 DIST_DIRS   := find * -type d -exec
-TARGETS     := darwin/amd64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le windows/amd64
-TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-386.tar.gz linux-386.tar.gz.sha256 linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 windows-amd64.zip windows-amd64.zip.sha256
+TARGETS     := darwin/amd64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le linux/s390x windows/amd64
+TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-386.tar.gz linux-386.tar.gz.sha256 linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-s390x.tar.gz linux-s390x.tar.gz.sha256 windows-amd64.zip windows-amd64.zip.sha256
 BINNAME     ?= helm
 
 GOPATH        = $(shell go env GOPATH)
@@ -9,7 +9,7 @@ DEP           = $(GOPATH)/bin/dep
 GOX           = $(GOPATH)/bin/gox
 GOIMPORTS     = $(GOPATH)/bin/goimports
 
-ACCEPTANCE_DIR:=$(GOPATH)/src/helm.sh/acceptance-testing
+ACCEPTANCE_DIR:=../acceptance-testing
 # To specify the subset of acceptance tests to run. '.' means all tests
 ACCEPTANCE_RUN_TESTS=.
 
diff --git a/cmd/helm/flags.go b/cmd/helm/flags.go
index 467abbd6e..aa22603f4 100644
--- a/cmd/helm/flags.go
+++ b/cmd/helm/flags.go
@@ -31,7 +31,7 @@ import (
 const outputFlag = "output"
 
 func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) {
-	f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL(can specify multiple)")
+	f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)")
 	f.StringArrayVar(&v.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
 	f.StringArrayVar(&v.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)")
 	f.StringArrayVar(&v.FileValues, "set-file", []string{}, "set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)")
diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go
index 2c3c9e401..112d5123f 100644
--- a/cmd/helm/helm.go
+++ b/cmd/helm/helm.go
@@ -68,8 +68,7 @@ func main() {
 	cmd := newRootCmd(actionConfig, os.Stdout, os.Args[1:])
 
 	if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), os.Getenv("HELM_DRIVER"), debug); err != nil {
-		debug("%+v", err)
-		os.Exit(1)
+		log.Fatal(err)
 	}
 
 	if err := cmd.Execute(); err != nil {
diff --git a/cmd/helm/list.go b/cmd/helm/list.go
index 0da0c21d3..57fc4be3c 100644
--- a/cmd/helm/list.go
+++ b/cmd/helm/list.go
@@ -32,7 +32,7 @@ import (
 )
 
 var listHelp = `
-This command lists all of the releases.
+This command lists all of the releases for a specified namespace (uses current namespace context if namespace not specified).
 
 By default, it lists only releases that are deployed or failed. Flags like
 '--uninstalled' and '--all' will alter this behavior. Such flags can be combined:
@@ -96,7 +96,7 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 	f.BoolVarP(&client.Short, "short", "q", false, "output short (quiet) listing format")
 	f.BoolVarP(&client.ByDate, "date", "d", false, "sort by release date")
 	f.BoolVarP(&client.SortReverse, "reverse", "r", false, "reverse the sort order")
-	f.BoolVarP(&client.All, "all", "a", false, "show all releases, not just the ones marked deployed or failed")
+	f.BoolVarP(&client.All, "all", "a", false, "show all releases without any filter applied")
 	f.BoolVar(&client.Uninstalled, "uninstalled", false, "show uninstalled releases (if 'helm uninstall --keep-history' was used)")
 	f.BoolVar(&client.Superseded, "superseded", false, "show superseded releases")
 	f.BoolVar(&client.Uninstalling, "uninstalling", false, "show releases that are currently being uninstalled")
diff --git a/cmd/helm/root.go b/cmd/helm/root.go
index 443d718d5..3405299fd 100644
--- a/cmd/helm/root.go
+++ b/cmd/helm/root.go
@@ -69,7 +69,7 @@ __helm_override_flags_to_kubectl_flags()
 
 __helm_get_repos()
 {
-    eval $(__helm_binary_name) repo list 2>/dev/null | \tail +2 | \cut -f1
+    eval $(__helm_binary_name) repo list 2>/dev/null | \tail -n +2 | \cut -f1
 }
 
 __helm_get_contexts()
@@ -236,7 +236,7 @@ __helm_list_plugins()
     __helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
     local out
     # Use eval in case helm_binary_name contains a variable (e.g., $HOME/bin/h3)
-    if out=$(eval $(__helm_binary_name) plugin list 2>/dev/null | \tail +2 | \cut -f1); then
+    if out=$(eval $(__helm_binary_name) plugin list 2>/dev/null | \tail -n +2 | \cut -f1); then
         COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
     fi
 }
diff --git a/cmd/helm/testdata/testcharts/object-order/values.yaml b/cmd/helm/testdata/testcharts/object-order/values.yaml
new file mode 100644
index 000000000..e69de29bb
diff --git a/go.mod b/go.mod
index 8e972fdfe..ed074074d 100644
--- a/go.mod
+++ b/go.mod
@@ -59,7 +59,6 @@ require (
 	google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6 // indirect
 	google.golang.org/grpc v1.24.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
-	gopkg.in/yaml.v2 v2.2.4
 	k8s.io/api v0.0.0-20191016110408-35e52d86657a
 	k8s.io/apiextensions-apiserver v0.0.0-20191016113550-5357c4baaf65
 	k8s.io/apimachinery v0.0.0-20191004115801-a2eda9f80ab8
diff --git a/go.sum b/go.sum
index fb11fdbc4..629d466c2 100644
--- a/go.sum
+++ b/go.sum
@@ -25,12 +25,8 @@ github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e h1:eb0Pzkt15Bm
 github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
 github.com/Masterminds/goutils v1.1.0 h1:zukEsf/1JZwCMgHiK3GZftabmxiCw4apj3a28RPBiVg=
 github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
-github.com/Masterminds/semver/v3 v3.0.1 h1:2kKm5lb7dKVrt5TYUiAavE6oFc1cFT0057UVGT+JqLk=
-github.com/Masterminds/semver/v3 v3.0.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
 github.com/Masterminds/semver/v3 v3.0.3 h1:znjIyLfpXEDQjOIEWh+ehwpTU14UzUPub3c3sm36u14=
 github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
-github.com/Masterminds/sprig/v3 v3.0.0 h1:KSQz7Nb08/3VU9E4ns29dDxcczhOD1q7O1UfM4G3t3g=
-github.com/Masterminds/sprig/v3 v3.0.0/go.mod h1:NEUY/Qq8Gdm2xgYA+NwJM6wmfdRV9xkh8h/Rld20R0U=
 github.com/Masterminds/sprig/v3 v3.0.2 h1:wz22D0CiSctrliXiI9ZO3HoNApweeRGftyDN+BQa3B8=
 github.com/Masterminds/sprig/v3 v3.0.2/go.mod h1:oesJ8kPONMONaZgtiHNzUShJbksypC5kWczhZAf6+aU=
 github.com/Masterminds/vcs v1.13.0 h1:USF5TvZGYgIpcbNAEMLfFhHqP08tFZVlUVrmTSpqnyA=
diff --git a/internal/monocular/search.go b/internal/monocular/search.go
index 61695f554..10e1f2136 100644
--- a/internal/monocular/search.go
+++ b/internal/monocular/search.go
@@ -58,7 +58,7 @@ type Chart struct {
 	Icon        string             `json:"icon"`
 }
 
-// Repo contains the name in monocular the the url for the repository
+// Repo contains the name in monocular the url for the repository
 type Repo struct {
 	Name string `json:"name"`
 	URL  string `json:"url"`
diff --git a/pkg/action/validate.go b/pkg/action/validate.go
index 526a4c7fc..6bbfc5e8d 100644
--- a/pkg/action/validate.go
+++ b/pkg/action/validate.go
@@ -33,7 +33,8 @@ func existingResourceConflict(resources kube.ResourceList) error {
 		}
 
 		helper := resource.NewHelper(info.Client, info.Mapping)
-		if _, err := helper.Get(info.Namespace, info.Name, info.Export); err != nil {
+		existing, err := helper.Get(info.Namespace, info.Name, info.Export)
+		if err != nil {
 			if apierrors.IsNotFound(err) {
 				return nil
 			}
@@ -41,7 +42,7 @@ func existingResourceConflict(resources kube.ResourceList) error {
 			return errors.Wrap(err, "could not get information about the resource")
 		}
 
-		return fmt.Errorf("existing resource conflict: kind: %s, namespace: %s, name: %s", info.Mapping.GroupVersionKind.Kind, info.Namespace, info.Name)
+		return fmt.Errorf("existing resource conflict: namespace: %s, name: %s, existing_kind: %s, new_kind: %s", info.Namespace, info.Name, existing.GetObjectKind().GroupVersionKind(), info.Mapping.GroupVersionKind)
 	})
 	return err
 }
diff --git a/pkg/chartutil/create.go b/pkg/chartutil/create.go
index 4ca813593..390f12f4c 100644
--- a/pkg/chartutil/create.go
+++ b/pkg/chartutil/create.go
@@ -103,6 +103,8 @@ fullnameOverride: ""
 serviceAccount:
   # Specifies whether a service account should be created
   create: true
+  # Annotations to add to the service account
+  annotations: {}
   # The name of the service account to use.
   # If not set and create is true, a name is generated using the fullname template
   name:
@@ -303,6 +305,10 @@ metadata:
   name: {{ include "<CHARTNAME>.serviceAccountName" . }}
   labels:
 {{ include "<CHARTNAME>.labels" . | nindent 4 }}
+  {{- with .Values.serviceAccount.annotations }}
+  annotations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
 {{- end -}}
 `
 
diff --git a/pkg/kube/client.go b/pkg/kube/client.go
index 07962faac..1234b5731 100644
--- a/pkg/kube/client.go
+++ b/pkg/kube/client.go
@@ -30,6 +30,7 @@ import (
 	"github.com/pkg/errors"
 	batch "k8s.io/api/batch/v1"
 	v1 "k8s.io/api/core/v1"
+	apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 
@@ -62,6 +63,10 @@ func New(getter genericclioptions.RESTClientGetter) *Client {
 		getter = genericclioptions.NewConfigFlags(true)
 	}
 	// Add CRDs to the scheme. They are missing by default.
+	if err := apiextv1.AddToScheme(scheme.Scheme); err != nil {
+		// This should never happen.
+		panic(err)
+	}
 	if err := apiextv1beta1.AddToScheme(scheme.Scheme); err != nil {
 		// This should never happen.
 		panic(err)
diff --git a/pkg/releaseutil/kind_sorter.go b/pkg/releaseutil/kind_sorter.go
index a5110a100..3ab8ab2a4 100644
--- a/pkg/releaseutil/kind_sorter.go
+++ b/pkg/releaseutil/kind_sorter.go
@@ -31,12 +31,12 @@ var InstallOrder KindSortOrder = []string{
 	"LimitRange",
 	"PodSecurityPolicy",
 	"PodDisruptionBudget",
+	"ServiceAccount",
 	"Secret",
 	"ConfigMap",
 	"StorageClass",
 	"PersistentVolume",
 	"PersistentVolumeClaim",
-	"ServiceAccount",
 	"CustomResourceDefinition",
 	"ClusterRole",
 	"ClusterRoleList",
@@ -85,12 +85,12 @@ var UninstallOrder KindSortOrder = []string{
 	"ClusterRoleList",
 	"ClusterRole",
 	"CustomResourceDefinition",
-	"ServiceAccount",
 	"PersistentVolumeClaim",
 	"PersistentVolume",
 	"StorageClass",
 	"ConfigMap",
 	"Secret",
+	"ServiceAccount",
 	"PodDisruptionBudget",
 	"PodSecurityPolicy",
 	"LimitRange",
@@ -101,10 +101,10 @@ var UninstallOrder KindSortOrder = []string{
 
 // sortByKind does an in-place sort of manifests by Kind.
 //
-// Results are sorted by 'ordering'
+// Results are sorted by 'ordering', keeping order of items with equal kind/priority
 func sortByKind(manifests []Manifest, ordering KindSortOrder) []Manifest {
 	ks := newKindSorter(manifests, ordering)
-	sort.Sort(ks)
+	sort.Stable(ks)
 	return ks.manifests
 }
 
@@ -134,13 +134,11 @@ func (k *kindSorter) Less(i, j int) bool {
 	b := k.manifests[j]
 	first, aok := k.ordering[a.Head.Kind]
 	second, bok := k.ordering[b.Head.Kind]
-	// if same kind (including unknown) sub sort alphanumeric
 	if first == second {
 		// if both are unknown and of different kind sort by kind alphabetically
 		if !aok && !bok && a.Head.Kind != b.Head.Kind {
 			return a.Head.Kind < b.Head.Kind
 		}
-		return a.Name < b.Name
 	}
 	// unknown kind is last
 	if !aok {
@@ -149,6 +147,6 @@ func (k *kindSorter) Less(i, j int) bool {
 	if !bok {
 		return true
 	}
-	// sort different kinds
+	// sort different kinds, keep original order if same priority
 	return first < second
 }
diff --git a/pkg/releaseutil/manifest.go b/pkg/releaseutil/manifest.go
index 37503b390..0b04a4599 100644
--- a/pkg/releaseutil/manifest.go
+++ b/pkg/releaseutil/manifest.go
@@ -19,6 +19,7 @@ package releaseutil
 import (
 	"fmt"
 	"regexp"
+	"strconv"
 	"strings"
 )
 
@@ -37,8 +38,9 @@ var sep = regexp.MustCompile("(?:^|\\s*\n)---\\s*")
 // SplitManifests takes a string of manifest and returns a map contains individual manifests
 func SplitManifests(bigFile string) map[string]string {
 	// Basically, we're quickly splitting a stream of YAML documents into an
-	// array of YAML docs. In the current implementation, the file name is just
-	// a place holder, and doesn't have any further meaning.
+	// array of YAML docs. The file name is just a place holder, but should be
+	// integer-sortable so that manifests get output in the same order as the
+	// input (see `BySplitManifestsOrder`).
 	tpl := "manifest-%d"
 	res := map[string]string{}
 	// Making sure that any extra whitespace in YAML stream doesn't interfere in splitting documents correctly.
@@ -56,3 +58,15 @@ func SplitManifests(bigFile string) map[string]string {
 	}
 	return res
 }
+
+// BySplitManifestsOrder sorts by in-file manifest order, as provided in function `SplitManifests`
+type BySplitManifestsOrder []string
+
+func (a BySplitManifestsOrder) Len() int { return len(a) }
+func (a BySplitManifestsOrder) Less(i, j int) bool {
+	// Split `manifest-%d`
+	anum, _ := strconv.ParseInt(a[i][len("manifest-"):], 10, 0)
+	bnum, _ := strconv.ParseInt(a[j][len("manifest-"):], 10, 0)
+	return anum < bnum
+}
+func (a BySplitManifestsOrder) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
diff --git a/pkg/releaseutil/manifest_sorter.go b/pkg/releaseutil/manifest_sorter.go
index 5f4b4e8d9..24b0c3c95 100644
--- a/pkg/releaseutil/manifest_sorter.go
+++ b/pkg/releaseutil/manifest_sorter.go
@@ -19,6 +19,7 @@ package releaseutil
 import (
 	"log"
 	"path"
+	"sort"
 	"strconv"
 	"strings"
 
@@ -74,10 +75,17 @@ var events = map[string]release.HookEvent{
 //
 // Files that do not parse into the expected format are simply placed into a map and
 // returned.
-func SortManifests(files map[string]string, apis chartutil.VersionSet, sort KindSortOrder) ([]*release.Hook, []Manifest, error) {
+func SortManifests(files map[string]string, apis chartutil.VersionSet, ordering KindSortOrder) ([]*release.Hook, []Manifest, error) {
 	result := &result{}
 
-	for filePath, c := range files {
+	var sortedFilePaths []string
+	for filePath := range files {
+		sortedFilePaths = append(sortedFilePaths, filePath)
+	}
+	sort.Strings(sortedFilePaths)
+
+	for _, filePath := range sortedFilePaths {
+		content := files[filePath]
 
 		// Skip partials. We could return these as a separate map, but there doesn't
 		// seem to be any need for that at this time.
@@ -85,12 +93,12 @@ func SortManifests(files map[string]string, apis chartutil.VersionSet, sort Kind
 			continue
 		}
 		// Skip empty files and log this.
-		if strings.TrimSpace(c) == "" {
+		if strings.TrimSpace(content) == "" {
 			continue
 		}
 
 		manifestFile := &manifestFile{
-			entries: SplitManifests(c),
+			entries: SplitManifests(content),
 			path:    filePath,
 			apis:    apis,
 		}
@@ -100,7 +108,7 @@ func SortManifests(files map[string]string, apis chartutil.VersionSet, sort Kind
 		}
 	}
 
-	return result.hooks, sortByKind(result.generic, sort), nil
+	return result.hooks, sortByKind(result.generic, ordering), nil
 }
 
 // sort takes a manifestFile object which may contain multiple resource definition
@@ -123,7 +131,16 @@ func SortManifests(files map[string]string, apis chartutil.VersionSet, sort Kind
 // 		annotations:
 // 			helm.sh/hook-delete-policy: hook-succeeded
 func (file *manifestFile) sort(result *result) error {
-	for _, m := range file.entries {
+	// Go through manifests in order found in file (function `SplitManifests` creates integer-sortable keys)
+	var sortedEntryKeys []string
+	for entryKey := range file.entries {
+		sortedEntryKeys = append(sortedEntryKeys, entryKey)
+	}
+	sort.Sort(BySplitManifestsOrder(sortedEntryKeys))
+
+	for _, entryKey := range sortedEntryKeys {
+		m := file.entries[entryKey]
+
 		var entry SimpleHead
 		if err := yaml.Unmarshal([]byte(m), &entry); err != nil {
 			return errors.Wrapf(err, "YAML parse error on %s", file.path)
diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go
index 58a7eb06f..6528c48ba 100644
--- a/pkg/storage/storage.go
+++ b/pkg/storage/storage.go
@@ -66,7 +66,7 @@ func (s *Storage) Create(rls *rspb.Release) error {
 	return s.Driver.Create(makeKey(rls.Name, rls.Version), rls)
 }
 
-// Update update the release in storage. An error is returned if the
+// Update updates the release in storage. An error is returned if the
 // storage backend fails to update the release or if the release
 // does not exist.
 func (s *Storage) Update(rls *rspb.Release) error {


Test patch:
diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go
index 924e8e9d3..a08720e7a 100644
--- a/cmd/helm/helm_test.go
+++ b/cmd/helm/helm_test.go
@@ -47,24 +47,26 @@ func init() {
 func runTestCmd(t *testing.T, tests []cmdTestCase) {
 	t.Helper()
 	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			defer resetEnv()()
-
-			storage := storageFixture()
-			for _, rel := range tt.rels {
-				if err := storage.Create(rel); err != nil {
-					t.Fatal(err)
+		for i := 0; i <= tt.repeat; i++ {
+			t.Run(tt.name, func(t *testing.T) {
+				defer resetEnv()()
+
+				storage := storageFixture()
+				for _, rel := range tt.rels {
+					if err := storage.Create(rel); err != nil {
+						t.Fatal(err)
+					}
 				}
-			}
-			t.Log("running cmd: ", tt.cmd)
-			_, out, err := executeActionCommandC(storage, tt.cmd)
-			if (err != nil) != tt.wantError {
-				t.Errorf("expected error, got '%v'", err)
-			}
-			if tt.golden != "" {
-				test.AssertGoldenString(t, out, tt.golden)
-			}
-		})
+				t.Logf("running cmd (attempt %d): %s", i+1, tt.cmd)
+				_, out, err := executeActionCommandC(storage, tt.cmd)
+				if (err != nil) != tt.wantError {
+					t.Errorf("expected error, got '%v'", err)
+				}
+				if tt.golden != "" {
+					test.AssertGoldenString(t, out, tt.golden)
+				}
+			})
+		}
 	}
 }
 
@@ -124,6 +126,9 @@ type cmdTestCase struct {
 	wantError bool
 	// Rels are the available releases at the start of the test.
 	rels []*release.Release
+	// Number of repeats (in case a feature was previously flaky and the test checks
+	// it's now stably producing identical results). 0 means test is run exactly once.
+	repeat int
 }
 
 func executeActionCommand(cmd string) (*cobra.Command, string, error) {
diff --git a/cmd/helm/repo_add_test.go b/cmd/helm/repo_add_test.go
index 1ac61fba8..4dbd0f435 100644
--- a/cmd/helm/repo_add_test.go
+++ b/cmd/helm/repo_add_test.go
@@ -23,7 +23,7 @@ import (
 	"sync"
 	"testing"
 
-	"gopkg.in/yaml.v2"
+	"sigs.k8s.io/yaml"
 
 	"helm.sh/helm/v3/internal/test/ensure"
 	"helm.sh/helm/v3/pkg/repo"
diff --git a/cmd/helm/template_test.go b/cmd/helm/template_test.go
index 735829432..35a8e996b 100644
--- a/cmd/helm/template_test.go
+++ b/cmd/helm/template_test.go
@@ -84,6 +84,14 @@ func TestTemplateCmd(t *testing.T) {
 			cmd:    fmt.Sprintf("template '%s' --include-crds", chartPath),
 			golden: "output/template-with-crds.txt",
 		},
+		{
+			name:   "sorted output of manifests (order of filenames, then order of objects within each YAML file)",
+			cmd:    fmt.Sprintf("template '%s'", "testdata/testcharts/object-order"),
+			golden: "output/object-order.txt",
+			// Helm previously used random file order. Repeat the test so we
+			// don't accidentally get the expected result.
+			repeat: 10,
+		},
 	}
 	runTestCmd(t, tests)
 }
diff --git a/cmd/helm/testdata/output/object-order.txt b/cmd/helm/testdata/output/object-order.txt
new file mode 100644
index 000000000..307f928f2
--- /dev/null
+++ b/cmd/helm/testdata/output/object-order.txt
@@ -0,0 +1,191 @@
+---
+# Source: object-order/templates/01-a.yml
+# 1
+kind: NetworkPolicy
+apiVersion: networking.k8s.io/v1
+metadata:
+  name: first
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+---
+# Source: object-order/templates/01-a.yml
+# 2
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: second
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+---
+# Source: object-order/templates/01-a.yml
+# 3
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: third
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+---
+# Source: object-order/templates/02-b.yml
+# 5
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: fifth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+---
+# Source: object-order/templates/02-b.yml
+# 7
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: seventh
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+---
+# Source: object-order/templates/02-b.yml
+# 8
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: eighth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+---
+# Source: object-order/templates/02-b.yml
+# 9
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: ninth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+---
+# Source: object-order/templates/02-b.yml
+# 10
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: tenth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+---
+# Source: object-order/templates/02-b.yml
+# 11
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: eleventh
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+---
+# Source: object-order/templates/02-b.yml
+# 12
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: twelfth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+---
+# Source: object-order/templates/02-b.yml
+# 13
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: thirteenth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+---
+# Source: object-order/templates/02-b.yml
+# 14
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: fourteenth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+---
+# Source: object-order/templates/02-b.yml
+# 15 (11th object within 02-b.yml, in order to test `SplitManifests` which assigns `manifest-10`
+# to this object which should then come *after* `manifest-9`)
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: fifteenth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+---
+# Source: object-order/templates/01-a.yml
+# 4 (Deployment should come after all NetworkPolicy manifests, since 'helm template' outputs in install order)
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: fourth
+spec:
+  selector:
+    matchLabels:
+      pod: fourth
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        pod: fourth
+    spec:
+      containers:
+        - name: hello-world
+          image: gcr.io/google-samples/node-hello:1.0
+---
+# Source: object-order/templates/02-b.yml
+# 6 (implementation detail: currently, 'helm template' outputs hook manifests last; and yes, NetworkPolicy won't make a reasonable hook, this is just a dummy unit test manifest)
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  annotations:
+    "helm.sh/hook": pre-install
+  name: sixth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
diff --git a/cmd/helm/testdata/testcharts/object-order/Chart.yaml b/cmd/helm/testdata/testcharts/object-order/Chart.yaml
new file mode 100644
index 000000000..d2eb42fd7
--- /dev/null
+++ b/cmd/helm/testdata/testcharts/object-order/Chart.yaml
@@ -0,0 +1,5 @@
+apiVersion: v2
+name: object-order
+description: Test ordering of manifests in output
+type: application
+version: 0.1.0
diff --git a/cmd/helm/testdata/testcharts/object-order/templates/01-a.yml b/cmd/helm/testdata/testcharts/object-order/templates/01-a.yml
new file mode 100644
index 000000000..32aa4a475
--- /dev/null
+++ b/cmd/helm/testdata/testcharts/object-order/templates/01-a.yml
@@ -0,0 +1,57 @@
+# 1
+kind: NetworkPolicy
+apiVersion: networking.k8s.io/v1
+metadata:
+  name: first
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+
+---
+
+# 2
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: second
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+
+---
+
+# 3
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: third
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+
+---
+
+# 4 (Deployment should come after all NetworkPolicy manifests, since 'helm template' outputs in install order)
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: fourth
+spec:
+  selector:
+    matchLabels:
+      pod: fourth
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        pod: fourth
+    spec:
+      containers:
+        - name: hello-world
+          image: gcr.io/google-samples/node-hello:1.0
diff --git a/cmd/helm/testdata/testcharts/object-order/templates/02-b.yml b/cmd/helm/testdata/testcharts/object-order/templates/02-b.yml
new file mode 100644
index 000000000..895db8cf7
--- /dev/null
+++ b/cmd/helm/testdata/testcharts/object-order/templates/02-b.yml
@@ -0,0 +1,143 @@
+# 5
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: fifth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+
+---
+
+# 6 (implementation detail: currently, 'helm template' outputs hook manifests last; and yes, NetworkPolicy won't make a reasonable hook, this is just a dummy unit test manifest)
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  annotations:
+    "helm.sh/hook": pre-install
+  name: sixth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+
+---
+
+# 7
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: seventh
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+
+---
+
+# 8
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: eighth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+
+---
+
+# 9
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: ninth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+
+---
+
+# 10
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: tenth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+
+---
+
+# 11
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: eleventh
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+
+---
+
+# 12
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: twelfth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+
+---
+
+# 13
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: thirteenth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+
+---
+
+# 14
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: fourteenth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
+
+---
+
+# 15 (11th object within 02-b.yml, in order to test `SplitManifests` which assigns `manifest-10`
+# to this object which should then come *after* `manifest-9`)
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+  name: fifteenth
+spec:
+  podSelector: {}
+  policyTypes:
+    - Egress
+    - Ingress
diff --git a/internal/third_party/dep/fs/fs_test.go b/internal/third_party/dep/fs/fs_test.go
index bf4b803f8..a9678d8c1 100644
--- a/internal/third_party/dep/fs/fs_test.go
+++ b/internal/third_party/dep/fs/fs_test.go
@@ -170,7 +170,7 @@ func TestCopyDir(t *testing.T) {
 func TestCopyDirFail_SrcInaccessible(t *testing.T) {
 	if runtime.GOOS == "windows" {
 		// XXX: setting permissions works differently in
-		// Microsoft Windows. Skipping this this until a
+		// Microsoft Windows. Skipping this until a
 		// compatible implementation is provided.
 		t.Skip("skipping on windows")
 	}
@@ -209,7 +209,7 @@ func TestCopyDirFail_SrcInaccessible(t *testing.T) {
 func TestCopyDirFail_DstInaccessible(t *testing.T) {
 	if runtime.GOOS == "windows" {
 		// XXX: setting permissions works differently in
-		// Microsoft Windows. Skipping this this until a
+		// Microsoft Windows. Skipping this until a
 		// compatible implementation is provided.
 		t.Skip("skipping on windows")
 	}
@@ -309,7 +309,7 @@ func TestCopyDirFailOpen(t *testing.T) {
 		// 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
+		// this until a compatible implementation is
 		// provided.
 		t.Skip("skipping on windows")
 	}
@@ -478,7 +478,7 @@ func TestCopyFileSymlink(t *testing.T) {
 func TestCopyFileFail(t *testing.T) {
 	if runtime.GOOS == "windows" {
 		// XXX: setting permissions works differently in
-		// Microsoft Windows. Skipping this this until a
+		// Microsoft Windows. Skipping this until a
 		// compatible implementation is provided.
 		t.Skip("skipping on windows")
 	}
@@ -521,7 +521,7 @@ func TestCopyFileFail(t *testing.T) {
 }
 
 // setupInaccessibleDir creates a temporary location with a single
-// directory in it, in such a way that that directory is not accessible
+// directory in it, in such a way that directory is not accessible
 // after this function returns.
 //
 // op is called with the directory as argument, so that it can create
diff --git a/pkg/releaseutil/kind_sorter_test.go b/pkg/releaseutil/kind_sorter_test.go
index 93d8ae782..f29065f73 100644
--- a/pkg/releaseutil/kind_sorter_test.go
+++ b/pkg/releaseutil/kind_sorter_test.go
@@ -40,7 +40,7 @@ func TestKindSorter(t *testing.T) {
 			Head: &SimpleHead{Kind: "ClusterRoleBindingList"},
 		},
 		{
-			Name: "e",
+			Name: "f",
 			Head: &SimpleHead{Kind: "ConfigMap"},
 		},
 		{
@@ -84,11 +84,11 @@ func TestKindSorter(t *testing.T) {
 			Head: &SimpleHead{Kind: "NetworkPolicy"},
 		},
 		{
-			Name: "f",
+			Name: "g",
 			Head: &SimpleHead{Kind: "PersistentVolume"},
 		},
 		{
-			Name: "g",
+			Name: "h",
 			Head: &SimpleHead{Kind: "PersistentVolumeClaim"},
 		},
 		{
@@ -132,7 +132,7 @@ func TestKindSorter(t *testing.T) {
 			Head: &SimpleHead{Kind: "RoleBindingList"},
 		},
 		{
-			Name: "d",
+			Name: "e",
 			Head: &SimpleHead{Kind: "Secret"},
 		},
 		{
@@ -140,7 +140,7 @@ func TestKindSorter(t *testing.T) {
 			Head: &SimpleHead{Kind: "Service"},
 		},
 		{
-			Name: "h",
+			Name: "d",
 			Head: &SimpleHead{Kind: "ServiceAccount"},
 		},
 		{
@@ -166,8 +166,8 @@ func TestKindSorter(t *testing.T) {
 		order       KindSortOrder
 		expected    string
 	}{
-		{"install", InstallOrder, "aAbcC3de1fgh2iIjJkKlLmnopqrxstuvw!"},
-		{"uninstall", UninstallOrder, "wvmutsxrqponLlKkJjIi2hgf1ed3CcbAa!"},
+		{"install", InstallOrder, "aAbcC3def1gh2iIjJkKlLmnopqrxstuvw!"},
+		{"uninstall", UninstallOrder, "wvmutsxrqponLlKkJjIi2hg1fed3CcbAa!"},
 	} {
 		var buf bytes.Buffer
 		t.Run(test.description, func(t *testing.T) {
@@ -185,8 +185,8 @@ func TestKindSorter(t *testing.T) {
 	}
 }
 
-// TestKindSorterSubSort verifies manifests of same kind are also sorted alphanumeric
-func TestKindSorterSubSort(t *testing.T) {
+// TestKindSorterKeepOriginalOrder verifies manifests of same kind are kept in original order
+func TestKindSorterKeepOriginalOrder(t *testing.T) {
 	manifests := []Manifest{
 		{
 			Name: "a",
@@ -230,8 +230,8 @@ func TestKindSorterSubSort(t *testing.T) {
 		order       KindSortOrder
 		expected    string
 	}{
-		// expectation is sorted by kind (unknown is last) and then sub sorted alphabetically within each group
-		{"cm,clusterRole,clusterRoleBinding,Unknown,Unknown2", InstallOrder, "01Aa!zu1u2t3"},
+		// expectation is sorted by kind (unknown is last) and within each group of same kind, the order is kept
+		{"cm,clusterRole,clusterRoleBinding,Unknown,Unknown2", InstallOrder, "01aAz!u2u1t3"},
 	} {
 		var buf bytes.Buffer
 		t.Run(test.description, func(t *testing.T) {