mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 01:28:45 +01:00
feat: Allow custom User-Agent headers for Helm repository requests (#25473)
Signed-off-by: Yugan <yugannkt@gmail.com>
This commit is contained in:
@@ -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, "/")
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user