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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)...)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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", "")
|
||||
|
||||
Reference in New Issue
Block a user