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

refactor: use google/wire for cache #7024

Merged
merged 6 commits into from
Jun 27, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion pkg/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
)

const (
cacheDirName = "fanal"
scanCacheDirName = "fanal"

// artifactBucket stores artifact information with artifact ID such as image ID
artifactBucket = "artifact"
Expand Down
146 changes: 33 additions & 113 deletions pkg/cache/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,148 +3,68 @@ package cache
import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"strings"
"time"

"github.com/go-redis/redis/v8"
"github.com/samber/lo"
"golang.org/x/xerrors"

"github.com/aquasecurity/trivy/pkg/log"
)

const (
TypeFS Type = "fs"
TypeRedis Type = "redis"
TypeUnknown Type = "unknown"
TypeFS Type = "fs"
TypeRedis Type = "redis"
)

type Type string

type Options struct {
Type Type
TTL time.Duration
Redis RedisOptions
}

func NewOptions(backend, redisCACert, redisCert, redisKey string, redisTLS bool, ttl time.Duration) (Options, error) {
t, err := NewType(backend)
if err != nil {
return Options{}, xerrors.Errorf("cache type error: %w", err)
}

var redisOpts RedisOptions
if t == TypeRedis {
redisTLSOpts, err := NewRedisTLSOptions(redisCACert, redisCert, redisKey)
if err != nil {
return Options{}, xerrors.Errorf("redis TLS option error: %w", err)
}
redisOpts = RedisOptions{
Backend: backend,
TLS: redisTLS,
TLSOptions: redisTLSOpts,
}
} else if ttl != 0 {
log.Warn("'--cache-ttl' is only available with Redis cache backend")
}

return Options{
Type: t,
TTL: ttl,
Redis: redisOpts,
}, nil
}

type RedisOptions struct {
Backend string
TLS bool
TLSOptions RedisTLSOptions
}

// BackendMasked returns the redis connection string masking credentials
func (o *RedisOptions) BackendMasked() string {
endIndex := strings.Index(o.Backend, "@")
if endIndex == -1 {
return o.Backend
}

startIndex := strings.Index(o.Backend, "//")

return fmt.Sprintf("%s****%s", o.Backend[:startIndex+2], o.Backend[endIndex:])
}

// RedisTLSOptions holds the options for redis cache
type RedisTLSOptions struct {
CACert string
Cert string
Key string
}

func NewRedisTLSOptions(caCert, cert, key string) (RedisTLSOptions, error) {
opts := RedisTLSOptions{
CACert: caCert,
Cert: cert,
Key: key,
}

// If one of redis option not nil, make sure CA, cert, and key provided
if !lo.IsEmpty(opts) {
if opts.CACert == "" || opts.Cert == "" || opts.Key == "" {
return RedisTLSOptions{}, xerrors.Errorf("you must provide Redis CA, cert and key file path when using TLS")
}
}
return opts, nil
Backend string
CacheDir string
RedisCACert string
RedisCert string
RedisKey string
RedisTLS bool
TTL time.Duration
}

func NewType(backend string) (Type, error) {
func NewType(backend string) Type {
// "redis://" or "fs" are allowed for now
// An empty value is also allowed for testability
switch {
case strings.HasPrefix(backend, "redis://"):
return TypeRedis, nil
return TypeRedis
case backend == "fs", backend == "":
return TypeFS, nil
return TypeFS
default:
return "", xerrors.Errorf("unknown cache backend: %s", backend)
return TypeUnknown
}
}

// New returns a new cache client
func New(dir string, opts Options) (Cache, error) {
if opts.Type == TypeRedis {
log.Info("Redis cache", log.String("url", opts.Redis.BackendMasked()))
options, err := redis.ParseURL(opts.Redis.Backend)
func New(opts Options) (Cache, func(), error) {
cleanup := func() {} // To avoid panic

var cache Cache
t := NewType(opts.Backend)
switch t {
case TypeRedis:
redisCache, err := NewRedisCache(opts.Backend, opts.RedisCACert, opts.RedisCert, opts.RedisKey, opts.RedisTLS, opts.TTL)
if err != nil {
return nil, err
return nil, cleanup, xerrors.Errorf("unable to initialize redis cache: %w", err)
}

if tlsOpts := opts.Redis.TLSOptions; !lo.IsEmpty(tlsOpts) {
caCert, cert, err := GetTLSConfig(tlsOpts.CACert, tlsOpts.Cert, tlsOpts.Key)
if err != nil {
return nil, err
}

options.TLSConfig = &tls.Config{
RootCAs: caCert,
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
}
} else if opts.Redis.TLS {
options.TLSConfig = &tls.Config{
MinVersion: tls.VersionTLS12,
}
cache = redisCache
case TypeFS:
// standalone mode
fsCache, err := NewFSCache(opts.CacheDir)
if err != nil {
return nil, cleanup, xerrors.Errorf("unable to initialize fs cache: %w", err)
}

return NewRedisCache(options, opts.TTL), nil
}

// standalone mode
fsCache, err := NewFSCache(dir)
if err != nil {
return nil, xerrors.Errorf("unable to initialize fs cache: %w", err)
cache = fsCache
default:
return nil, cleanup, xerrors.Errorf("unknown cache type: %s", t)
}
return fsCache, nil
return cache, func() { _ = cache.Close() }, nil
}

// GetTLSConfig gets tls config from CA, Cert and Key file
DmitriyLewen marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
150 changes: 71 additions & 79 deletions pkg/cache/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,128 +2,120 @@ package cache_test

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/aquasecurity/trivy/pkg/cache"
)

func TestNewOptions(t *testing.T) {
type args struct {
backend string
redisCACert string
redisCert string
redisKey string
redisTLS bool
ttl time.Duration
}
func TestNew(t *testing.T) {
tests := []struct {
name string
args args
want cache.Options
assertion require.ErrorAssertionFunc
name string
opts cache.Options
wantType any
wantErr string
}{
{
name: "fs",
args: args{backend: "fs"},
want: cache.Options{Type: cache.TypeFS},
assertion: require.NoError,
name: "fs backend",
opts: cache.Options{
Backend: "fs",
CacheDir: "/tmp/cache",
},
wantType: cache.FSCache{},
},
{
name: "redis",
args: args{backend: "redis://localhost:6379"},
want: cache.Options{
Type: cache.TypeRedis,
Redis: cache.RedisOptions{Backend: "redis://localhost:6379"},
name: "redis backend",
opts: cache.Options{
Backend: "redis://localhost:6379",
},
assertion: require.NoError,
wantType: cache.RedisCache{},
},
{
name: "redis tls",
args: args{
backend: "redis://localhost:6379",
redisCACert: "ca-cert.pem",
redisCert: "cert.pem",
redisKey: "key.pem",
},
want: cache.Options{
Type: cache.TypeRedis,
Redis: cache.RedisOptions{
Backend: "redis://localhost:6379",
TLSOptions: cache.RedisTLSOptions{
CACert: "ca-cert.pem",
Cert: "cert.pem",
Key: "key.pem",
},
},
name: "unknown backend",
opts: cache.Options{
Backend: "unknown",
},
assertion: require.NoError,
wantErr: "unknown cache type",
},
{
name: "redis tls with public certificates",
args: args{
backend: "redis://localhost:6379",
redisTLS: true,
name: "invalid redis URL",
opts: cache.Options{
Backend: "redis://invalid-url:foo/bar",
},
want: cache.Options{
Type: cache.TypeRedis,
Redis: cache.RedisOptions{
Backend: "redis://localhost:6379",
TLS: true,
},
},
assertion: require.NoError,
wantErr: "failed to parse Redis URL",
},
{
name: "unknown backend",
args: args{backend: "unknown"},
assertion: func(t require.TestingT, err error, msgs ...any) {
require.ErrorContains(t, err, "unknown cache backend")
name: "incomplete TLS options",
opts: cache.Options{
Backend: "redis://localhost:6379",
RedisCACert: "testdata/ca-cert.pem",
RedisTLS: true,
},
wantErr: "you must provide Redis CA, cert and key file path when using TLS",
},
{
name: "sad redis tls",
args: args{
backend: "redis://localhost:6379",
redisCACert: "ca-cert.pem",
},
assertion: func(t require.TestingT, err error, msgs ...any) {
require.ErrorContains(t, err, "you must provide Redis CA, cert and key file path when using TLS")
name: "invalid TLS file paths",
opts: cache.Options{
Backend: "redis://localhost:6379",
RedisCACert: "testdata/non-existent-ca-cert.pem",
RedisCert: "testdata/non-existent-cert.pem",
RedisKey: "testdata/non-existent-key.pem",
RedisTLS: true,
},
wantErr: "failed to get TLS config",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := cache.NewOptions(tt.args.backend, tt.args.redisCACert, tt.args.redisCert, tt.args.redisKey, tt.args.redisTLS, tt.args.ttl)
tt.assertion(t, err)
assert.Equal(t, tt.want, got)
c, cleanup, err := cache.New(tt.opts)
defer cleanup()

if tt.wantErr != "" {
assert.ErrorContains(t, err, tt.wantErr)
return
}

require.NoError(t, err)
assert.NotNil(t, c)
assert.IsType(t, tt.wantType, c)
})
}
}

func TestRedisOptions_BackendMasked(t *testing.T) {
func TestNewType(t *testing.T) {
tests := []struct {
name string
fields cache.RedisOptions
want string
name string
backend string
wantType cache.Type
}{
{
name: "redis cache backend masked",
fields: cache.RedisOptions{Backend: "redis://root:password@localhost:6379"},
want: "redis://****@localhost:6379",
name: "redis backend",
backend: "redis://localhost:6379",
wantType: cache.TypeRedis,
},
{
name: "fs backend",
backend: "fs",
wantType: cache.TypeFS,
},
{
name: "redis cache backend masked does nothing",
fields: cache.RedisOptions{Backend: "redis://localhost:6379"},
want: "redis://localhost:6379",
name: "empty backend",
backend: "",
wantType: cache.TypeFS,
},
{
name: "unknown backend",
backend: "unknown",
wantType: cache.TypeUnknown,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tt.fields.BackendMasked())
got := cache.NewType(tt.backend)
assert.Equal(t, tt.wantType, got)
})
}
}
Loading