Repository: helm/helm (go)

Issue:
`helm repo add` is missing --password-stdin
**What**

You currently can't pipe the password into `helm repo add`:

```
$ echo password | helm repo add repo-name https://repo-url --username username
Password:
Error: inappropriate ioctl for device
```

I would expect the option `--password-stdin` to be available:

```
$ echo password | helm repo add repo-name https://repo-url --username username --password-stdin
"repo-name" has been added to your repositories
```

**Why**

There are two reasons I see for adding this:

* I personally would expect that it's possible to pipe the password into
  `helm repo add` but that's currently not the case. If I understand it
  correctly, you currently either need to pass the password via a cli
  parameter (`--password`) or use a expect/send mechanism. The
  cli parameter mechanism also isn't secure as it might expose the
  password via the process name.

* Subcommands like `helm registry login` already support
  `--password-stdin`. The cli interfaces should be consistent regarding
  which options they support.

Pull request:
feat(helm): add --password-stdin to `helm repo add`
**What**

Without this commit, you can't pipe the password into `helm repo add`:

```
$ echo password | helm repo add repo-name https://repo-url --username username
Password:
Error: inappropriate ioctl for device
```

This commit introduces `--password-stdin`:

```
$ echo password | helm repo add repo-name https://repo-url --username username --password-stdin
"repo-name" has been added to your repositories
```

Fixes #10032

**Why**

There are two reasons I see for adding this:

* I personally would expect that it's possible to pipe the password into
  `helm repo add` but that's currently not the case. If I understand it
  correctly, you currently either need to pass the password via a cli
  parameter (`--password`) or use a expect/send mechanism.

* Subcommands like `helm registry login` already support
  `--password-stdin`. The cli interfaces should be consistent regarding
  which options they support.

**Notes**

* I basically just copy-pasted code from `cmd/helm/registry_login.go`.

* I don't know if this is the best approach to enable piping into the command. Maybe it would be sufficient to replace `term.ReadPassword(fd)` with something else that also supports piping/ doesn't yield `inappropriate ioctl for device`.

**Special notes for your reviewer**:

**If applicable**:
- [ ] this PR contains documentation
- [ ] this PR contains unit tests
- [ ] this PR has been tested for backwards compatibility


Patch:
diff --git a/cmd/helm/completion.go b/cmd/helm/completion.go
index 310c915b8..880e77c57 100644
--- a/cmd/helm/completion.go
+++ b/cmd/helm/completion.go
@@ -72,16 +72,6 @@ To load completions for every new session, execute once:
 You will need to start a new shell for this setup to take effect.
 `
 
-const powershellCompDesc = `
-Generate the autocompletion script for powershell.
-
-To load completions in your current shell session:
-PS C:\> helm completion powershell | Out-String | Invoke-Expression
-
-To load completions for every new session, add the output of the above command
-to your powershell profile.
-`
-
 const (
 	noDescFlagName = "no-descriptions"
 	noDescFlagText = "disable completion descriptions"
@@ -98,16 +88,16 @@ func newCompletionCmd(out io.Writer) *cobra.Command {
 	}
 
 	bash := &cobra.Command{
-		Use:               "bash",
-		Short:             "generate autocompletion script for bash",
-		Long:              bashCompDesc,
-		Args:              require.NoArgs,
-		ValidArgsFunction: noCompletions,
+		Use:                   "bash",
+		Short:                 "generate autocompletion script for bash",
+		Long:                  bashCompDesc,
+		Args:                  require.NoArgs,
+		DisableFlagsInUseLine: true,
+		ValidArgsFunction:     noCompletions,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			return runCompletionBash(out, cmd)
 		},
 	}
-	bash.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
 
 	zsh := &cobra.Command{
 		Use:               "zsh",
@@ -133,25 +123,13 @@ func newCompletionCmd(out io.Writer) *cobra.Command {
 	}
 	fish.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
 
-	powershell := &cobra.Command{
-		Use:               "powershell",
-		Short:             "generate autocompletion script for powershell",
-		Long:              powershellCompDesc,
-		Args:              require.NoArgs,
-		ValidArgsFunction: noCompletions,
-		RunE: func(cmd *cobra.Command, args []string) error {
-			return runCompletionPowershell(out, cmd)
-		},
-	}
-	powershell.Flags().BoolVar(&disableCompDescriptions, noDescFlagName, false, noDescFlagText)
-
-	cmd.AddCommand(bash, zsh, fish, powershell)
+	cmd.AddCommand(bash, zsh, fish)
 
 	return cmd
 }
 
 func runCompletionBash(out io.Writer, cmd *cobra.Command) error {
-	err := cmd.Root().GenBashCompletionV2(out, !disableCompDescriptions)
+	err := cmd.Root().GenBashCompletion(out)
 
 	// In case the user renamed the helm binary (e.g., to be able to run
 	// both helm2 and helm3), we hook the new binary name to the completion function
@@ -202,13 +180,6 @@ func runCompletionFish(out io.Writer, cmd *cobra.Command) error {
 	return cmd.Root().GenFishCompletion(out, !disableCompDescriptions)
 }
 
-func runCompletionPowershell(out io.Writer, cmd *cobra.Command) error {
-	if disableCompDescriptions {
-		return cmd.Root().GenPowerShellCompletion(out)
-	}
-	return cmd.Root().GenPowerShellCompletionWithDesc(out)
-}
-
 // Function to disable file completion
 func noCompletions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 	return nil, cobra.ShellCompDirectiveNoFileComp
diff --git a/cmd/helm/repo_add.go b/cmd/helm/repo_add.go
index beafb31cf..8844174be 100644
--- a/cmd/helm/repo_add.go
+++ b/cmd/helm/repo_add.go
@@ -48,6 +48,7 @@ type repoAddOptions struct {
 	url                  string
 	username             string
 	password             string
+	passwordFromStdinOpt bool
 	passCredentialsAll   bool
 	forceUpdate          bool
 	allowDeprecatedRepos bool
@@ -85,6 +86,7 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
 	f := cmd.Flags()
 	f.StringVar(&o.username, "username", "", "chart repository username")
 	f.StringVar(&o.password, "password", "", "chart repository password")
+	f.BoolVarP(&o.passwordFromStdinOpt, "password-stdin", "", false, "read chart repository password from stdin")
 	f.BoolVar(&o.forceUpdate, "force-update", false, "replace (overwrite) the repo if it already exists")
 	f.BoolVar(&o.deprecatedNoUpdate, "no-update", false, "Ignored. Formerly, it would disabled forced updates. It is deprecated by force-update.")
 	f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
@@ -143,14 +145,24 @@ func (o *repoAddOptions) run(out io.Writer) error {
 	}
 
 	if o.username != "" && o.password == "" {
-		fd := int(os.Stdin.Fd())
-		fmt.Fprint(out, "Password: ")
-		password, err := term.ReadPassword(fd)
-		fmt.Fprintln(out)
-		if err != nil {
-			return err
+		if o.passwordFromStdinOpt {
+			passwordFromStdin, err := io.ReadAll(os.Stdin)
+			if err != nil {
+				return err
+			}
+			password := strings.TrimSuffix(string(passwordFromStdin), "\n")
+			password = strings.TrimSuffix(password, "\r")
+			o.password = password
+		} else {
+			fd := int(os.Stdin.Fd())
+			fmt.Fprint(out, "Password: ")
+			password, err := term.ReadPassword(fd)
+			fmt.Fprintln(out)
+			if err != nil {
+				return err
+			}
+			o.password = string(password)
 		}
-		o.password = string(password)
 	}
 
 	c := repo.Entry{
diff --git a/pkg/repo/index.go b/pkg/repo/index.go
index 1b65ac497..e86b17349 100644
--- a/pkg/repo/index.go
+++ b/pkg/repo/index.go
@@ -50,8 +50,6 @@ var (
 	ErrNoChartVersion = errors.New("no chart version found")
 	// ErrNoChartName indicates that a chart with the given name is not found.
 	ErrNoChartName = errors.New("no chart name found")
-	// ErrEmptyIndexYaml indicates that the content of index.yaml is empty.
-	ErrEmptyIndexYaml = errors.New("empty index.yaml file")
 )
 
 // ChartVersions is a list of versioned chart references.
@@ -328,11 +326,6 @@ func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
 // This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
 func loadIndex(data []byte, source string) (*IndexFile, error) {
 	i := &IndexFile{}
-
-	if len(data) == 0 {
-		return i, ErrEmptyIndexYaml
-	}
-
 	if err := yaml.UnmarshalStrict(data, i); err != nil {
 		return i, err
 	}


Test patch:
diff --git a/cmd/helm/repo_add_test.go b/cmd/helm/repo_add_test.go
index 739173ee7..c88479ea1 100644
--- a/cmd/helm/repo_add_test.go
+++ b/cmd/helm/repo_add_test.go
@@ -21,6 +21,7 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"strings"
 	"sync"
 	"testing"
 
@@ -204,3 +205,33 @@ func TestRepoAddFileCompletion(t *testing.T) {
 	checkFileCompletion(t, "repo add reponame", false)
 	checkFileCompletion(t, "repo add reponame https://example.com", false)
 }
+
+func TestRepoAddWithPasswordFromStdin(t *testing.T) {
+	srv := repotest.NewTempServerWithCleanupAndBasicAuth(t, "testdata/testserver/*.*")
+	defer srv.Stop()
+
+	defer resetEnv()()
+
+	in, err := os.Open("testdata/password")
+	if err != nil {
+		t.Errorf("unexpected error, got '%v'", err)
+	}
+
+	tmpdir := ensure.TempDir(t)
+	repoFile := filepath.Join(tmpdir, "repositories.yaml")
+
+	store := storageFixture()
+
+	const testName = "test-name"
+	const username = "username"
+	cmd := fmt.Sprintf("repo add %s %s --repository-config %s --repository-cache %s --username %s --password-stdin", testName, srv.URL(), repoFile, tmpdir, username)
+	var result string
+	_, result, err = executeActionCommandStdinC(store, in, cmd)
+	if err != nil {
+		t.Errorf("unexpected error, got '%v'", err)
+	}
+
+	if !strings.Contains(result, fmt.Sprintf("\"%s\" has been added to your repositories", testName)) {
+		t.Errorf("Repo was not successfully added. Output: %s", result)
+	}
+}
diff --git a/cmd/helm/testdata/password b/cmd/helm/testdata/password
new file mode 100644
index 000000000..f3097ab13
--- /dev/null
+++ b/cmd/helm/testdata/password
@@ -0,0 +1,1 @@
+password
diff --git a/pkg/downloader/chart_downloader_test.go b/pkg/downloader/chart_downloader_test.go
index 38a06671c..f70a56422 100644
--- a/pkg/downloader/chart_downloader_test.go
+++ b/pkg/downloader/chart_downloader_test.go
@@ -16,7 +16,6 @@ limitations under the License.
 package downloader
 
 import (
-	"net/http"
 	"os"
 	"path/filepath"
 	"testing"
@@ -171,19 +170,7 @@ func TestIsTar(t *testing.T) {
 }
 
 func TestDownloadTo(t *testing.T) {
-	// Set up a fake repo with basic auth enabled
-	srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*")
-	srv.Stop()
-	if err != nil {
-		t.Fatal(err)
-	}
-	srv.WithMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		username, password, ok := r.BasicAuth()
-		if !ok || username != "username" || password != "password" {
-			t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password)
-		}
-	}))
-	srv.Start()
+	srv := repotest.NewTempServerWithCleanupAndBasicAuth(t, "testdata/*.tgz*")
 	defer srv.Stop()
 	if err := srv.CreateIndex(); err != nil {
 		t.Fatal(err)
diff --git a/pkg/repo/index_test.go b/pkg/repo/index_test.go
index 292856527..47f3c6b2e 100644
--- a/pkg/repo/index_test.go
+++ b/pkg/repo/index_test.go
@@ -152,12 +152,6 @@ func TestLoadIndex_Duplicates(t *testing.T) {
 	}
 }
 
-func TestLoadIndex_Empty(t *testing.T) {
-	if _, err := loadIndex([]byte(""), "indexWithEmpty"); err == nil {
-		t.Errorf("Expected an error when index.yaml is empty.")
-	}
-}
-
 func TestLoadIndexFileAnnotations(t *testing.T) {
 	i, err := LoadIndexFile(annotationstestfile)
 	if err != nil {
diff --git a/pkg/repo/repotest/server.go b/pkg/repo/repotest/server.go
index 64d22257b..5c13fdf69 100644
--- a/pkg/repo/repotest/server.go
+++ b/pkg/repo/repotest/server.go
@@ -56,6 +56,23 @@ func NewTempServerWithCleanup(t *testing.T, glob string) (*Server, error) {
 	return srv, err
 }
 
+// Set up a fake repo with basic auth enabled
+func NewTempServerWithCleanupAndBasicAuth(t *testing.T, glob string) *Server {
+	srv, err := NewTempServerWithCleanup(t, glob)
+	srv.Stop()
+	if err != nil {
+		t.Fatal(err)
+	}
+	srv.WithMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		username, password, ok := r.BasicAuth()
+		if !ok || username != "username" || password != "password" {
+			t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password)
+		}
+	}))
+	srv.Start()
+	return srv
+}
+
 type OCIServer struct {
 	*registry.Registry
 	RegistryURL  string