Repository: helm/helm
Pull request #7602 (2020-02-15 03:31:49)

feat(comp): Move kube-context completion to Go
This moves that last custom completion done in Bash to Go.
This actually removes the last dependency to `kubectl`.  

I hadn't done that before because the call to `action.Init()` would fail before doing the completion, when the `--kube-context` value was partial (because it was being completed).  But I found a way around that by looking for that specific scenario, as implemented in this PR.


Patch:
diff --git a/ADOPTERS.md b/ADOPTERS.md
index 5dccb8fc6..a72f51e09 100644
--- a/ADOPTERS.md
+++ b/ADOPTERS.md
@@ -1,5 +1,7 @@
- To add your organization to this list, simply add your organization's name,
- optionally with a link. The list is in alphabetical order.
+ To add your organization to this list, open a pull request that adds your
+ organization's name, optionally with a link. The list is in alphabetical order.
+
+ (Remember to use `git commit --signoff` to comply with the DCO)
 
 # Organizations Using Helm
 
@@ -8,5 +10,6 @@
 - [IBM](https://www.ibm.com)
 - [Qovery](https://www.qovery.com/)
 - [Samsung SDS](https://www.samsungsds.com/)
-[Ville de Montreal](https://montreal.ca)
+- [Ville de Montreal](https://montreal.ca)
+
 _This file is part of the CNCF official documentation for projects._
diff --git a/Makefile b/Makefile
index eea3b1e9c..27404bca8 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 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
+TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum
 BINNAME     ?= helm
 
 GOPATH        = $(shell go env GOPATH)
@@ -24,7 +24,7 @@ GOFLAGS    :=
 SRC        := $(shell find . -type f -name '*.go' -print)
 
 # Required for globs to work correctly
-SHELL      = /bin/bash
+SHELL      = /usr/bin/env bash
 
 GIT_COMMIT = $(shell git rev-parse HEAD)
 GIT_SHA    = $(shell git rev-parse --short HEAD)
diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go
index 112d5123f..0ce40732b 100644
--- a/cmd/helm/helm.go
+++ b/cmd/helm/helm.go
@@ -30,6 +30,7 @@ import (
 	// Import to initialize client auth plugins.
 	_ "k8s.io/client-go/plugin/pkg/client/auth"
 
+	"helm.sh/helm/v3/internal/completion"
 	"helm.sh/helm/v3/pkg/action"
 	"helm.sh/helm/v3/pkg/cli"
 	"helm.sh/helm/v3/pkg/gates"
@@ -67,6 +68,16 @@ func main() {
 	actionConfig := new(action.Configuration)
 	cmd := newRootCmd(actionConfig, os.Stdout, os.Args[1:])
 
+	if calledCmd, _, err := cmd.Find(os.Args[1:]); err == nil && calledCmd.Name() == completion.CompRequestCmd {
+		// If completion is being called, we have to check if the completion is for the "--kube-context"
+		// value; if it is, we cannot call the action.Init() method with an incomplete kube-context value
+		// or else it will fail immediately.  So, we simply unset the invalid kube-context value.
+		if args := os.Args[1:]; len(args) > 2 && args[len(args)-2] == "--kube-context" {
+			// We are completing the kube-context value!  Reset it as the current value is not valid.
+			settings.KubeContext = ""
+		}
+	}
+
 	if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), os.Getenv("HELM_DRIVER"), debug); err != nil {
 		log.Fatal(err)
 	}
diff --git a/cmd/helm/root.go b/cmd/helm/root.go
index 6ce1dcbf4..3c6a0d18e 100644
--- a/cmd/helm/root.go
+++ b/cmd/helm/root.go
@@ -24,6 +24,7 @@ import (
 	"github.com/spf13/cobra"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/tools/clientcmd"
 
 	"helm.sh/helm/v3/cmd/helm/require"
 	"helm.sh/helm/v3/internal/completion"
@@ -31,30 +32,6 @@ import (
 	"helm.sh/helm/v3/pkg/action"
 )
 
-const (
-	contextCompFunc = `
-__helm_get_contexts()
-{
-    __helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
-    local template out
-    template="{{ range .contexts  }}{{ .name }} {{ end }}"
-    if out=$(kubectl config -o template --template="${template}" view 2>/dev/null); then
-        COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
-    fi
-}
-`
-)
-
-var (
-	// Mapping of global flags that can have dynamic completion and the
-	// completion function to be used.
-	bashCompletionFlags = map[string]string{
-		// Cannot convert the kube-context flag to Go completion yet because
-		// an incomplete kube-context will make actionConfig.Init() fail at the very start
-		"kube-context": "__helm_get_contexts",
-	}
-)
-
 var globalUsage = `The Kubernetes package manager
 
 Common actions for Helm:
@@ -101,14 +78,14 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
 		Long:                   globalUsage,
 		SilenceUsage:           true,
 		Args:                   require.NoArgs,
-		BashCompletionFunction: fmt.Sprintf("%s%s", contextCompFunc, completion.GetBashCustomFunction()),
+		BashCompletionFunction: completion.GetBashCustomFunction(),
 	}
 	flags := cmd.PersistentFlags()
 
 	settings.AddFlags(flags)
 
-	flag := flags.Lookup("namespace")
 	// Setup shell completion for the namespace flag
+	flag := flags.Lookup("namespace")
 	completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
 		if client, err := actionConfig.KubernetesClientSet(); err == nil {
 			// Choose a long enough timeout that the user notices somethings is not working
@@ -129,6 +106,29 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
 		return nil, completion.BashCompDirectiveDefault
 	})
 
+	// Setup shell completion for the kube-context flag
+	flag = flags.Lookup("kube-context")
+	completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
+		completion.CompDebugln("About to get the different kube-contexts")
+
+		loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
+		if len(settings.KubeConfig) > 0 {
+			loadingRules = &clientcmd.ClientConfigLoadingRules{ExplicitPath: settings.KubeConfig}
+		}
+		if config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
+			loadingRules,
+			&clientcmd.ConfigOverrides{}).RawConfig(); err == nil {
+			ctxs := []string{}
+			for name := range config.Contexts {
+				if strings.HasPrefix(name, toComplete) {
+					ctxs = append(ctxs, name)
+				}
+			}
+			return ctxs, completion.BashCompDirectiveNoFileComp
+		}
+		return nil, completion.BashCompDirectiveNoFileComp
+	})
+
 	// We can safely ignore any errors that flags.Parse encounters since
 	// those errors will be caught later during the call to cmd.Execution.
 	// This call is required to gather configuration information prior to
@@ -173,19 +173,6 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
 		completion.NewCompleteCmd(settings, out),
 	)
 
-	// Add annotation to flags for which we can generate completion choices
-	for name, completion := range bashCompletionFlags {
-		if cmd.Flag(name) != nil {
-			if cmd.Flag(name).Annotations == nil {
-				cmd.Flag(name).Annotations = map[string][]string{}
-			}
-			cmd.Flag(name).Annotations[cobra.BashCompCustom] = append(
-				cmd.Flag(name).Annotations[cobra.BashCompCustom],
-				completion,
-			)
-		}
-	}
-
 	// Add *experimental* subcommands
 	registryClient, err := registry.NewClient(
 		registry.ClientOptDebug(settings.Debug),
diff --git a/cmd/helm/search_repo.go b/cmd/helm/search_repo.go
index 9f5af1e3c..8a8ac379d 100644
--- a/cmd/helm/search_repo.go
+++ b/cmd/helm/search_repo.go
@@ -22,6 +22,7 @@ import (
 	"fmt"
 	"io"
 	"io/ioutil"
+	"os"
 	"path/filepath"
 	"strings"
 
@@ -102,7 +103,7 @@ func newSearchRepoCmd(out io.Writer) *cobra.Command {
 func (o *searchRepoOptions) run(out io.Writer, args []string) error {
 	o.setupSearchedVersion()
 
-	index, err := o.buildIndex(out)
+	index, err := o.buildIndex()
 	if err != nil {
 		return err
 	}
@@ -171,7 +172,7 @@ func (o *searchRepoOptions) applyConstraint(res []*search.Result) ([]*search.Res
 	return data, nil
 }
 
-func (o *searchRepoOptions) buildIndex(out io.Writer) (*search.Index, error) {
+func (o *searchRepoOptions) buildIndex() (*search.Index, error) {
 	// Load the repositories.yaml
 	rf, err := repo.LoadFile(o.repoFile)
 	if isNotExist(err) || len(rf.Repositories) == 0 {
@@ -184,8 +185,7 @@ func (o *searchRepoOptions) buildIndex(out io.Writer) (*search.Index, error) {
 		f := filepath.Join(o.repoCacheDir, helmpath.CacheIndexFile(n))
 		ind, err := repo.LoadIndexFile(f)
 		if err != nil {
-			// TODO should print to stderr
-			fmt.Fprintf(out, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update'.", n)
+			fmt.Fprintf(os.Stderr, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update'.", n)
 			continue
 		}
 
diff --git a/go.mod b/go.mod
index 696c2b6d2..9c22ea9e5 100644
--- a/go.mod
+++ b/go.mod
@@ -29,13 +29,13 @@ require (
 	github.com/stretchr/testify v1.4.0
 	github.com/xeipuuv/gojsonschema v1.1.0
 	golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d
-	k8s.io/api v0.17.2
-	k8s.io/apiextensions-apiserver v0.17.2
-	k8s.io/apimachinery v0.17.2
-	k8s.io/cli-runtime v0.17.2
-	k8s.io/client-go v0.17.2
+	k8s.io/api v0.17.3
+	k8s.io/apiextensions-apiserver v0.17.3
+	k8s.io/apimachinery v0.17.3
+	k8s.io/cli-runtime v0.17.3
+	k8s.io/client-go v0.17.3
 	k8s.io/klog v1.0.0
-	k8s.io/kubectl v0.17.2
+	k8s.io/kubectl v0.17.3
 	sigs.k8s.io/yaml v1.1.0
 )
 
diff --git a/go.sum b/go.sum
index cc6b8dd9c..89c7762d5 100644
--- a/go.sum
+++ b/go.sum
@@ -630,25 +630,27 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
 gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
 gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc=
-k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4=
-k8s.io/apiextensions-apiserver v0.17.2 h1:cP579D2hSZNuO/rZj9XFRzwJNYb41DbNANJb6Kolpss=
-k8s.io/apiextensions-apiserver v0.17.2/go.mod h1:4KdMpjkEjjDI2pPfBA15OscyNldHWdBCfsWMDWAmSTs=
-k8s.io/apimachinery v0.17.2 h1:hwDQQFbdRlpnnsR64Asdi55GyCaIP/3WQpMmbNBeWr4=
-k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg=
-k8s.io/apiserver v0.17.2/go.mod h1:lBmw/TtQdtxvrTk0e2cgtOxHizXI+d0mmGQURIHQZlo=
-k8s.io/cli-runtime v0.17.2 h1:YH4txSplyGudvxjhAJeHEtXc7Tr/16clKGfN076ydGk=
-k8s.io/cli-runtime v0.17.2/go.mod h1:aa8t9ziyQdbkuizkNLAw3qe3srSyWh9zlSB7zTqRNPI=
-k8s.io/client-go v0.17.2 h1:ndIfkfXEGrNhLIgkr0+qhRguSD3u6DCmonepn1O6NYc=
-k8s.io/client-go v0.17.2/go.mod h1:QAzRgsa0C2xl4/eVpeVAZMvikCn8Nm81yqVx3Kk9XYI=
-k8s.io/code-generator v0.17.2/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s=
-k8s.io/component-base v0.17.2 h1:0XHf+cerTvL9I5Xwn9v+0jmqzGAZI7zNydv4tL6Cw6A=
-k8s.io/component-base v0.17.2/go.mod h1:zMPW3g5aH7cHJpKYQ/ZsGMcgbsA/VyhEugF3QT1awLs=
+k8s.io/api v0.17.3 h1:XAm3PZp3wnEdzekNkcmj/9Y1zdmQYJ1I4GKSBBZ8aG0=
+k8s.io/api v0.17.3/go.mod h1:YZ0OTkuw7ipbe305fMpIdf3GLXZKRigjtZaV5gzC2J0=
+k8s.io/apiextensions-apiserver v0.17.3 h1:WDZWkPcbgvchEdDd7ysL21GGPx3UKZQLDZXEkevT6n4=
+k8s.io/apiextensions-apiserver v0.17.3/go.mod h1:CJbCyMfkKftAd/X/V6OTHYhVn7zXnDdnkUjS1h0GTeY=
+k8s.io/apimachinery v0.17.3 h1:f+uZV6rm4/tHE7xXgLyToprg6xWairaClGVkm2t8omg=
+k8s.io/apimachinery v0.17.3/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g=
+k8s.io/apiserver v0.17.3/go.mod h1:iJtsPpu1ZpEnHaNawpSV0nYTGBhhX2dUlnn7/QS7QiY=
+k8s.io/cli-runtime v0.17.3 h1:0ZlDdJgJBKsu77trRUynNiWsRuAvAVPBNaQfnt/1qtc=
+k8s.io/cli-runtime v0.17.3/go.mod h1:X7idckYphH4SZflgNpOOViSxetiMj6xI0viMAjM81TA=
+k8s.io/client-go v0.17.3 h1:deUna1Ksx05XeESH6XGCyONNFfiQmDdqeqUvicvP6nU=
+k8s.io/client-go v0.17.3/go.mod h1:cLXlTMtWHkuK4tD360KpWz2gG2KtdWEr/OT02i3emRQ=
+k8s.io/code-generator v0.17.3/go.mod h1:l8BLVwASXQZTo2xamW5mQNFCe1XPiAesVq7Y1t7PiQQ=
+k8s.io/component-base v0.17.3 h1:hQzTSshY14aLSR6WGIYvmw+w+u6V4d+iDR2iDGMrlUg=
+k8s.io/component-base v0.17.3/go.mod h1:GeQf4BrgelWm64PXkIXiPh/XS0hnO42d9gx9BtbZRp8=
 k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
@@ -657,10 +659,10 @@ k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8=
 k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
 k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU=
 k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
-k8s.io/kubectl v0.17.2 h1:QZR8Q6lWiVRjwKslekdbN5WPMp53dS/17j5e+oi5XVU=
-k8s.io/kubectl v0.17.2/go.mod h1:y4rfLV0n6aPmvbRCqZQjvOp3ezxsFgpqL+zF5jH/lxk=
+k8s.io/kubectl v0.17.3 h1:9HHYj07kuFkM+sMJMOyQX29CKWq4lvKAG1UIPxNPMQ4=
+k8s.io/kubectl v0.17.3/go.mod h1:NUn4IBY7f7yCMwSop2HCXlw/MVYP4HJBiUmOR3n9w28=
 k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
-k8s.io/metrics v0.17.2/go.mod h1:3TkNHET4ROd+NfzNxkjoVfQ0Ob4iZnaHmSEA4vYpwLw=
+k8s.io/metrics v0.17.3/go.mod h1:HEJGy1fhHOjHggW9rMDBJBD3YuGroH3Y1pnIRw9FFaI=
 k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo=
 k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
 modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=
diff --git a/internal/completion/complete.go b/internal/completion/complete.go
index a24390fc0..aa0d134b9 100644
--- a/internal/completion/complete.go
+++ b/internal/completion/complete.go
@@ -35,9 +35,9 @@ import (
 // This should ultimately be pushed down into Cobra.
 // ==================================================================================
 
-// compRequestCmd Hidden command to request completion results from the program.
+// CompRequestCmd Hidden command to request completion results from the program.
 // Used by the shell completion script.
-const compRequestCmd = "__complete"
+const CompRequestCmd = "__complete"
 
 // Global map allowing to find completion functions for commands or flags.
 var validArgsFunctions = map[interface{}]func(cmd *cobra.Command, args []string, toComplete string) ([]string, BashCompDirective){}
@@ -123,7 +123,7 @@ __helm_custom_func()
         done < <(compgen -W "${out[*]}" -- "$cur")
     fi
 }
-`, compRequestCmd, BashCompDirectiveError, BashCompDirectiveNoSpace, BashCompDirectiveNoFileComp)
+`, CompRequestCmd, BashCompDirectiveError, BashCompDirectiveNoSpace, BashCompDirectiveNoFileComp)
 }
 
 // RegisterValidArgsFunc should be called to register a function to provide argument completion for a command
@@ -177,14 +177,14 @@ func (d BashCompDirective) string() string {
 func NewCompleteCmd(settings *cli.EnvSettings, out io.Writer) *cobra.Command {
 	debug = settings.Debug
 	return &cobra.Command{
-		Use:                   fmt.Sprintf("%s [command-line]", compRequestCmd),
+		Use:                   fmt.Sprintf("%s [command-line]", CompRequestCmd),
 		DisableFlagsInUseLine: true,
 		Hidden:                true,
 		DisableFlagParsing:    true,
 		Args:                  require.MinimumNArgs(1),
 		Short:                 "Request shell completion choices for the specified command-line",
 		Long: fmt.Sprintf("%s is a special command that is used by the shell completion logic\n%s",
-			compRequestCmd, "to request completion choices for the specified command-line."),
+			CompRequestCmd, "to request completion choices for the specified command-line."),
 		Run: func(cmd *cobra.Command, args []string) {
 			CompDebugln(fmt.Sprintf("%s was called with args %v", cmd.Name(), args))
 
diff --git a/pkg/action/action.go b/pkg/action/action.go
index a97533696..1af5b3d9a 100644
--- a/pkg/action/action.go
+++ b/pkg/action/action.go
@@ -135,6 +135,7 @@ func (c *Configuration) getCapabilities() (*chartutil.Capabilities, error) {
 	return c.Capabilities, nil
 }
 
+// KubernetesClientSet creates a new kubernetes ClientSet based on the configuration
 func (c *Configuration) KubernetesClientSet() (kubernetes.Interface, error) {
 	conf, err := c.RESTClientGetter.ToRESTConfig()
 	if err != nil {
@@ -219,7 +220,7 @@ func (c *Configuration) recordRelease(r *release.Release) {
 	}
 }
 
-// InitActionConfig initializes the action configuration
+// Init initializes the action configuration
 func (c *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace string, helmDriver string, log DebugLog) error {
 	kc := kube.New(getter)
 	kc.Log = log
diff --git a/pkg/action/lint.go b/pkg/action/lint.go
index ddb0101c7..2292c14bf 100644
--- a/pkg/action/lint.go
+++ b/pkg/action/lint.go
@@ -38,6 +38,7 @@ type Lint struct {
 	WithSubcharts bool
 }
 
+// LintResult is the result of Lint
 type LintResult struct {
 	TotalChartsLinted int
 	Messages          []support.Message
diff --git a/pkg/action/list.go b/pkg/action/list.go
index 5be60ac42..ac6fd1b75 100644
--- a/pkg/action/list.go
+++ b/pkg/action/list.go
@@ -165,6 +165,10 @@ func (l *List) Run() ([]*release.Release, error) {
 		return true
 	})
 
+	if err != nil {
+		return nil, err
+	}
+
 	if results == nil {
 		return results, nil
 	}
@@ -238,7 +242,7 @@ func filterList(releases []*release.Release) []*release.Release {
 	return list
 }
 
-// setStateMask calculates the state mask based on parameters.
+// SetStateMask calculates the state mask based on parameters.
 func (l *List) SetStateMask() {
 	if l.All {
 		l.StateMask = ListAll
diff --git a/pkg/action/package.go b/pkg/action/package.go
index b48fc65f0..19d845cf3 100644
--- a/pkg/action/package.go
+++ b/pkg/action/package.go
@@ -112,6 +112,7 @@ func setVersion(ch *chart.Chart, ver string) error {
 	return nil
 }
 
+// Clearsign signs a chart
 func (p *Package) Clearsign(filename string) error {
 	// Load keyring
 	signer, err := provenance.NewFromKeyring(p.Keyring, p.Key)
diff --git a/pkg/action/show.go b/pkg/action/show.go
index b29107d4e..14b59a5ea 100644
--- a/pkg/action/show.go
+++ b/pkg/action/show.go
@@ -27,12 +27,17 @@ import (
 	"helm.sh/helm/v3/pkg/chartutil"
 )
 
+// ShowOutputFormat is the format of the output of `helm show`
 type ShowOutputFormat string
 
 const (
-	ShowAll    ShowOutputFormat = "all"
-	ShowChart  ShowOutputFormat = "chart"
+	// ShowAll is the format which shows all the information of a chart
+	ShowAll ShowOutputFormat = "all"
+	// ShowChart is the format which only shows the chart's definition
+	ShowChart ShowOutputFormat = "chart"
+	// ShowValues is the format which only shows the chart's values
 	ShowValues ShowOutputFormat = "values"
+	// ShowReadme is the format which only shows the chart's README
 	ShowReadme ShowOutputFormat = "readme"
 )
 
diff --git a/pkg/kube/client.go b/pkg/kube/client.go
index 9243f4361..31dabcc5d 100644
--- a/pkg/kube/client.go
+++ b/pkg/kube/client.go
@@ -88,7 +88,7 @@ func (c *Client) IsReachable() error {
 	client, _ := c.Factory.KubernetesClientSet()
 	_, err := client.ServerVersion()
 	if err != nil {
-		return errors.New("Kubernetes cluster unreachable")
+		return fmt.Errorf("Kubernetes cluster unreachable: %s", err.Error())
 	}
 	return nil
 }
diff --git a/pkg/kube/converter.go b/pkg/kube/converter.go
index a5a6ae1f7..3bf0e358c 100644
--- a/pkg/kube/converter.go
+++ b/pkg/kube/converter.go
@@ -17,6 +17,10 @@ limitations under the License.
 package kube // import "helm.sh/helm/v3/pkg/kube"
 
 import (
+	"sync"
+
+	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
+	apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
 	"k8s.io/apimachinery/pkg/api/meta"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/schema"
@@ -24,6 +28,9 @@ import (
 	"k8s.io/client-go/kubernetes/scheme"
 )
 
+var k8sNativeScheme *runtime.Scheme
+var k8sNativeSchemeOnce sync.Once
+
 // AsVersioned converts the given info into a runtime.Object with the correct
 // group and version set
 func AsVersioned(info *resource.Info) runtime.Object {
@@ -33,12 +40,30 @@ func AsVersioned(info *resource.Info) runtime.Object {
 // convertWithMapper converts the given object with the optional provided
 // RESTMapping. If no mapping is provided, the default schema versioner is used
 func convertWithMapper(obj runtime.Object, mapping *meta.RESTMapping) runtime.Object {
-	var gv = runtime.GroupVersioner(schema.GroupVersions(scheme.Scheme.PrioritizedVersionsAllGroups()))
+	s := kubernetesNativeScheme()
+	var gv = runtime.GroupVersioner(schema.GroupVersions(s.PrioritizedVersionsAllGroups()))
 	if mapping != nil {
 		gv = mapping.GroupVersionKind.GroupVersion()
 	}
-	if obj, err := runtime.ObjectConvertor(scheme.Scheme).ConvertToVersion(obj, gv); err == nil {
+	if obj, err := runtime.ObjectConvertor(s).ConvertToVersion(obj, gv); err == nil {
 		return obj
 	}
 	return obj
 }
+
+// kubernetesNativeScheme returns a clean *runtime.Scheme with _only_ Kubernetes
+// native resources added to it. This is required to break free of custom resources
+// that may have been added to scheme.Scheme due to Helm being used as a package in
+// combination with e.g. a versioned kube client. If we would not do this, the client
+// may attempt to perform e.g. a 3-way-merge strategy patch for custom resources.
+func kubernetesNativeScheme() *runtime.Scheme {
+	k8sNativeSchemeOnce.Do(func() {
+		k8sNativeScheme = runtime.NewScheme()
+		scheme.AddToScheme(k8sNativeScheme)
+		// API extensions are not in the above scheme set,
+		// and must thus be added separately.
+		apiextensionsv1beta1.AddToScheme(k8sNativeScheme)
+		apiextensionsv1.AddToScheme(k8sNativeScheme)
+	})
+	return k8sNativeScheme
+}
diff --git a/pkg/kube/wait.go b/pkg/kube/wait.go
index f0005a61e..0254a60bb 100644
--- a/pkg/kube/wait.go
+++ b/pkg/kube/wait.go
@@ -188,12 +188,25 @@ func (w *waiter) serviceReady(s *corev1.Service) bool {
 	}
 
 	// Make sure the service is not explicitly set to "None" before checking the IP
-	if (s.Spec.ClusterIP != corev1.ClusterIPNone && s.Spec.ClusterIP == "") ||
-		// This checks if the service has a LoadBalancer and that balancer has an Ingress defined
-		(s.Spec.Type == corev1.ServiceTypeLoadBalancer && s.Status.LoadBalancer.Ingress == nil) {
-		w.log("Service does not have IP address: %s/%s", s.GetNamespace(), s.GetName())
+	if s.Spec.ClusterIP != corev1.ClusterIPNone && s.Spec.ClusterIP == "" {
+		w.log("Service does not have cluster IP address: %s/%s", s.GetNamespace(), s.GetName())
 		return false
 	}
+
+	// This checks if the service has a LoadBalancer and that balancer has an Ingress defined
+	if s.Spec.Type == corev1.ServiceTypeLoadBalancer {
+		// do not wait when at least 1 external IP is set
+		if len(s.Spec.ExternalIPs) > 0 {
+			w.log("Service %s/%s has external IP addresses (%v), marking as ready", s.GetNamespace(), s.GetName(), s.Spec.ExternalIPs)
+			return true
+		}
+
+		if s.Status.LoadBalancer.Ingress == nil {
+			w.log("Service does not have load balancer ingress IP address: %s/%s", s.GetNamespace(), s.GetName())
+			return false
+		}
+	}
+
 	return true
 }
 


Test patch:
diff --git a/pkg/gates/gates_test.go b/pkg/gates/gates_test.go
index a71d4905b..6bdd17ed6 100644
--- a/pkg/gates/gates_test.go
+++ b/pkg/gates/gates_test.go
@@ -45,3 +45,12 @@ func TestError(t *testing.T) {
 		t.Errorf("incorrect error message. Received %s", g.Error().Error())
 	}
 }
+
+func TestString(t *testing.T) {
+	os.Unsetenv(name)
+	g := Gate(name)
+
+	if g.String() != "HELM_EXPERIMENTAL_FEATURE" {
+		t.Errorf("incorrect string representation. Received %s", g.String())
+	}
+}
diff --git a/pkg/helmpath/home_windows_test.go b/pkg/helmpath/home_windows_test.go
index af74558a8..796ced62c 100644
--- a/pkg/helmpath/home_windows_test.go
+++ b/pkg/helmpath/home_windows_test.go
@@ -23,9 +23,9 @@ import (
 )
 
 func TestHelmHome(t *testing.T) {
-	os.Setenv(xdg.XDGCacheHomeEnvVar, "c:\\")
-	os.Setenv(xdg.XDGConfigHomeEnvVar, "d:\\")
-	os.Setenv(xdg.XDGDataHomeEnvVar, "e:\\")
+	os.Setenv(xdg.CacheHomeEnvVar, "c:\\")
+	os.Setenv(xdg.ConfigHomeEnvVar, "d:\\")
+	os.Setenv(xdg.DataHomeEnvVar, "e:\\")
 	isEq := func(t *testing.T, a, b string) {
 		if a != b {
 			t.Errorf("Expected %q, got %q", b, a)
diff --git a/pkg/helmpath/lazypath_windows_test.go b/pkg/helmpath/lazypath_windows_test.go
index d02a0a7f6..866e7b9d9 100644
--- a/pkg/helmpath/lazypath_windows_test.go
+++ b/pkg/helmpath/lazypath_windows_test.go
@@ -32,7 +32,7 @@ const (
 )
 
 func TestDataPath(t *testing.T) {
-	os.Unsetenv(DataHomeEnvVar)
+	os.Unsetenv(xdg.DataHomeEnvVar)
 	os.Setenv("APPDATA", filepath.Join(homedir.HomeDir(), "foo"))
 
 	expected := filepath.Join(homedir.HomeDir(), "foo", appName, testFile)
@@ -41,7 +41,7 @@ func TestDataPath(t *testing.T) {
 		t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile))
 	}
 
-	os.Setenv(DataHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg"))
+	os.Setenv(xdg.DataHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg"))
 
 	expected = filepath.Join(homedir.HomeDir(), "xdg", appName, testFile)
 
@@ -70,8 +70,8 @@ func TestConfigPath(t *testing.T) {
 }
 
 func TestCachePath(t *testing.T) {
-	os.Unsetenv(CacheHomeEnvVar)
-	os.Setenv("APPDATA", filepath.Join(homedir.HomeDir(), "foo"))
+	os.Unsetenv(xdg.CacheHomeEnvVar)
+	os.Setenv("TEMP", filepath.Join(homedir.HomeDir(), "foo"))
 
 	expected := filepath.Join(homedir.HomeDir(), "foo", appName, testFile)
 
@@ -79,7 +79,7 @@ func TestCachePath(t *testing.T) {
 		t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile))
 	}
 
-	os.Setenv(CacheHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg"))
+	os.Setenv(xdg.CacheHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg"))
 
 	expected = filepath.Join(homedir.HomeDir(), "xdg", appName, testFile)
 
diff --git a/pkg/strvals/parser_test.go b/pkg/strvals/parser_test.go
index 7f38efa52..44d317220 100644
--- a/pkg/strvals/parser_test.go
+++ b/pkg/strvals/parser_test.go
@@ -449,6 +449,39 @@ func TestParseIntoString(t *testing.T) {
 	}
 }
 
+func TestParseFile(t *testing.T) {
+	input := "name1=path1"
+	expect := map[string]interface{}{
+		"name1": "value1",
+	}
+	rs2v := func(rs []rune) (interface{}, error) {
+		v := string(rs)
+		if v != "path1" {
+			t.Errorf("%s: runesToVal: Expected value path1, got %s", input, v)
+			return "", nil
+		}
+		return "value1", nil
+	}
+
+	got, err := ParseFile(input, rs2v)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	y1, err := yaml.Marshal(expect)
+	if err != nil {
+		t.Fatal(err)
+	}
+	y2, err := yaml.Marshal(got)
+	if err != nil {
+		t.Fatalf("Error serializing parsed value: %s", err)
+	}
+
+	if string(y1) != string(y2) {
+		t.Errorf("%s: Expected:\n%s\nGot:\n%s", input, y1, y2)
+	}
+}
+
 func TestParseIntoFile(t *testing.T) {
 	got := map[string]interface{}{}
 	input := "name1=path1"