feat: Allow custom User-Agent headers for Helm repository requests (#25473)

Signed-off-by: Yugan <yugannkt@gmail.com>
This commit is contained in:
Yugan
2026-01-19 22:36:09 +05:30
committed by GitHub
parent 228378474a
commit 408e99e9e9
17 changed files with 280 additions and 2 deletions

View File

@@ -27,6 +27,7 @@ import (
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials"
"github.com/argoproj/argo-cd/v3/common"
"github.com/argoproj/argo-cd/v3/util/cache"
utilio "github.com/argoproj/argo-cd/v3/util/io"
"github.com/argoproj/argo-cd/v3/util/io/files"
@@ -40,6 +41,18 @@ var (
ErrOCINotEnabled = errors.New("could not perform the action when oci is not enabled")
)
// userAgentTransport wraps an http.RoundTripper to add User-Agent header to all requests
type userAgentTransport struct {
Transport http.RoundTripper
UserAgent string
}
// RoundTrip implements the http.RoundTripper interface
func (t *userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", t.UserAgent)
return t.Transport.RoundTrip(req)
}
type indexCache interface {
SetHelmIndex(repo string, indexData []byte) error
GetHelmIndex(repo string, indexData *[]byte) error
@@ -67,6 +80,14 @@ func WithChartPaths(chartPaths utilio.TempPaths) ClientOpts {
}
}
// WithUserAgent sets a custom User-Agent string for HTTP requests.
// If not set, a default User-Agent will be generated automatically.
func WithUserAgent(userAgent string) ClientOpts {
return func(c *nativeHelmChart) {
c.customUserAgent = userAgent
}
}
func NewClient(repoURL string, creds Creds, enableOci bool, proxy string, noProxy string, opts ...ClientOpts) Client {
return NewClientWithLock(repoURL, creds, globalLock, enableOci, proxy, noProxy, opts...)
}
@@ -98,6 +119,19 @@ type nativeHelmChart struct {
indexCache indexCache
proxy string
noProxy string
customUserAgent string // Custom User-Agent string (optional)
}
// getUserAgent returns the User-Agent string to use for HTTP requests.
// If a custom User-Agent is set, it will be used; otherwise, a default will be generated.
func (c *nativeHelmChart) getUserAgent() string {
if c.customUserAgent != "" {
return c.customUserAgent
}
// Default User-Agent with version and platform info
version := common.GetVersion()
return fmt.Sprintf("argocd-repo-server/%s (%s)", version.Version, version.Platform)
}
func fileExist(filePath string) (bool, error) {
@@ -317,6 +351,10 @@ func (c *nativeHelmChart) loadRepoIndex(ctx context.Context, maxIndexSize int64)
if err != nil {
return nil, fmt.Errorf("error creating HTTP request: %w", err)
}
// Set User-Agent header to comply with robot policies
req.Header.Set("User-Agent", c.getUserAgent())
helmPassword, err := c.creds.GetPassword()
if err != nil {
return nil, fmt.Errorf("failed to get password for helm registry: %w", err)
@@ -448,11 +486,21 @@ func (c *nativeHelmChart) GetTags(chart string, noCache bool) ([]string, error)
if err != nil {
return nil, fmt.Errorf("failed setup tlsConfig: %w", err)
}
client := &http.Client{Transport: &http.Transport{
// Create base transport with TLS config and proxy
baseTransport := &http.Transport{
Proxy: proxy.GetCallback(c.proxy, c.noProxy),
TLSClientConfig: tlsConf,
DisableKeepAlives: true,
}}
}
// Wrap transport to add User-Agent header to all requests
client := &http.Client{
Transport: &userAgentTransport{
Transport: baseTransport,
UserAgent: c.getUserAgent(),
},
}
repoHost, _, _ := strings.Cut(tagsURL, "/")

View File

@@ -573,3 +573,135 @@ func TestGetTagsCaching(t *testing.T) {
assert.Equal(t, 2, requestCount)
})
}
func TestUserAgentIsSet(t *testing.T) {
t.Run("Default User-Agent for traditional Helm repo", func(t *testing.T) {
// Create a test server that captures the User-Agent header
receivedUserAgent := ""
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedUserAgent = r.Header.Get("User-Agent")
// Return a valid minimal index.yaml
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`apiVersion: v1
entries: {}
`))
}))
defer ts.Close()
// Create client and make a request
client := NewClient(ts.URL, HelmCreds{}, false, "", "")
_, err := client.GetIndex(false, 10000)
require.NoError(t, err)
// Verify User-Agent was set and contains expected components
assert.NotEmpty(t, receivedUserAgent, "User-Agent header should be set")
assert.Contains(t, receivedUserAgent, "argocd-repo-server", "User-Agent should contain 'argocd-repo-server'")
t.Logf("User-Agent sent: %s", receivedUserAgent)
})
t.Run("Custom User-Agent via WithUserAgent option", func(t *testing.T) {
// Create a test server that captures the User-Agent header
receivedUserAgent := ""
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedUserAgent = r.Header.Get("User-Agent")
// Return a valid minimal index.yaml
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`apiVersion: v1
entries: {}
`))
}))
defer ts.Close()
// Create client with custom User-Agent
customUA := "my-custom-app/1.2.3 (contact@example.com)"
client := NewClient(ts.URL, HelmCreds{}, false, "", "", WithUserAgent(customUA))
_, err := client.GetIndex(false, 10000)
require.NoError(t, err)
// Verify custom User-Agent was used
assert.Equal(t, customUA, receivedUserAgent, "Custom User-Agent should be used")
t.Logf("Custom User-Agent sent: %s", receivedUserAgent)
})
}
func TestUserAgentRequiredByServer(t *testing.T) {
t.Run("Server rejects requests without User-Agent", func(t *testing.T) {
// Create a test server that mimics Wikimedia's behavior
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userAgent := r.Header.Get("User-Agent")
t.Logf("Server received User-Agent: '%s'", userAgent)
if userAgent == "" {
// Mimic Wikimedia's rejection of empty User-Agent
w.WriteHeader(http.StatusForbidden)
_, _ = w.Write([]byte(`authorization failed: Please set a user-agent and respect our robot policy`))
return
}
// Accept request with User-Agent
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`apiVersion: v1
entries: {}
`))
}))
defer ts.Close()
// Create client (should automatically set User-Agent)
client := NewClient(ts.URL, HelmCreds{}, false, "", "")
_, err := client.GetIndex(false, 10000)
// Should succeed because our implementation sets User-Agent
require.NoError(t, err, "Request should succeed with User-Agent set")
t.Logf("Success! Server accepted request with User-Agent")
})
}
func TestUserAgentPriority(t *testing.T) {
t.Run("Custom User-Agent set via WithUserAgent option", func(t *testing.T) {
// Create a test server that captures the User-Agent header
receivedUserAgent := ""
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedUserAgent = r.Header.Get("User-Agent")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`apiVersion: v1
entries: {}
`))
}))
defer ts.Close()
// Set custom User-Agent using WithUserAgent option
customUA := "CustomAgent/1.0 (test)"
client := NewClient(ts.URL, HelmCreds{}, false, "", "", WithUserAgent(customUA))
_, err := client.GetIndex(false, 10000)
require.NoError(t, err)
// Verify custom User-Agent was used
assert.Equal(t, customUA, receivedUserAgent, "Custom User-Agent should be used when set")
t.Logf("Custom User-Agent sent: %s", receivedUserAgent)
})
t.Run("Default User-Agent used when no custom value provided", func(t *testing.T) {
// Create a test server that captures the User-Agent header
receivedUserAgent := ""
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedUserAgent = r.Header.Get("User-Agent")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`apiVersion: v1
entries: {}
`))
}))
defer ts.Close()
// Create client without custom User-Agent
client := NewClient(ts.URL, HelmCreds{}, false, "", "")
_, err := client.GetIndex(false, 10000)
require.NoError(t, err)
// Verify default User-Agent was used
assert.Contains(t, receivedUserAgent, "argocd-repo-server", "Should use default User-Agent format")
t.Logf("Default User-Agent sent: %s", receivedUserAgent)
})
}