feature!: support for setting HTTP User-Agent header (#157)

* feature!: support for setting HTTP User-Agent header

* fix lint

* fix e2e

* refactor: move http.go to internal/util/useragent package and rename functions to Get and Set

* Move packages and use attest version
This commit is contained in:
James Carnegie
2024-09-09 14:22:17 +01:00
committed by GitHub
parent ed0ae8ecf6
commit b4e6767cc6
33 changed files with 307 additions and 340 deletions

View File

@@ -1,6 +1,7 @@
package tuf_test
import (
"context"
"os"
"path/filepath"
@@ -20,7 +21,7 @@ func ExampleNewClient_registry() {
metadataURI := "registry-1.docker.io/docker/tuf-metadata:latest"
targetsURI := "registry-1.docker.io/docker/tuf-targets"
registryClient, err := tuf.NewClient(&tuf.ClientOptions{tuf.DockerTUFRootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker()})
registryClient, err := tuf.NewClient(context.Background(), &tuf.ClientOptions{tuf.DockerTUFRootStaging.Data, tufOutputPath, metadataURI, targetsURI, tuf.NewMockVersionChecker()})
if err != nil {
panic(err)
}

View File

@@ -1,6 +1,7 @@
package tuf
import (
"context"
"encoding/json"
"fmt"
"io"
@@ -11,6 +12,7 @@ import (
"time"
"github.com/distribution/reference"
"github.com/docker/attest/internal/useragent"
"github.com/docker/attest/oci"
"github.com/google/go-containerregistry/pkg/authn"
"github.com/google/go-containerregistry/pkg/crane"
@@ -70,7 +72,7 @@ type Layers struct {
MediaType string `json:"mediaType"`
}
func NewRegistryFetcher(cfg *config.UpdaterConfig) (*RegistryFetcher, error) {
func NewRegistryFetcher(ctx context.Context, cfg *config.UpdaterConfig) (*RegistryFetcher, error) {
ref, err := reference.ParseNormalizedNamed(cfg.RemoteMetadataURL)
if err != nil {
return nil, fmt.Errorf("failed to parse metadata repo: %w", err)
@@ -89,11 +91,12 @@ func NewRegistryFetcher(cfg *config.UpdaterConfig) (*RegistryFetcher, error) {
targetsRepo := targetsRef.Name()
return &RegistryFetcher{
// we need to keep these reference so that we can unmangle the URL paths when downloading files
cfg: cfg,
metadataRepo: metadataRepo,
metadataTag: metadataTag,
targetsRepo: targetsRepo,
cache: NewImageCache(),
cfg: cfg,
metadataRepo: metadataRepo,
metadataTag: metadataTag,
targetsRepo: targetsRepo,
cache: NewImageCache(),
httpUserAgent: useragent.Get(ctx),
}, nil
}

View File

@@ -9,6 +9,8 @@ import (
"strings"
"testing"
"github.com/docker/attest/internal/test"
"github.com/docker/attest/internal/useragent"
"github.com/docker/attest/internal/util"
"github.com/docker/attest/oci"
"github.com/google/go-containerregistry/pkg/crane"
@@ -22,7 +24,6 @@ import (
"github.com/google/go-containerregistry/pkg/v1/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go/modules/registry"
"github.com/theupdateframework/go-tuf/v2/metadata"
"github.com/theupdateframework/go-tuf/v2/metadata/config"
"github.com/theupdateframework/go-tuf/v2/metadata/updater"
@@ -38,14 +39,14 @@ const (
)
func TestRegistryFetcher(t *testing.T) {
// run local registry
registry, regAddr := RunTestRegistry(t)
defer func() {
if err := registry.Terminate(context.Background()); err != nil {
t.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()
LoadRegistryTestData(t, regAddr, OCITUFTestDataPath)
ctx := context.Background()
regServer := test.NewLocalRegistry(ctx)
defer regServer.Close()
regAddr, err := url.Parse(regServer.URL)
require.NoError(t, err)
LoadRegistryTestData(ctx, t, regAddr, OCITUFTestDataPath)
metadataRepo := regAddr.Host + metadataPath
targetsRepo := regAddr.Host + targetsPath
@@ -62,7 +63,7 @@ func TestRegistryFetcher(t *testing.T) {
cfg.LocalTargetsDir = dir
cfg.RemoteTargetsURL = targetsRepo
cfg.RemoteMetadataURL = metadataRepo
cfg.Fetcher, err = NewRegistryFetcher(cfg)
cfg.Fetcher, err = NewRegistryFetcher(context.Background(), cfg)
require.NoError(t, err)
// create a new Updater instance
@@ -228,7 +229,7 @@ func TestParseImgRef(t *testing.T) {
RemoteMetadataURL: repo,
RemoteTargetsURL: targets,
}
d, err := NewRegistryFetcher(cfg)
d, err := NewRegistryFetcher(context.Background(), cfg)
if tc.expectedConstructorError != "" {
assert.ErrorContains(t, err, tc.expectedConstructorError)
} else {
@@ -272,13 +273,12 @@ func TestGetDataFromLayer(t *testing.T) {
}
func TestPullFileLayer(t *testing.T) {
// run local registry
registry, url := RunTestRegistry(t)
defer func() {
if err := registry.Terminate(context.Background()); err != nil {
t.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()
ctx := context.Background()
regServer := test.NewLocalRegistry(ctx)
defer regServer.Close()
url, err := url.Parse(regServer.URL)
require.NoError(t, err)
// make test layer
repo := tufMetadataRepo
@@ -288,10 +288,13 @@ func TestPullFileLayer(t *testing.T) {
assert.NoError(t, err)
layerRef := fmt.Sprintf("%s/%s@%s", url.Host, repo, hash.String())
// cache test layer
d := &RegistryFetcher{
cache: NewImageCache(),
}
// cache test manifest
d, err := NewRegistryFetcher(context.Background(), &config.UpdaterConfig{
RemoteMetadataURL: tufMetadataRepo,
RemoteTargetsURL: tufMetadataRepo,
})
require.NoError(t, err)
d.cache.Put(layerRef, data)
// push uncached image layer to local registry
@@ -303,7 +306,7 @@ func TestPullFileLayer(t *testing.T) {
img := empty.Image
img, err = mutate.Append(img, mutate.Addendum{Layer: uncachedTestLayer})
assert.NoError(t, err)
err = crane.Push(img, fmt.Sprintf("%s/%s", url.Host, fmt.Sprintf("%s:latest", repo)))
err = crane.Push(img, fmt.Sprintf("%s/%s", url.Host, fmt.Sprintf("%s:latest", repo)), crane.WithUserAgent(useragent.Get(ctx)))
assert.NoError(t, err)
testCases := []struct {
@@ -329,13 +332,12 @@ func TestPullFileLayer(t *testing.T) {
}
func TestGetManifest(t *testing.T) {
// run local registry
registry, url := RunTestRegistry(t)
defer func() {
if err := registry.Terminate(context.Background()); err != nil {
t.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()
ctx := context.Background()
regServer := test.NewLocalRegistry(ctx)
defer regServer.Close()
url, err := url.Parse(regServer.URL)
require.NoError(t, err)
// make test manifest
repo := tufMetadataRepo
@@ -345,16 +347,19 @@ func TestGetManifest(t *testing.T) {
imgRef := fmt.Sprintf("%s/%s:latest", url.Host, repo)
// cache test manifest
d := &RegistryFetcher{
cache: NewImageCache(),
}
d, err := NewRegistryFetcher(context.Background(), &config.UpdaterConfig{
RemoteMetadataURL: tufMetadataRepo,
RemoteTargetsURL: tufMetadataRepo,
})
require.NoError(t, err)
mf, err := img.RawManifest()
assert.NoError(t, err)
d.cache.Put(imgRef, mf)
// push test image to local registry
unchachedImgRef := fmt.Sprintf("%s/%s:unchached", url.Host, repo)
err = crane.Push(img, unchachedImgRef)
err = crane.Push(img, unchachedImgRef, crane.WithUserAgent(useragent.Get(ctx)))
assert.NoError(t, err)
testCases := []struct {
@@ -374,38 +379,21 @@ func TestGetManifest(t *testing.T) {
}
}
// RunTestRegistry starts a registry testcontainer for TUF on OCI testdata.
func RunTestRegistry(t *testing.T) (*registry.RegistryContainer, *url.URL) {
registryContainer, err := registry.Run(context.Background(), "registry:2.8.3")
if err != nil {
t.Fatalf("failed to start container: %s", err)
}
httpAddress, err := registryContainer.Address(context.Background())
if err != nil {
t.Fatalf("failed to get container address: %s", err)
}
addr, err := url.Parse(httpAddress)
if err != nil {
t.Fatalf("failed to parse container address: %s", err)
}
return registryContainer, addr
}
// LoadRegistryTestData pushes TUF metadata and targets to an OCI registry.
func LoadRegistryTestData(t *testing.T, registry *url.URL, path string) {
func LoadRegistryTestData(ctx context.Context, t *testing.T, registry *url.URL, path string) {
// push tuf metadata and targets to local registry
MetadataRepo := tufMetadataRepo
TargetsRepo := "tuf-targets"
DelegatedRole := testRole
// push top-level metadata -> metadata:latest
err := LoadMetadata(filepath.Join(path, "metadata"), registry.Host, MetadataRepo, LatestTag)
err := LoadMetadata(ctx, filepath.Join(path, "metadata"), registry.Host, MetadataRepo, LatestTag)
if err != nil {
t.Fatal(err)
}
// push delegated metadata -> metadata:<DELEGATED_ROLE>
err = LoadMetadata(filepath.Join(path, "metadata", DelegatedRole), registry.Host, MetadataRepo, DelegatedRole)
err = LoadMetadata(ctx, filepath.Join(path, "metadata", DelegatedRole), registry.Host, MetadataRepo, DelegatedRole)
if err != nil {
t.Fatal(err)
}
@@ -438,13 +426,13 @@ func LoadRegistryTestData(t *testing.T, registry *url.URL, path string) {
if err != nil {
t.Fatal(err)
}
err = remote.Write(ref, img, oci.MultiKeychainOption())
err = remote.Write(ref, img, oci.WithOptions(ctx, nil)...)
if err != nil {
t.Fatal(err)
}
case 2:
// delegated target
err = remote.WriteIndex(ref, tIdx, oci.MultiKeychainOption())
err = remote.WriteIndex(ref, tIdx, oci.WithOptions(ctx, nil)...)
if err != nil {
t.Fatal(err)
}
@@ -455,7 +443,7 @@ func LoadRegistryTestData(t *testing.T, registry *url.URL, path string) {
}
// LoadMetadata loads TUF metadata from a local path and pushes to a registry.
func LoadMetadata(path, host, repo, tag string) error {
func LoadMetadata(ctx context.Context, path, host, repo, tag string) error {
mIdx, err := layout.ImageIndexFromPath(path)
if err != nil {
return err
@@ -472,5 +460,5 @@ func LoadMetadata(path, host, repo, tag string) error {
if err != nil {
return err
}
return remote.Write(ref, img, oci.MultiKeychainOption())
return remote.Write(ref, img, oci.WithOptions(ctx, nil)...)
}

View File

@@ -1,6 +1,7 @@
package tuf
import (
"context"
"errors"
"fmt"
"io/fs"
@@ -75,7 +76,7 @@ func NewDockerDefaultClientOptions(tufPath string) *ClientOptions {
}
// NewClient creates a new TUF client.
func NewClient(opts *ClientOptions) (*Client, error) {
func NewClient(ctx context.Context, opts *ClientOptions) (*Client, error) {
var tufSource Source
if strings.HasPrefix(opts.MetadataSource, "https://") || strings.HasPrefix(opts.MetadataSource, "http://") {
tufSource = HTTPSource
@@ -119,7 +120,7 @@ func NewClient(opts *ClientOptions) (*Client, error) {
cfg.RemoteTargetsURL = opts.TargetsSource
if tufSource == OCISource {
cfg.Fetcher, err = NewRegistryFetcher(cfg)
cfg.Fetcher, err = NewRegistryFetcher(ctx, cfg)
if err != nil {
return nil, fmt.Errorf("failed to create registry fetcher: %w", err)
}

View File

@@ -5,10 +5,12 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"
"github.com/docker/attest/internal/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/theupdateframework/go-tuf/v2/metadata"
@@ -43,14 +45,13 @@ func TestRootInit(t *testing.T) {
server := httptest.NewServer(http.FileServer(http.Dir(HTTPTUFTestDataPath)))
defer server.Close()
// run local registry
registry, regAddr := RunTestRegistry(t)
defer func() {
if err := registry.Terminate(context.Background()); err != nil {
t.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()
LoadRegistryTestData(t, regAddr, OCITUFTestDataPath)
ctx := context.Background()
regServer := test.NewLocalRegistry(ctx)
defer regServer.Close()
regAddr, err := url.Parse(regServer.URL)
require.NoError(t, err)
LoadRegistryTestData(ctx, t, regAddr, OCITUFTestDataPath)
alwaysGoodVersionChecker := &MockVersionChecker{err: nil}
alwaysBadVersionChecker := &MockVersionChecker{err: assert.AnError}
@@ -63,19 +64,18 @@ func TestRootInit(t *testing.T) {
{"http", server.URL + "/metadata", server.URL + "/targets"},
{"oci", regAddr.Host + "/tuf-metadata:latest", regAddr.Host + "/tuf-targets"},
}
for _, tc := range testCases {
_, err := NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
_, err := NewClient(ctx, &ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
assert.NoErrorf(t, err, "Failed to create TUF client: %v", err)
// recreation should work with same root
_, err = NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
_, err = NewClient(ctx, &ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
assert.NoErrorf(t, err, "Failed to recreate TUF client: %v", err)
_, err = NewClient(&ClientOptions{[]byte("broken"), tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
_, err = NewClient(ctx, &ClientOptions{[]byte("broken"), tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
assert.Errorf(t, err, "Expected error recreating TUF client with broken root: %v", err)
_, err = NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysBadVersionChecker})
_, err = NewClient(ctx, &ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysBadVersionChecker})
assert.Errorf(t, err, "Expected error recreating TUF client with bad version checker")
}
}
@@ -90,14 +90,13 @@ func TestDownloadTarget(t *testing.T) {
server := httptest.NewServer(http.FileServer(http.Dir(HTTPTUFTestDataPath)))
defer server.Close()
// run local registry
registry, regAddr := RunTestRegistry(t)
defer func() {
if err := registry.Terminate(context.Background()); err != nil {
t.Fatalf("failed to terminate container: %s", err) // nolint:gocritic
}
}()
LoadRegistryTestData(t, regAddr, OCITUFTestDataPath)
ctx := context.Background()
regServer := test.NewLocalRegistry(ctx)
defer regServer.Close()
regAddr, err := url.Parse(regServer.URL)
require.NoError(t, err)
LoadRegistryTestData(ctx, t, regAddr, OCITUFTestDataPath)
alwaysGoodVersionChecker := &MockVersionChecker{err: nil}
@@ -113,7 +112,7 @@ func TestDownloadTarget(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
tufClient, err := NewClient(&ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
tufClient, err := NewClient(ctx, &ClientOptions{DockerTUFRootDev.Data, tufPath, tc.metadataSource, tc.targetsSource, alwaysGoodVersionChecker})
require.NoErrorf(t, err, "Failed to create TUF client: %v", err)
require.NotNil(t, tufClient.updater, "Failed to create updater")

View File

@@ -2,10 +2,10 @@ package tuf
import (
"fmt"
"runtime/debug"
"strings"
"github.com/Masterminds/semver/v3"
"github.com/docker/attest/internal/version"
)
const ThisModulePath = "github.com/docker/attest"
@@ -39,32 +39,13 @@ func NewDefaultVersionChecker() *DefaultVersionChecker {
type DefaultVersionChecker struct{}
func (vc *DefaultVersionChecker) CheckVersion(client Downloader) error {
var attestMod *debug.Module
bi, ok := debug.ReadBuildInfo()
if !ok {
// if we can't read the build info, assume we're good. this should only happen if we're not running in a module
return nil
}
if bi.Main.Path == ThisModulePath {
attestMod = &bi.Main
} else {
for _, dep := range bi.Deps {
if dep.Path == ThisModulePath {
attestMod = dep
break
}
}
}
if attestMod == nil {
// if we can't find the attest dep, assume we're good. this should only happen in a test
return nil
}
attestVersion, err := semver.NewVersion(attestMod.Version)
attestVersion, err := version.Get()
if err != nil {
return fmt.Errorf("failed to parse version %s: %w", attestMod.Version, err)
return fmt.Errorf("failed to get version: %w", err)
}
if attestVersion == nil {
return nil
}
// see https://github.com/Masterminds/semver/blob/v3.2.1/README.md#checking-version-constraints
// for more information on the expected format of the version constraints in the TUF repo
target, err := client.DownloadTarget("version-constraints", "")