Skip to content

Commit

Permalink
feat: add support for registry mirrors (#8244)
Browse files Browse the repository at this point in the history
Signed-off-by: knqyf263 <knqyf263@gmail.com>
Co-authored-by: Teppei Fukuda <knqyf263@gmail.com>
  • Loading branch information
DmitriyLewen and knqyf263 authored Jan 22, 2025
1 parent 2acd8e3 commit 4316bcb
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 24 deletions.
43 changes: 43 additions & 0 deletions docs/docs/configuration/others.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,46 @@ The following example will fail when a critical vulnerability is found or the OS
```
$ trivy image --exit-code 1 --exit-on-eol 1 --severity CRITICAL alpine:3.16.3
```

## Mirror Registries

!!! warning "EXPERIMENTAL"
This feature might change without preserving backwards compatibility.

Trivy supports mirrors for [remote container images](../target/container_image.md#container-registry) and [databases](./db.md).

To configure them, add a list of mirrors along with the host to the [trivy config file](../references/configuration/config-file.md#registry-options).

!!! note
Use the `index.docker.io` host for images from `Docker Hub`, even if you don't use that prefix.

Example for `index.docker.io`:
```yaml
registry:
mirrors:
index.docker.io:
- mirror.gcr.io
```
### Registry check procedure
Trivy uses the following registry order to get the image:
- mirrors in the same order as they are specified in the configuration file
- source registry
In cases where we can't get the image from the mirror registry (e.g. when authentication fails, image doesn't exist, etc.) - Trivy will check other mirrors (or the source registry if all mirrors have already been checked).
Example:
```yaml
registry:
mirrors:
index.docker.io:
- mirror.with.bad.auth // We don't have credentials for this registry
- mirror.without.image // Registry doesn't have this image
```
When we want to get the image `alpine` with the settings above. The logic will be as follows:

1. Try to get the image from `mirror.with.bad.auth/library/alpine`, but we get an error because there are no credentials for this registry.
2. Try to get the image from `mirror.without.image/library/alpine`, but we get an error because this registry doesn't have this image (but most likely it will be an error about authorization).
3. Get the image from `index.docker.io` (the original registry).
2 changes: 2 additions & 0 deletions docs/docs/references/configuration/config-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,8 @@ pkg:

```yaml
registry:
mirrors:

# Same as '--password'
password: []

Expand Down
8 changes: 8 additions & 0 deletions magefiles/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ func writeFlagValue(val any, ind string, w *os.File) {
} else {
w.WriteString(" []\n")
}
case map[string][]string:
w.WriteString("\n")
for k, vv := range v {
fmt.Fprintf(w, "%s %s:\n", ind, k)
for _, vvv := range vv {
fmt.Fprintf(w, " %s - %s\n", ind, vvv)
}
}
case string:
fmt.Fprintf(w, " %q\n", v)
default:
Expand Down
3 changes: 3 additions & 0 deletions pkg/fanal/types/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ type RegistryOptions struct {
// RegistryToken is a bearer token to be sent to a registry
RegistryToken string

// RegistryMirrors is a map of hosts with mirrors for them
RegistryMirrors map[string][]string

// SSL/TLS
Insecure bool

Expand Down
15 changes: 9 additions & 6 deletions pkg/flag/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import (
)

type FlagType interface {
int | string | []string | bool | time.Duration | float64
int | string | []string | bool | time.Duration | float64 | map[string][]string
}

type Flag[T FlagType] struct {
Expand Down Expand Up @@ -161,6 +161,8 @@ func (f *Flag[T]) cast(val any) any {
return cast.ToFloat64(val)
case time.Duration:
return cast.ToDuration(val)
case map[string][]string:
return cast.ToStringMapStringSlice(val)
case []string:
if s, ok := val.(string); ok && strings.Contains(s, ",") {
// Split environmental variables by comma as it is not done by viper.
Expand Down Expand Up @@ -467,11 +469,12 @@ func (o *Options) ScanOpts() types.ScanOptions {
// RegistryOpts returns options for OCI registries
func (o *Options) RegistryOpts() ftypes.RegistryOptions {
return ftypes.RegistryOptions{
Credentials: o.Credentials,
RegistryToken: o.RegistryToken,
Insecure: o.Insecure,
Platform: o.Platform,
AWSRegion: o.AWSOptions.Region,
Credentials: o.Credentials,
RegistryToken: o.RegistryToken,
Insecure: o.Insecure,
Platform: o.Platform,
AWSRegion: o.AWSOptions.Region,
RegistryMirrors: o.RegistryMirrors,
}
}

Expand Down
33 changes: 21 additions & 12 deletions pkg/flag/registry_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,33 @@ var (
ConfigName: "registry.token",
Usage: "registry token",
}
RegistryMirrorsFlag = Flag[map[string][]string]{
ConfigName: "registry.mirrors",
Usage: "map of hosts and registries for them.",
}
)

type RegistryFlagGroup struct {
Username *Flag[[]string]
Password *Flag[[]string]
PasswordStdin *Flag[bool]
RegistryToken *Flag[string]
Username *Flag[[]string]
Password *Flag[[]string]
PasswordStdin *Flag[bool]
RegistryToken *Flag[string]
RegistryMirrors *Flag[map[string][]string]
}

type RegistryOptions struct {
Credentials []types.Credential
RegistryToken string
Credentials []types.Credential
RegistryToken string
RegistryMirrors map[string][]string
}

func NewRegistryFlagGroup() *RegistryFlagGroup {
return &RegistryFlagGroup{
Username: UsernameFlag.Clone(),
Password: PasswordFlag.Clone(),
PasswordStdin: PasswordStdinFlag.Clone(),
RegistryToken: RegistryTokenFlag.Clone(),
Username: UsernameFlag.Clone(),
Password: PasswordFlag.Clone(),
PasswordStdin: PasswordStdinFlag.Clone(),
RegistryToken: RegistryTokenFlag.Clone(),
RegistryMirrors: RegistryMirrorsFlag.Clone(),
}
}

Expand All @@ -64,6 +71,7 @@ func (f *RegistryFlagGroup) Flags() []Flagger {
f.Password,
f.PasswordStdin,
f.RegistryToken,
f.RegistryMirrors,
}
}

Expand Down Expand Up @@ -97,7 +105,8 @@ func (f *RegistryFlagGroup) ToOptions() (RegistryOptions, error) {
}

return RegistryOptions{
Credentials: credentials,
RegistryToken: f.RegistryToken.Value(),
Credentials: credentials,
RegistryToken: f.RegistryToken.Value(),
RegistryMirrors: f.RegistryMirrors.Value(),
}, nil
}
83 changes: 77 additions & 6 deletions pkg/remote/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package remote
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"net/http"
"strings"
"time"

"github.com/google/go-containerregistry/pkg/authn"
Expand Down Expand Up @@ -35,8 +37,14 @@ func Get(ctx context.Context, ref name.Reference, option types.RegistryOptions)
return nil, xerrors.Errorf("failed to create http transport: %w", err)
}

return tryWithMirrors(ref, option, func(r name.Reference) (*Descriptor, error) {
return tryGet(ctx, tr, r, option)
})
}

// tryGet checks all auth options and tries to get Descriptor.
func tryGet(ctx context.Context, tr http.RoundTripper, ref name.Reference, option types.RegistryOptions) (*Descriptor, error) {
var errs error
// Try each authentication method until it succeeds
for _, authOpt := range authOptions(ctx, ref, option) {
remoteOpts := []remote.Option{
remote.WithTransport(tr),
Expand Down Expand Up @@ -67,8 +75,6 @@ func Get(ctx context.Context, ref name.Reference, option types.RegistryOptions)
}
return desc, nil
}

// No authentication succeeded
return nil, errs
}

Expand All @@ -80,8 +86,49 @@ func Image(ctx context.Context, ref name.Reference, option types.RegistryOptions
return nil, xerrors.Errorf("failed to create http transport: %w", err)
}

return tryWithMirrors(ref, option, func(r name.Reference) (v1.Image, error) {
return tryImage(ctx, tr, r, option)
})
}

// tryWithMirrors handles common mirror logic for Get and Image functions
func tryWithMirrors[T any](ref name.Reference, option types.RegistryOptions, fn func(name.Reference) (T, error)) (T, error) {
var zero T
mirrors, err := registryMirrors(ref, option)
if err != nil {
return zero, xerrors.Errorf("unable to parse mirrors: %w", err)
}

// Try each mirrors/host until it succeeds
var errs error
for _, r := range append(mirrors, ref) {
result, err := fn(r)
if err != nil {
var multiErr *multierror.Error
// All auth options failed, try the next mirror/host
if errors.As(err, &multiErr) {
errs = multierror.Append(errs, multiErr.Errors...)
continue
}
// Other errors
return zero, err
}

if ref.Context().RegistryStr() != r.Context().RegistryStr() {
log.WithPrefix("remote").Info("Using the mirror registry to get the image",
log.String("image", ref.String()), log.String("mirror", r.Context().RegistryStr()))
}
return result, nil
}

// No authentication for mirrors/host succeeded
return zero, errs
}

// tryImage checks all auth options and tries to get v1.Image.
// If none of the auth options work - function returns multierrors for each auth option.
func tryImage(ctx context.Context, tr http.RoundTripper, ref name.Reference, option types.RegistryOptions) (v1.Image, error) {
var errs error
// Try each authentication method until it succeeds
for _, authOpt := range authOptions(ctx, ref, option) {
remoteOpts := []remote.Option{
remote.WithTransport(tr),
Expand All @@ -92,10 +139,9 @@ func Image(ctx context.Context, ref name.Reference, option types.RegistryOptions
errs = multierror.Append(errs, err)
continue
}

return index, nil
}

// No authentication succeeded
return nil, errs
}

Expand Down Expand Up @@ -126,6 +172,31 @@ func Referrers(ctx context.Context, d name.Digest, option types.RegistryOptions)
return nil, errs
}

// registryMirrors returns a list of mirrors for ref, obtained from options.RegistryMirrors
// `go-containerregistry` doesn't support mirrors, so we need to handle them ourselves.
// TODO: use `WithMirror` when `go-containerregistry` will support mirrors.
// cf. https://github.com/google/go-containerregistry/pull/2010
func registryMirrors(hostRef name.Reference, option types.RegistryOptions) ([]name.Reference, error) {
var mirrors []name.Reference

reg := hostRef.Context().RegistryStr()
if ms, ok := option.RegistryMirrors[reg]; ok {
for _, m := range ms {
var nameOpts []name.Option
if option.Insecure {
nameOpts = append(nameOpts, name.Insecure)
}
mirrorImageName := strings.Replace(hostRef.Name(), reg, m, 1)
ref, err := name.ParseReference(mirrorImageName, nameOpts...)
if err != nil {
return nil, xerrors.Errorf("unable to parse image from mirror registry: %w", err)
}
mirrors = append(mirrors, ref)
}
}
return mirrors, nil
}

func httpTransport(option types.RegistryOptions) (http.RoundTripper, error) {
d := &net.Dialer{
Timeout: 10 * time.Minute,
Expand Down
Loading

0 comments on commit 4316bcb

Please sign in to comment.