Repository: argoproj/argo-cd
Pull request #11735 (2022-12-16 15:49:00)

Update USERS.md
Signed-off-by: hopisaurus <hopisaurus@gmail.com>

Note on DCO:

If the DCO action in the integration test fails, one or more of your commits are not signed off. Please click on the *Details* link next to the DCO action for instructions on how to resolve this.

Checklist:

* [ ] Either (a) I've created an [enhancement proposal](https://github.com/argoproj/argo-cd/issues/new/choose) and discussed it with the community, (b) this is a bug fix, or (c) this does not need to be in the release notes.
* [ ] The title of the PR states what changed and the related issues number (used for the release note).
* [ ] I've included "Closes [ISSUE #]" or "Fixes [ISSUE #]" in the description to automatically close the associated issue.
* [ ] I've updated both the CLI and UI to expose my feature, or I plan to submit a second PR with them.
* [ ] Does this PR require documentation updates?
* [ ] I've updated documentation as required by this PR.
* [x] Optional. My organization is added to USERS.md.
* [ ] I have signed off all my commits as required by [DCO](https://github.com/argoproj/argoproj/tree/master/community#contributing-to-argo)
* [ ] I have written unit and/or e2e tests for my change. PRs without these are unlikely to be merged.
* [ ] My build is green ([troubleshooting builds](https://argo-cd.readthedocs.io/en/latest/developer-guide/ci/)). 




Patch:
diff --git a/USERS.md b/USERS.md
index 42f181e3c..a1b667b21 100644
--- a/USERS.md
+++ b/USERS.md
@@ -240,6 +240,7 @@ Currently, the following organizations are **officially** using Argo CD:
 1. [Virtuo](https://www.govirtuo.com/)
 1. [VISITS Technologies](https://visits.world/en)
 1. [Volvo Cars](https://www.volvocars.com/)
+1. [Voyager Digital](https://www.investvoyager.com/)
 1. [VSHN - The DevOps Company](https://vshn.ch/)
 1. [Walkbase](https://www.walkbase.com/)
 1. [Webstores](https://www.webstores.nl)
diff --git a/util/helm/client.go b/util/helm/client.go
index 7ef14e2cb..ec0c0bea4 100644
--- a/util/helm/client.go
+++ b/util/helm/client.go
@@ -390,10 +390,10 @@ func getTagsListURL(rawURL string, chart string) (string, error) {
 	if err != nil {
 		return "", fmt.Errorf("unable to parse repo url: %v", err)
 	}
+	tagsPathFormat := "%s/v2/%s/tags/list"
 	repoURL.Scheme = "https"
-	tagsList := strings.Join([]string{"v2", url.PathEscape(chart), "tags/list"}, "/")
-	repoURL.Path = strings.Join([]string{repoURL.Path, tagsList}, "/")
-	repoURL.RawPath = strings.Join([]string{repoURL.RawPath, tagsList}, "/")
+	repoURL.Path = fmt.Sprintf(tagsPathFormat, repoURL.Path, chart)
+	repoURL.RawPath = fmt.Sprintf(tagsPathFormat, repoURL.RawPath, url.PathEscape(chart))
 	return repoURL.String(), nil
 }
 
@@ -406,7 +406,7 @@ func (c *nativeHelmChart) getTags(chart string) ([]byte, error) {
 	allTags := &TagsList{}
 	var data []byte
 	for nextURL != "" {
-		log.Debugf("fetching %s tags from %s", chart, text.Trunc(nextURL, 100))
+		log.Debugf("fetching %s tags from %s", chart, sanitizeLog(text.Trunc(nextURL, 100)))
 		data, nextURL, err = c.getTagsFromUrl(nextURL)
 		if err != nil {
 			return nil, fmt.Errorf("failed tags part: %v", err)
@@ -426,14 +426,30 @@ func (c *nativeHelmChart) getTags(chart string) ([]byte, error) {
 	return data, nil
 }
 
-func getNextUrl(linkHeader string) string {
-	nextUrl := ""
-	if linkHeader != "" {
-		// drop < >; ref= from the Link header, see: https://docs.docker.com/registry/spec/api/#pagination
-		nextUrl = strings.Split(linkHeader, ";")[0][1:]
-		nextUrl = nextUrl[:len(nextUrl)-1]
+func getNextUrl(resp *http.Response) (string, error) {
+	link := resp.Header.Get("Link")
+	if link == "" {
+		return "", nil
 	}
-	return nextUrl
+	if link[0] != '<' {
+		return "", fmt.Errorf("invalid next link %q: missing '<'", link)
+	}
+	if i := strings.IndexByte(link, '>'); i == -1 {
+		return "", fmt.Errorf("invalid next link %q: missing '>'", link)
+	} else {
+		link = link[1:i]
+	}
+	linkURL, err := resp.Request.URL.Parse(link)
+	if err != nil {
+		return "", err
+	}
+	return linkURL.String(), nil
+}
+
+func sanitizeLog(input string) string {
+	sanitized := strings.ReplaceAll(input, "\r", "")
+	sanitized = strings.ReplaceAll(sanitized, "\n", "")
+	return sanitized
 }
 
 func (c *nativeHelmChart) getTagsFromUrl(tagsURL string) ([]byte, string, error) {
@@ -484,8 +500,8 @@ func (c *nativeHelmChart) getTagsFromUrl(tagsURL string) ([]byte, string, error)
 	if err != nil {
 		return nil, "", fmt.Errorf("failed to read body: %v", err)
 	}
-	nextUrl := getNextUrl(resp.Header.Get("Link"))
-	return data, nextUrl, nil
+	nextUrl, err := getNextUrl(resp)
+	return data, nextUrl, err
 }
 
 func (c *nativeHelmChart) GetTags(chart string, noCache bool) (*TagsList, error) {


Test patch:
diff --git a/util/helm/client_test.go b/util/helm/client_test.go
index 8f12eaf40..8107a586e 100644
--- a/util/helm/client_test.go
+++ b/util/helm/client_test.go
@@ -10,6 +10,7 @@ import (
 
 	"net/http"
 	"net/http/httptest"
+	"net/url"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -177,11 +178,34 @@ func TestGetTagsFromUrl(t *testing.T) {
 }
 
 func Test_getNextUrl(t *testing.T) {
-	nextUrl := getNextUrl("")
+	baseUrl, err := url.Parse("https://my.repo.com/v2/chart/tags/list")
+	if err != nil {
+		t.Errorf("failed to parse url in test case: %v", err)
+	}
+	resp := &http.Response{
+		Request: &http.Request{
+			URL: baseUrl,
+		},
+	}
+	nextUrl, err := getNextUrl(resp)
 	assert.Equal(t, nextUrl, "")
+	assert.NoError(t, err)
+
+	var nextUrlAbsolute = "https://my.repo.com/v2/chart/tags/list?n=123&orderby="
+	resp.Header = http.Header{
+		"Link": []string{fmt.Sprintf(`<%s>; rel="next"`, nextUrlAbsolute)},
+	}
+	nextUrl, err = getNextUrl(resp)
+	assert.NoError(t, err)
+	assert.Equal(t, nextUrl, nextUrlAbsolute)
 
-	nextUrl = getNextUrl("<https://my.repo.com/v2/chart/tags/list?token=123>; rel=next")
-	assert.Equal(t, nextUrl, "https://my.repo.com/v2/chart/tags/list?token=123")
+	var nextUrlRelative = "/v2/chart/tags/list?n=123&orderby="
+	resp.Header = http.Header{
+		"Link": []string{fmt.Sprintf(`<%s>; rel="next"`, nextUrlRelative)},
+	}
+	nextUrl, err = getNextUrl(resp)
+	assert.NoError(t, err)
+	assert.Equal(t, nextUrl, "https://my.repo.com/v2/chart/tags/list?n=123&orderby=")
 }
 
 func Test_getTagsListURL(t *testing.T) {
@@ -197,4 +221,14 @@ func Test_getTagsListURL(t *testing.T) {
 	tagsListURL, err = getTagsListURL("https://account.dkr.ecr.eu-central-1.amazonaws.com/", "dss")
 	assert.Nil(t, err)
 	assert.Equal(t, tagsListURL, "https://account.dkr.ecr.eu-central-1.amazonaws.com/v2/dss/tags/list")
+
+	// with unescaped characters allowed by https://www.rfc-editor.org/rfc/rfc3986#page-50
+	tagsListURL, err = getTagsListURL("https://account.dkr.ecr.eu-central-1.amazonaws.com/", "charts.-_~$&+=:@dss")
+	assert.Nil(t, err)
+	assert.Equal(t, tagsListURL, "https://account.dkr.ecr.eu-central-1.amazonaws.com/v2/charts.-_~$&+=:@dss/tags/list")
+
+	// with escaped characters not allowed in path by https://www.rfc-editor.org/rfc/rfc3986#page-50
+	tagsListURL, err = getTagsListURL("https://account.dkr.ecr.eu-central-1.amazonaws.com/", "charts%/dss")
+	assert.Nil(t, err)
+	assert.Equal(t, tagsListURL, "https://account.dkr.ecr.eu-central-1.amazonaws.com/v2/charts%25%2Fdss/tags/list")
 }