Skip to content

Commit 4316bcb

Browse files
feat: add support for registry mirrors (#8244)
Signed-off-by: knqyf263 <knqyf263@gmail.com> Co-authored-by: Teppei Fukuda <knqyf263@gmail.com>
1 parent 2acd8e3 commit 4316bcb

File tree

8 files changed

+260
-24
lines changed

8 files changed

+260
-24
lines changed

docs/docs/configuration/others.md

+43
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,46 @@ The following example will fail when a critical vulnerability is found or the OS
117117
```
118118
$ trivy image --exit-code 1 --exit-on-eol 1 --severity CRITICAL alpine:3.16.3
119119
```
120+
121+
## Mirror Registries
122+
123+
!!! warning "EXPERIMENTAL"
124+
This feature might change without preserving backwards compatibility.
125+
126+
Trivy supports mirrors for [remote container images](../target/container_image.md#container-registry) and [databases](./db.md).
127+
128+
To configure them, add a list of mirrors along with the host to the [trivy config file](../references/configuration/config-file.md#registry-options).
129+
130+
!!! note
131+
Use the `index.docker.io` host for images from `Docker Hub`, even if you don't use that prefix.
132+
133+
Example for `index.docker.io`:
134+
```yaml
135+
registry:
136+
mirrors:
137+
index.docker.io:
138+
- mirror.gcr.io
139+
```
140+
141+
### Registry check procedure
142+
Trivy uses the following registry order to get the image:
143+
144+
- mirrors in the same order as they are specified in the configuration file
145+
- source registry
146+
147+
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).
148+
149+
Example:
150+
```yaml
151+
registry:
152+
mirrors:
153+
index.docker.io:
154+
- mirror.with.bad.auth // We don't have credentials for this registry
155+
- mirror.without.image // Registry doesn't have this image
156+
```
157+
158+
When we want to get the image `alpine` with the settings above. The logic will be as follows:
159+
160+
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.
161+
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).
162+
3. Get the image from `index.docker.io` (the original registry).

docs/docs/references/configuration/config-file.md

+2
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,8 @@ pkg:
461461

462462
```yaml
463463
registry:
464+
mirrors:
465+
464466
# Same as '--password'
465467
password: []
466468

magefiles/docs.go

+8
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,14 @@ func writeFlagValue(val any, ind string, w *os.File) {
147147
} else {
148148
w.WriteString(" []\n")
149149
}
150+
case map[string][]string:
151+
w.WriteString("\n")
152+
for k, vv := range v {
153+
fmt.Fprintf(w, "%s %s:\n", ind, k)
154+
for _, vvv := range vv {
155+
fmt.Fprintf(w, " %s - %s\n", ind, vvv)
156+
}
157+
}
150158
case string:
151159
fmt.Fprintf(w, " %q\n", v)
152160
default:

pkg/fanal/types/image.go

+3
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ type RegistryOptions struct {
8181
// RegistryToken is a bearer token to be sent to a registry
8282
RegistryToken string
8383

84+
// RegistryMirrors is a map of hosts with mirrors for them
85+
RegistryMirrors map[string][]string
86+
8487
// SSL/TLS
8588
Insecure bool
8689

pkg/flag/options.go

+9-6
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import (
3030
)
3131

3232
type FlagType interface {
33-
int | string | []string | bool | time.Duration | float64
33+
int | string | []string | bool | time.Duration | float64 | map[string][]string
3434
}
3535

3636
type Flag[T FlagType] struct {
@@ -161,6 +161,8 @@ func (f *Flag[T]) cast(val any) any {
161161
return cast.ToFloat64(val)
162162
case time.Duration:
163163
return cast.ToDuration(val)
164+
case map[string][]string:
165+
return cast.ToStringMapStringSlice(val)
164166
case []string:
165167
if s, ok := val.(string); ok && strings.Contains(s, ",") {
166168
// Split environmental variables by comma as it is not done by viper.
@@ -467,11 +469,12 @@ func (o *Options) ScanOpts() types.ScanOptions {
467469
// RegistryOpts returns options for OCI registries
468470
func (o *Options) RegistryOpts() ftypes.RegistryOptions {
469471
return ftypes.RegistryOptions{
470-
Credentials: o.Credentials,
471-
RegistryToken: o.RegistryToken,
472-
Insecure: o.Insecure,
473-
Platform: o.Platform,
474-
AWSRegion: o.AWSOptions.Region,
472+
Credentials: o.Credentials,
473+
RegistryToken: o.RegistryToken,
474+
Insecure: o.Insecure,
475+
Platform: o.Platform,
476+
AWSRegion: o.AWSOptions.Region,
477+
RegistryMirrors: o.RegistryMirrors,
475478
}
476479
}
477480

pkg/flag/registry_flags.go

+21-12
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,33 @@ var (
3131
ConfigName: "registry.token",
3232
Usage: "registry token",
3333
}
34+
RegistryMirrorsFlag = Flag[map[string][]string]{
35+
ConfigName: "registry.mirrors",
36+
Usage: "map of hosts and registries for them.",
37+
}
3438
)
3539

3640
type RegistryFlagGroup struct {
37-
Username *Flag[[]string]
38-
Password *Flag[[]string]
39-
PasswordStdin *Flag[bool]
40-
RegistryToken *Flag[string]
41+
Username *Flag[[]string]
42+
Password *Flag[[]string]
43+
PasswordStdin *Flag[bool]
44+
RegistryToken *Flag[string]
45+
RegistryMirrors *Flag[map[string][]string]
4146
}
4247

4348
type RegistryOptions struct {
44-
Credentials []types.Credential
45-
RegistryToken string
49+
Credentials []types.Credential
50+
RegistryToken string
51+
RegistryMirrors map[string][]string
4652
}
4753

4854
func NewRegistryFlagGroup() *RegistryFlagGroup {
4955
return &RegistryFlagGroup{
50-
Username: UsernameFlag.Clone(),
51-
Password: PasswordFlag.Clone(),
52-
PasswordStdin: PasswordStdinFlag.Clone(),
53-
RegistryToken: RegistryTokenFlag.Clone(),
56+
Username: UsernameFlag.Clone(),
57+
Password: PasswordFlag.Clone(),
58+
PasswordStdin: PasswordStdinFlag.Clone(),
59+
RegistryToken: RegistryTokenFlag.Clone(),
60+
RegistryMirrors: RegistryMirrorsFlag.Clone(),
5461
}
5562
}
5663

@@ -64,6 +71,7 @@ func (f *RegistryFlagGroup) Flags() []Flagger {
6471
f.Password,
6572
f.PasswordStdin,
6673
f.RegistryToken,
74+
f.RegistryMirrors,
6775
}
6876
}
6977

@@ -97,7 +105,8 @@ func (f *RegistryFlagGroup) ToOptions() (RegistryOptions, error) {
97105
}
98106

99107
return RegistryOptions{
100-
Credentials: credentials,
101-
RegistryToken: f.RegistryToken.Value(),
108+
Credentials: credentials,
109+
RegistryToken: f.RegistryToken.Value(),
110+
RegistryMirrors: f.RegistryMirrors.Value(),
102111
}, nil
103112
}

pkg/remote/remote.go

+77-6
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package remote
33
import (
44
"context"
55
"crypto/tls"
6+
"errors"
67
"fmt"
78
"net"
89
"net/http"
10+
"strings"
911
"time"
1012

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

40+
return tryWithMirrors(ref, option, func(r name.Reference) (*Descriptor, error) {
41+
return tryGet(ctx, tr, r, option)
42+
})
43+
}
44+
45+
// tryGet checks all auth options and tries to get Descriptor.
46+
func tryGet(ctx context.Context, tr http.RoundTripper, ref name.Reference, option types.RegistryOptions) (*Descriptor, error) {
3847
var errs error
39-
// Try each authentication method until it succeeds
4048
for _, authOpt := range authOptions(ctx, ref, option) {
4149
remoteOpts := []remote.Option{
4250
remote.WithTransport(tr),
@@ -67,8 +75,6 @@ func Get(ctx context.Context, ref name.Reference, option types.RegistryOptions)
6775
}
6876
return desc, nil
6977
}
70-
71-
// No authentication succeeded
7278
return nil, errs
7379
}
7480

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

89+
return tryWithMirrors(ref, option, func(r name.Reference) (v1.Image, error) {
90+
return tryImage(ctx, tr, r, option)
91+
})
92+
}
93+
94+
// tryWithMirrors handles common mirror logic for Get and Image functions
95+
func tryWithMirrors[T any](ref name.Reference, option types.RegistryOptions, fn func(name.Reference) (T, error)) (T, error) {
96+
var zero T
97+
mirrors, err := registryMirrors(ref, option)
98+
if err != nil {
99+
return zero, xerrors.Errorf("unable to parse mirrors: %w", err)
100+
}
101+
102+
// Try each mirrors/host until it succeeds
103+
var errs error
104+
for _, r := range append(mirrors, ref) {
105+
result, err := fn(r)
106+
if err != nil {
107+
var multiErr *multierror.Error
108+
// All auth options failed, try the next mirror/host
109+
if errors.As(err, &multiErr) {
110+
errs = multierror.Append(errs, multiErr.Errors...)
111+
continue
112+
}
113+
// Other errors
114+
return zero, err
115+
}
116+
117+
if ref.Context().RegistryStr() != r.Context().RegistryStr() {
118+
log.WithPrefix("remote").Info("Using the mirror registry to get the image",
119+
log.String("image", ref.String()), log.String("mirror", r.Context().RegistryStr()))
120+
}
121+
return result, nil
122+
}
123+
124+
// No authentication for mirrors/host succeeded
125+
return zero, errs
126+
}
127+
128+
// tryImage checks all auth options and tries to get v1.Image.
129+
// If none of the auth options work - function returns multierrors for each auth option.
130+
func tryImage(ctx context.Context, tr http.RoundTripper, ref name.Reference, option types.RegistryOptions) (v1.Image, error) {
83131
var errs error
84-
// Try each authentication method until it succeeds
85132
for _, authOpt := range authOptions(ctx, ref, option) {
86133
remoteOpts := []remote.Option{
87134
remote.WithTransport(tr),
@@ -92,10 +139,9 @@ func Image(ctx context.Context, ref name.Reference, option types.RegistryOptions
92139
errs = multierror.Append(errs, err)
93140
continue
94141
}
142+
95143
return index, nil
96144
}
97-
98-
// No authentication succeeded
99145
return nil, errs
100146
}
101147

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

175+
// registryMirrors returns a list of mirrors for ref, obtained from options.RegistryMirrors
176+
// `go-containerregistry` doesn't support mirrors, so we need to handle them ourselves.
177+
// TODO: use `WithMirror` when `go-containerregistry` will support mirrors.
178+
// cf. https://github.com/google/go-containerregistry/pull/2010
179+
func registryMirrors(hostRef name.Reference, option types.RegistryOptions) ([]name.Reference, error) {
180+
var mirrors []name.Reference
181+
182+
reg := hostRef.Context().RegistryStr()
183+
if ms, ok := option.RegistryMirrors[reg]; ok {
184+
for _, m := range ms {
185+
var nameOpts []name.Option
186+
if option.Insecure {
187+
nameOpts = append(nameOpts, name.Insecure)
188+
}
189+
mirrorImageName := strings.Replace(hostRef.Name(), reg, m, 1)
190+
ref, err := name.ParseReference(mirrorImageName, nameOpts...)
191+
if err != nil {
192+
return nil, xerrors.Errorf("unable to parse image from mirror registry: %w", err)
193+
}
194+
mirrors = append(mirrors, ref)
195+
}
196+
}
197+
return mirrors, nil
198+
}
199+
129200
func httpTransport(option types.RegistryOptions) (http.RoundTripper, error) {
130201
d := &net.Dialer{
131202
Timeout: 10 * time.Minute,

0 commit comments

Comments
 (0)