mirror of
https://github.com/containers/skopeo.git
synced 2025-10-22 03:24:25 +00:00
main: Add support for overriding HTTP User-Agent
I want this for https://github.com/bootc-dev/bootc/issues/1686 so we can distinguish pulls there. But more generally it's can be a good idea for people writing scripts using skopeo to set custom user agents so that registries can more easily trace which actors are performing tasks. Assisted-by: Claude Code Signed-off-by: Colin Walters <walters@verbum.org>
This commit is contained in:
committed by
Miloslav Trmač
parent
6b2c20caef
commit
53f9612136
@@ -30,6 +30,7 @@ type globalOptions struct {
|
||||
commandTimeout time.Duration // Timeout for the command execution
|
||||
registriesConfPath string // Path to the "registries.conf" file
|
||||
tmpDir string // Path to use for big temporary files
|
||||
userAgentPrefix string // Prefix to add to the user agent string
|
||||
}
|
||||
|
||||
// requireSubcommand returns an error if no sub command is provided
|
||||
@@ -90,6 +91,7 @@ func createApp() (*cobra.Command, *globalOptions) {
|
||||
logrus.Fatal("unable to mark registries-conf flag as hidden")
|
||||
}
|
||||
rootCommand.PersistentFlags().StringVar(&opts.tmpDir, "tmpdir", "", "directory used to store temporary files")
|
||||
rootCommand.PersistentFlags().StringVar(&opts.userAgentPrefix, "user-agent-prefix", "", "prefix to add to the user agent string")
|
||||
flag := commonFlag.OptionalBoolFlag(rootCommand.Flags(), &opts.tlsVerify, "tls-verify", "Require HTTPS and verify certificates when accessing the registry")
|
||||
flag.Hidden = true
|
||||
rootCommand.AddCommand(
|
||||
@@ -181,6 +183,10 @@ func (opts *globalOptions) commandTimeoutContext() (context.Context, context.Can
|
||||
// newSystemContext returns a *types.SystemContext corresponding to opts.
|
||||
// It is guaranteed to return a fresh instance, so it is safe to make additional updates to it.
|
||||
func (opts *globalOptions) newSystemContext() *types.SystemContext {
|
||||
userAgent := defaultUserAgent
|
||||
if opts.userAgentPrefix != "" {
|
||||
userAgent = opts.userAgentPrefix + " " + defaultUserAgent
|
||||
}
|
||||
ctx := &types.SystemContext{
|
||||
RegistriesDirPath: opts.registriesDirPath,
|
||||
ArchitectureChoice: opts.overrideArch,
|
||||
@@ -188,7 +194,7 @@ func (opts *globalOptions) newSystemContext() *types.SystemContext {
|
||||
VariantChoice: opts.overrideVariant,
|
||||
SystemRegistriesConfPath: opts.registriesConfPath,
|
||||
BigFilesTemporaryDir: opts.tmpDir,
|
||||
DockerRegistryUserAgent: defaultUserAgent,
|
||||
DockerRegistryUserAgent: userAgent,
|
||||
}
|
||||
// DEPRECATED: We support this for backward compatibility, but override it if a per-image flag is provided.
|
||||
if opts.tlsVerify.Present() {
|
||||
|
@@ -96,6 +96,10 @@ Use registry configuration files in _dir_ (e.g. for container signature storage)
|
||||
|
||||
Directory used to store temporary files. Defaults to /var/tmp.
|
||||
|
||||
**--user-agent-prefix** _prefix_
|
||||
|
||||
Prefix to add to the user agent string. The resulting user agent will be in the format "_prefix_ skopeo/_version_".
|
||||
|
||||
**--version**, **-v**
|
||||
|
||||
Print the version number
|
||||
|
114
integration/user_agent_test.go
Normal file
114
integration/user_agent_test.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// mockRegistryHandler implements a minimal Docker Registry V2 API that captures User-Agent headers
|
||||
type mockRegistryHandler struct {
|
||||
mu sync.Mutex
|
||||
userAgents []string
|
||||
}
|
||||
|
||||
func (h *mockRegistryHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Capture the User-Agent header
|
||||
h.mu.Lock()
|
||||
h.userAgents = append(h.userAgents, r.Header.Get("User-Agent"))
|
||||
h.mu.Unlock()
|
||||
|
||||
// Implement minimal Docker Registry V2 API endpoints for inspect --raw
|
||||
switch {
|
||||
case r.URL.Path == "/v2/":
|
||||
// Registry version check endpoint
|
||||
w.Header().Set("Docker-Distribution-API-Version", "registry/2.0")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
case strings.HasSuffix(r.URL.Path, "/manifests/latest"):
|
||||
// Return a minimal OCI manifest as raw string
|
||||
// The digest matches this exact content
|
||||
manifest := `{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","size":0}]}`
|
||||
w.Header().Set("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if _, err := w.Write([]byte(manifest)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *mockRegistryHandler) getUserAgents() []string {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
return slices.Clone(h.userAgents)
|
||||
}
|
||||
|
||||
func TestUserAgent(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
extraArgs []string
|
||||
userAgentValidator func(string) bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "default user agent",
|
||||
extraArgs: []string{},
|
||||
userAgentValidator: func(ua string) bool {
|
||||
return strings.HasPrefix(ua, "skopeo/")
|
||||
},
|
||||
description: "Default user agent should start with 'skopeo/'",
|
||||
},
|
||||
{
|
||||
name: "custom user agent prefix",
|
||||
extraArgs: []string{"--user-agent-prefix", "bootc/1.0"},
|
||||
userAgentValidator: func(ua string) bool {
|
||||
return strings.HasPrefix(ua, "bootc/1.0 skopeo/")
|
||||
},
|
||||
description: "Custom user agent should be in format 'prefix skopeo/version'",
|
||||
},
|
||||
{
|
||||
name: "prefix with spaces",
|
||||
extraArgs: []string{"--user-agent-prefix", "my cool app"},
|
||||
userAgentValidator: func(ua string) bool {
|
||||
return strings.HasPrefix(ua, "my cool app skopeo/")
|
||||
},
|
||||
description: "User agent with spaces should work correctly",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
handler := &mockRegistryHandler{}
|
||||
server := httptest.NewServer(handler)
|
||||
defer server.Close()
|
||||
|
||||
// Extract host:port from the test server URL
|
||||
registryAddr := strings.TrimPrefix(server.URL, "http://")
|
||||
imageRef := "docker://" + registryAddr + "/test/image:latest"
|
||||
|
||||
// Build arguments: base args + test-specific args + image ref
|
||||
args := append([]string{"--tls-verify=false"}, tc.extraArgs...)
|
||||
args = append(args, "inspect", "--raw", imageRef)
|
||||
|
||||
// Run skopeo inspect --raw
|
||||
assertSkopeoSucceeds(t, "", args...)
|
||||
|
||||
// Verify that at least one request was made with the expected User-Agent
|
||||
userAgents := handler.getUserAgents()
|
||||
require.NotEmpty(t, userAgents, "Expected at least one request to be made")
|
||||
|
||||
// Check that at least one User-Agent matches the validator
|
||||
require.True(t,
|
||||
slices.ContainsFunc(userAgents, tc.userAgentValidator),
|
||||
"%s, got: %v", tc.description, userAgents)
|
||||
})
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user