Repository: helm/helm (go)

Issue:
'helm repo update {REPO} ' feature
Some `helm repo` commands accept the repository name as parameter, that is not the case of `helm repo update`, this command doesn't receive any extra parameter, updating all the installed repos.

There are some cases where it would be useful to just update one specific repository, for example, if I am working with one specific repository, I don't need to update all of them, just the one I want to obtain the latest changes.

With the "new" approach of distributed repositories, it will be more habitual to have a larger list of repositories. Do you think something like this feature is useful? Is it a good option for a first-contribution PR?

```console
$ helm repo list
NAME             	URL
bitnami          	https://charts.bitnami.com/bitnami
stable           	https://kubernetes-charts.storage.googleapis.com
azure-marketplace	https://marketplace.azurecr.io/helm/v1/repo

$ helm repo update bitnami
Hang tight while we grab the latest from your chart repository...
...Successfully got an update from the "bitnami" chart repository
Update Complete. ⎈ Happy Helming!⎈
```
instead of
```console
$ helm repo list
NAME             	URL
bitnami          	https://charts.bitnami.com/bitnami
stable           	https://kubernetes-charts.storage.googleapis.com
azure-marketplace	https://marketplace.azurecr.io/helm/v1/repo

$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "azure-marketplace" chart repository
...Successfully got an update from the "bitnami" chart repository
...Successfully got an update from the "stable" chart repository
Update Complete. ⎈ Happy Helming!⎈
```

Pull request:
feat(cmd): Allow to specify which repos to update
Fixed #8071 

**What this PR does / why we need it**:

As discussed in the Helm dev call, this is a port to helm v3 of #5182, which allows to specify which repositories to update.
 
 A little more flexible than the v2 version, this PR allows to specify a list of repositories that should be updated instead of a single one.  This is in line with v3's support for `helm repo remove` which also allows a list of repos to be specified.

**Special notes for your reviewer**:

**If applicable**:
- [ ] this PR contains documentation
- [x] this PR contains unit tests
- [ ] this PR has been tested for backwards compatibility


Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 491e648a1..5c2445711 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,5 +1,5 @@
 run:
-  timeout: 2m
+  timeout: 10m
 
 linters:
   disable-all: true
diff --git a/Makefile b/Makefile
index d897bc939..555484fcd 100644
--- a/Makefile
+++ b/Makefile
@@ -113,7 +113,7 @@ test-coverage:
 
 .PHONY: test-style
 test-style:
-	GO111MODULE=on golangci-lint run --timeout 5m0s
+	GO111MODULE=on golangci-lint run
 	@scripts/validate-license.sh
 
 .PHONY: test-acceptance
diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go
index d0aab5c18..1766f8646 100644
--- a/cmd/helm/helm.go
+++ b/cmd/helm/helm.go
@@ -32,6 +32,7 @@ import (
 	"helm.sh/helm/v3/pkg/action"
 	"helm.sh/helm/v3/pkg/cli"
 	"helm.sh/helm/v3/pkg/gates"
+	"helm.sh/helm/v3/pkg/kube"
 	kubefake "helm.sh/helm/v3/pkg/kube/fake"
 	"helm.sh/helm/v3/pkg/release"
 	"helm.sh/helm/v3/pkg/storage/driver"
@@ -59,6 +60,12 @@ func warning(format string, v ...interface{}) {
 }
 
 func main() {
+	// Setting the name of the app for managedFields in the Kubernetes client.
+	// It is set here to the full name of "helm" so that renaming of helm to
+	// another name (e.g., helm2 or helm3) does not change the name of the
+	// manager as picked up by the automated name detection.
+	kube.ManagedFieldsManager = "helm"
+
 	actionConfig := new(action.Configuration)
 	cmd, err := newRootCmd(actionConfig, os.Stdout, os.Args[1:])
 	if err != nil {
diff --git a/cmd/helm/repo_update.go b/cmd/helm/repo_update.go
index 23dca194a..43036847d 100644
--- a/cmd/helm/repo_update.go
+++ b/cmd/helm/repo_update.go
@@ -32,6 +32,10 @@ import (
 const updateDesc = `
 Update gets the latest information about charts from the respective chart repositories.
 Information is cached locally, where it is used by commands like 'helm search'.
+
+You can optionally specify a list of repositories you want to update.
+	$ helm repo update <repo_name> ...
+To update all the repositories, use 'helm repo update'.
 `
 
 var errNoRepositories = errors.New("no repositories found. You must add one before updating")
@@ -40,21 +44,25 @@ type repoUpdateOptions struct {
 	update    func([]*repo.ChartRepository, io.Writer)
 	repoFile  string
 	repoCache string
+	names     []string
 }
 
 func newRepoUpdateCmd(out io.Writer) *cobra.Command {
 	o := &repoUpdateOptions{update: updateCharts}
 
 	cmd := &cobra.Command{
-		Use:               "update",
-		Aliases:           []string{"up"},
-		Short:             "update information of available charts locally from chart repositories",
-		Long:              updateDesc,
-		Args:              require.NoArgs,
-		ValidArgsFunction: noCompletions,
+		Use:     "update [REPO1 [REPO2 ...]]",
+		Aliases: []string{"up"},
+		Short:   "update information of available charts locally from chart repositories",
+		Long:    updateDesc,
+		Args:    require.MinimumNArgs(0),
+		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
+			return compListRepos(toComplete, args), cobra.ShellCompDirectiveNoFileComp
+		},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			o.repoFile = settings.RepositoryConfig
 			o.repoCache = settings.RepositoryCache
+			o.names = args
 			return o.run(out)
 		},
 	}
@@ -73,15 +81,26 @@ func (o *repoUpdateOptions) run(out io.Writer) error {
 	}
 
 	var repos []*repo.ChartRepository
-	for _, cfg := range f.Repositories {
-		r, err := repo.NewChartRepository(cfg, getter.All(settings))
-		if err != nil {
+	updateAllRepos := len(o.names) == 0
+
+	if !updateAllRepos {
+		// Fail early if the user specified an invalid repo to update
+		if err := checkRequestedRepos(o.names, f.Repositories); err != nil {
 			return err
 		}
-		if o.repoCache != "" {
-			r.CachePath = o.repoCache
+	}
+
+	for _, cfg := range f.Repositories {
+		if updateAllRepos || isRepoRequested(cfg.Name, o.names) {
+			r, err := repo.NewChartRepository(cfg, getter.All(settings))
+			if err != nil {
+				return err
+			}
+			if o.repoCache != "" {
+				r.CachePath = o.repoCache
+			}
+			repos = append(repos, r)
 		}
-		repos = append(repos, r)
 	}
 
 	o.update(repos, out)
@@ -105,3 +124,28 @@ func updateCharts(repos []*repo.ChartRepository, out io.Writer) {
 	wg.Wait()
 	fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈")
 }
+
+func checkRequestedRepos(requestedRepos []string, validRepos []*repo.Entry) error {
+	for _, requestedRepo := range requestedRepos {
+		found := false
+		for _, repo := range validRepos {
+			if requestedRepo == repo.Name {
+				found = true
+				break
+			}
+		}
+		if !found {
+			return errors.Errorf("no repositories found matching '%s'.  Nothing will be updated", requestedRepo)
+		}
+	}
+	return nil
+}
+
+func isRepoRequested(repoName string, requestedRepos []string) bool {
+	for _, requestedRepo := range requestedRepos {
+		if repoName == requestedRepo {
+			return true
+		}
+	}
+	return false
+}
diff --git a/internal/resolver/resolver.go b/internal/resolver/resolver.go
index de0634093..9c65d2965 100644
--- a/internal/resolver/resolver.go
+++ b/internal/resolver/resolver.go
@@ -57,6 +57,11 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
 	locked := make([]*chart.Dependency, len(reqs))
 	missing := []string{}
 	for i, d := range reqs {
+		constraint, err := semver.NewConstraint(d.Version)
+		if err != nil {
+			return nil, errors.Wrapf(err, "dependency %q has an invalid version/constraint format", d.Name)
+		}
+
 		if d.Repository == "" {
 			// Local chart subfolder
 			if _, err := GetLocalPath(filepath.Join("charts", d.Name), r.chartpath); err != nil {
@@ -77,13 +82,22 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
 				return nil, err
 			}
 
-			// The version of the chart locked will be the version of the chart
-			// currently listed in the file system within the chart.
 			ch, err := loader.LoadDir(chartpath)
 			if err != nil {
 				return nil, err
 			}
 
+			v, err := semver.NewVersion(ch.Metadata.Version)
+			if err != nil {
+				// Not a legit entry.
+				continue
+			}
+
+			if !constraint.Check(v) {
+				missing = append(missing, d.Name)
+				continue
+			}
+
 			locked[i] = &chart.Dependency{
 				Name:       d.Name,
 				Repository: d.Repository,
@@ -92,11 +106,6 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string
 			continue
 		}
 
-		constraint, err := semver.NewConstraint(d.Version)
-		if err != nil {
-			return nil, errors.Wrapf(err, "dependency %q has an invalid version/constraint format", d.Name)
-		}
-
 		repoName := repoNames[d.Name]
 		// if the repository was not defined, but the dependency defines a repository url, bypass the cache
 		if repoName == "" && d.Repository != "" {
diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go
index 2c0d55a55..575c94151 100644
--- a/pkg/downloader/chart_downloader.go
+++ b/pkg/downloader/chart_downloader.go
@@ -158,7 +158,6 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er
 	if err != nil {
 		return nil, errors.Errorf("invalid chart URL format: %s", ref)
 	}
-	c.Options = append(c.Options, getter.WithURL(ref))
 
 	rf, err := loadRepoConfig(c.RepositoryConfig)
 	if err != nil {
@@ -177,6 +176,8 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er
 			// If there is no special config, return the default HTTP client and
 			// swallow the error.
 			if err == ErrNoOwnerRepo {
+				// Make sure to add the ref URL as the URL for the getter
+				c.Options = append(c.Options, getter.WithURL(ref))
 				return u, nil
 			}
 			return u, err
@@ -215,6 +216,10 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er
 		return u, err
 	}
 
+	// Now that we have the chart repository information we can use that URL
+	// to set the URL for the getter.
+	c.Options = append(c.Options, getter.WithURL(rc.URL))
+
 	r, err := repo.NewChartRepository(rc, c.Getters)
 	if err != nil {
 		return u, err
diff --git a/pkg/getter/httpgetter.go b/pkg/getter/httpgetter.go
index 94b64381c..454eb6eb6 100644
--- a/pkg/getter/httpgetter.go
+++ b/pkg/getter/httpgetter.go
@@ -43,13 +43,11 @@ func (g *HTTPGetter) Get(href string, options ...Option) (*bytes.Buffer, error)
 }
 
 func (g *HTTPGetter) get(href string) (*bytes.Buffer, error) {
-	buf := bytes.NewBuffer(nil)
-
 	// Set a helm specific user agent so that a repo server and metrics can
 	// separate helm calls from other tools interacting with repos.
-	req, err := http.NewRequest("GET", href, nil)
+	req, err := http.NewRequest(http.MethodGet, href, nil)
 	if err != nil {
-		return buf, err
+		return nil, err
 	}
 
 	req.Header.Set("User-Agent", version.GetUserAgent())
@@ -61,11 +59,11 @@ func (g *HTTPGetter) get(href string) (*bytes.Buffer, error) {
 	// with the basic auth is the one being fetched.
 	u1, err := url.Parse(g.opts.url)
 	if err != nil {
-		return buf, errors.Wrap(err, "Unable to parse getter URL")
+		return nil, errors.Wrap(err, "Unable to parse getter URL")
 	}
 	u2, err := url.Parse(href)
 	if err != nil {
-		return buf, errors.Wrap(err, "Unable to parse URL getting from")
+		return nil, errors.Wrap(err, "Unable to parse URL getting from")
 	}
 
 	// Host on URL (returned from url.Parse) contains the port if present.
@@ -84,14 +82,15 @@ func (g *HTTPGetter) get(href string) (*bytes.Buffer, error) {
 
 	resp, err := client.Do(req)
 	if err != nil {
-		return buf, err
+		return nil, err
 	}
-	if resp.StatusCode != 200 {
-		return buf, errors.Errorf("failed to fetch %s : %s", href, resp.Status)
+	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusOK {
+		return nil, errors.Errorf("failed to fetch %s : %s", href, resp.Status)
 	}
 
+	buf := bytes.NewBuffer(nil)
 	_, err = io.Copy(buf, resp.Body)
-	resp.Body.Close()
 	return buf, err
 }
 
diff --git a/pkg/kube/client.go b/pkg/kube/client.go
index 2565d1832..76a832b0e 100644
--- a/pkg/kube/client.go
+++ b/pkg/kube/client.go
@@ -21,6 +21,8 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"os"
+	"path/filepath"
 	"strings"
 	"sync"
 	"time"
@@ -55,6 +57,10 @@ var ErrNoObjectsVisited = errors.New("no objects visited")
 
 var metadataAccessor = meta.NewAccessor()
 
+// ManagedFieldsManager is the name of the manager of Kubernetes managedFields
+// first introduced in Kubernetes 1.18
+var ManagedFieldsManager string
+
 // Client represents a client capable of communicating with the Kubernetes API.
 type Client struct {
 	Factory Factory
@@ -100,7 +106,7 @@ func (c *Client) getKubeClient() (*kubernetes.Clientset, error) {
 	return c.kubeClient, err
 }
 
-// IsReachable tests connectivity to the cluster
+// IsReachable tests connectivity to the cluster.
 func (c *Client) IsReachable() error {
 	client, err := c.getKubeClient()
 	if err == genericclioptions.ErrEmptyConfig {
@@ -126,7 +132,7 @@ func (c *Client) Create(resources ResourceList) (*Result, error) {
 	return &Result{Created: resources}, nil
 }
 
-// Wait up to the given timeout for the specified resources to be ready
+// Wait waits up to the given timeout for the specified resources to be ready.
 func (c *Client) Wait(resources ResourceList, timeout time.Duration) error {
 	cs, err := c.getKubeClient()
 	if err != nil {
@@ -206,7 +212,7 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err
 			return err
 		}
 
-		helper := resource.NewHelper(info.Client, info.Mapping)
+		helper := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager())
 		if _, err := helper.Get(info.Namespace, info.Name); err != nil {
 			if !apierrors.IsNotFound(err) {
 				return errors.Wrap(err, "could not get information about the resource")
@@ -324,7 +330,7 @@ func (c *Client) watchTimeout(t time.Duration) func(*resource.Info) error {
 
 // WatchUntilReady watches the resources given and waits until it is ready.
 //
-// This function is mainly for hook implementations. It watches for a resource to
+// This method is mainly for hook implementations. It watches for a resource to
 // hit a particular milestone. The milestone depends on the Kind.
 //
 // For most kinds, it checks to see if the resource is marked as Added or Modified
@@ -359,6 +365,26 @@ func perform(infos ResourceList, fn func(*resource.Info) error) error {
 	return nil
 }
 
+// getManagedFieldsManager returns the manager string. If one was set it will be returned.
+// Otherwise, one is calculated based on the name of the binary.
+func getManagedFieldsManager() string {
+
+	// When a manager is explicitly set use it
+	if ManagedFieldsManager != "" {
+		return ManagedFieldsManager
+	}
+
+	// When no manager is set and no calling application can be found it is unknown
+	if len(os.Args[0]) == 0 {
+		return "unknown"
+	}
+
+	// When there is an application that can be determined and no set manager
+	// use the base name. This is one of the ways Kubernetes libs handle figuring
+	// names out.
+	return filepath.Base(os.Args[0])
+}
+
 func batchPerform(infos ResourceList, fn func(*resource.Info) error, errs chan<- error) {
 	var kind string
 	var wg sync.WaitGroup
@@ -377,7 +403,7 @@ func batchPerform(infos ResourceList, fn func(*resource.Info) error, errs chan<-
 }
 
 func createResource(info *resource.Info) error {
-	obj, err := resource.NewHelper(info.Client, info.Mapping).Create(info.Namespace, true, info.Object)
+	obj, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).Create(info.Namespace, true, info.Object)
 	if err != nil {
 		return err
 	}
@@ -387,7 +413,7 @@ func createResource(info *resource.Info) error {
 func deleteResource(info *resource.Info) error {
 	policy := metav1.DeletePropagationBackground
 	opts := &metav1.DeleteOptions{PropagationPolicy: &policy}
-	_, err := resource.NewHelper(info.Client, info.Mapping).DeleteWithOptions(info.Namespace, info.Name, opts)
+	_, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).DeleteWithOptions(info.Namespace, info.Name, opts)
 	return err
 }
 
@@ -402,7 +428,7 @@ func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.P
 	}
 
 	// Fetch the current object for the three way merge
-	helper := resource.NewHelper(target.Client, target.Mapping)
+	helper := resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager())
 	currentObj, err := helper.Get(target.Namespace, target.Name)
 	if err != nil && !apierrors.IsNotFound(err) {
 		return nil, types.StrategicMergePatchType, errors.Wrapf(err, "unable to get data for current object %s/%s", target.Namespace, target.Name)
@@ -444,7 +470,7 @@ func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.P
 func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, force bool) error {
 	var (
 		obj    runtime.Object
-		helper = resource.NewHelper(target.Client, target.Mapping)
+		helper = resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager())
 		kind   = target.Mapping.GroupVersionKind.Kind
 	)
 
diff --git a/pkg/kube/interface.go b/pkg/kube/interface.go
index 545985996..59a4cbdcc 100644
--- a/pkg/kube/interface.go
+++ b/pkg/kube/interface.go
@@ -30,14 +30,19 @@ type Interface interface {
 	// Create creates one or more resources.
 	Create(resources ResourceList) (*Result, error)
 
+	// Wait waits up to the given timeout for the specified resources to be ready.
 	Wait(resources ResourceList, timeout time.Duration) error
 
+	// WaitWithJobs wait up to the given timeout for the specified resources to be ready, including jobs.
 	WaitWithJobs(resources ResourceList, timeout time.Duration) error
 
 	// Delete destroys one or more resources.
 	Delete(resources ResourceList) (*Result, []error)
 
-	// Watch the resource in reader until it is "ready". This method
+	// WatchUntilReady watches the resources given and waits until it is ready.
+	//
+	// This method is mainly for hook implementations. It watches for a resource to
+	// hit a particular milestone. The milestone depends on the Kind.
 	//
 	// For Jobs, "ready" means the Job ran to completion (exited without error).
 	// For Pods, "ready" means the Pod phase is marked "succeeded".
@@ -49,9 +54,9 @@ type Interface interface {
 	// if it doesn't exist.
 	Update(original, target ResourceList, force bool) (*Result, error)
 
-	// Build creates a resource list from a Reader
+	// Build creates a resource list from a Reader.
 	//
-	// reader must contain a YAML stream (one or more YAML documents separated
+	// Reader must contain a YAML stream (one or more YAML documents separated
 	// by "\n---\n")
 	//
 	// Validates against OpenAPI schema if validate is true.
@@ -61,7 +66,7 @@ type Interface interface {
 	// and returns said phase (PodSucceeded or PodFailed qualify).
 	WaitAndGetCompletedPodPhase(name string, timeout time.Duration) (v1.PodPhase, error)
 
-	// isReachable checks whether the client is able to connect to the cluster
+	// IsReachable checks whether the client is able to connect to the cluster.
 	IsReachable() error
 }
 


Test patch:
diff --git a/cmd/helm/install_test.go b/cmd/helm/install_test.go
index 13c994b92..b7349e3d5 100644
--- a/cmd/helm/install_test.go
+++ b/cmd/helm/install_test.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"net/http"
 	"net/http/httptest"
+	"path/filepath"
 	"testing"
 
 	"helm.sh/helm/v3/pkg/repo/repotest"
@@ -48,6 +49,8 @@ func TestInstall(t *testing.T) {
 		t.Fatal(err)
 	}
 
+	repoFile := filepath.Join(srv.Root(), "repositories.yaml")
+
 	tests := []cmdTestCase{
 		// Install, base case
 		{
@@ -244,6 +247,11 @@ func TestInstall(t *testing.T) {
 			cmd:    "install aeneas reqtest --namespace default --repo " + srv2.URL + " --username username --password password --pass-credentials",
 			golden: "output/install.txt",
 		},
+		{
+			name:   "basic install with credentials and no repo",
+			cmd:    fmt.Sprintf("install aeneas test/reqtest --username username --password password --repository-config %s --repository-cache %s", repoFile, srv.Root()),
+			golden: "output/install.txt",
+		},
 	}
 
 	runTestActionCmd(t, tests)
diff --git a/cmd/helm/repo_update_test.go b/cmd/helm/repo_update_test.go
index 83ef24349..d769c8aa7 100644
--- a/cmd/helm/repo_update_test.go
+++ b/cmd/helm/repo_update_test.go
@@ -48,8 +48,54 @@ func TestUpdateCmd(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	if got := out.String(); !strings.Contains(got, "charts") {
-		t.Errorf("Expected 'charts' got %q", got)
+	if got := out.String(); !strings.Contains(got, "charts") ||
+		!strings.Contains(got, "firstexample") ||
+		!strings.Contains(got, "secondexample") {
+		t.Errorf("Expected 'charts', 'firstexample' and 'secondexample' but got %q", got)
+	}
+}
+
+func TestUpdateCmdMultiple(t *testing.T) {
+	var out bytes.Buffer
+	// Instead of using the HTTP updater, we provide our own for this test.
+	// The TestUpdateCharts test verifies the HTTP behavior independently.
+	updater := func(repos []*repo.ChartRepository, out io.Writer) {
+		for _, re := range repos {
+			fmt.Fprintln(out, re.Config.Name)
+		}
+	}
+	o := &repoUpdateOptions{
+		update:   updater,
+		repoFile: "testdata/repositories.yaml",
+		names:    []string{"firstexample", "charts"},
+	}
+	if err := o.run(&out); err != nil {
+		t.Fatal(err)
+	}
+
+	if got := out.String(); !strings.Contains(got, "charts") ||
+		!strings.Contains(got, "firstexample") ||
+		strings.Contains(got, "secondexample") {
+		t.Errorf("Expected 'charts' and 'firstexample' but not 'secondexample' but got %q", got)
+	}
+}
+
+func TestUpdateCmdInvalid(t *testing.T) {
+	var out bytes.Buffer
+	// Instead of using the HTTP updater, we provide our own for this test.
+	// The TestUpdateCharts test verifies the HTTP behavior independently.
+	updater := func(repos []*repo.ChartRepository, out io.Writer) {
+		for _, re := range repos {
+			fmt.Fprintln(out, re.Config.Name)
+		}
+	}
+	o := &repoUpdateOptions{
+		update:   updater,
+		repoFile: "testdata/repositories.yaml",
+		names:    []string{"firstexample", "invalid"},
+	}
+	if err := o.run(&out); err == nil {
+		t.Fatal("expected error but did not get one")
 	}
 }
 
@@ -111,4 +157,5 @@ func TestUpdateCharts(t *testing.T) {
 
 func TestRepoUpdateFileCompletion(t *testing.T) {
 	checkFileCompletion(t, "repo update", false)
+	checkFileCompletion(t, "repo update repo1", false)
 }
diff --git a/cmd/helm/testdata/repositories.yaml b/cmd/helm/testdata/repositories.yaml
index 423b9f195..6be26b771 100644
--- a/cmd/helm/testdata/repositories.yaml
+++ b/cmd/helm/testdata/repositories.yaml
@@ -2,3 +2,8 @@ apiVersion: v1
 repositories:
   - name: charts
     url: "https://charts.helm.sh/stable"
+  - name: firstexample
+    url: "http://firstexample.com"
+  - name: secondexample
+    url: "http://secondexample.com"
+
diff --git a/internal/resolver/resolver_test.go b/internal/resolver/resolver_test.go
index 204a72622..bc84ba952 100644
--- a/internal/resolver/resolver_test.go
+++ b/internal/resolver/resolver_test.go
@@ -28,6 +28,18 @@ func TestResolve(t *testing.T) {
 		expect *chart.Lock
 		err    bool
 	}{
+		{
+			name: "repo from invalid version",
+			req: []*chart.Dependency{
+				{Name: "base", Repository: "file://base", Version: "1.1.0"},
+			},
+			expect: &chart.Lock{
+				Dependencies: []*chart.Dependency{
+					{Name: "base", Repository: "file://base", Version: "0.1.0"},
+				},
+			},
+			err: true,
+		},
 		{
 			name: "version failure",
 			req: []*chart.Dependency{