Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add HTTP support for downloading DBs #7892

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions internal/dbtest/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import (
"github.com/samber/lo"
"github.com/stretchr/testify/require"

"github.com/aquasecurity/trivy/pkg/asset"
ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/oci"
)

const defaultMediaType = "application/vnd.aquasec.trivy.db.layer.v1.tar+gzip"
Expand All @@ -38,7 +38,7 @@ type FakeDBOptions struct {
MediaType types.MediaType
}

func NewFakeDB(t *testing.T, dbPath string, opts FakeDBOptions) *oci.Artifact {
func NewFakeDB(t *testing.T, dbPath string, opts FakeDBOptions) *asset.OCI {
mediaType := lo.Ternary(opts.MediaType != "", opts.MediaType, defaultMediaType)
img := new(fakei.FakeImage)
img.LayersReturns([]v1.Layer{NewFakeLayer(t, dbPath, mediaType)}, nil)
Expand All @@ -59,10 +59,13 @@ func NewFakeDB(t *testing.T, dbPath string, opts FakeDBOptions) *oci.Artifact {
}, nil)

// Mock OCI artifact
opt := ftypes.RegistryOptions{
Insecure: false,
assetOpts := asset.Options{
MediaType: defaultMediaType,
RegistryOptions: ftypes.RegistryOptions{
Insecure: false,
},
}
return oci.NewArtifact("dummy", opt, oci.WithImage(img))
return asset.NewOCI("dummy", assetOpts, asset.WithImage(img))
}

func ArchiveDir(t *testing.T, dir string) string {
Expand Down
86 changes: 86 additions & 0 deletions pkg/asset/asset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package asset

import (
"context"
"strings"

"github.com/google/go-containerregistry/pkg/v1/remote/transport"
"github.com/hashicorp/go-multierror"
"golang.org/x/xerrors"

"github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/version/doc"
)

type Options struct {
// For OCI
MediaType string // Accept any media type if not specified

// Common
Filename string // Use the annotation if not specified
Quiet bool

types.RegistryOptions
}

type Assets []Asset

type Asset interface {
Location() string
Download(ctx context.Context, dst string) error
ShouldTryOtherRepo(err error) bool
}

func NewAssets(locations []string, assetOpts Options, opts ...Option) Assets {
var assets Assets
for _, location := range locations {
switch {
case strings.HasPrefix(location, "https://"), strings.HasPrefix(location, "http://"):
assets = append(assets, NewHTTP(location, assetOpts))
default:
assets = append(assets, NewOCI(location, assetOpts, opts...))
}
}
return assets
}

// Download downloads artifacts until one of them succeeds.
// Attempts to download next artifact if the first one fails due to a temporary error.
func (a Assets) Download(ctx context.Context, dst string) error {
var errs error
for i, art := range a {
logger := log.With("location", art.Location())
logger.InfoContext(ctx, "Downloading artifact...")
err := art.Download(ctx, dst)
if err == nil {
logger.InfoContext(ctx, "OCI successfully downloaded")
return nil
}

if !art.ShouldTryOtherRepo(err) {
return xerrors.Errorf("failed to download artifact from %s: %w", art.Location(), err)
}
logger.ErrorContext(ctx, "Failed to download artifact", log.Err(err))
if i < len(a)-1 {
log.InfoContext(ctx, "Trying to download artifact from other location...") // Use the default logger
}
errs = multierror.Append(errs, err)
}

return xerrors.Errorf("failed to download artifact from any source: %w", errs)
}

func shouldTryOtherRepo(terr *transport.Error) bool {
for _, diagnostic := range terr.Errors {
// For better user experience
if diagnostic.Code == transport.DeniedErrorCode || diagnostic.Code == transport.UnauthorizedErrorCode {
// e.g. https://aquasecurity.github.io/trivy/latest/docs/references/troubleshooting/#db
log.Warnf("See %s", doc.URL("/docs/references/troubleshooting/", "db"))
break
}
}

// try the following artifact only if a temporary error occurs
return terr.Temporary()
}
Loading