Skip to content

Commit b75c6c4

Browse files
feat(cache): support Redis (fanal#143)
* feat(cache): support Redis * chore(mod): update * feat(main): support Redis * test: update error messages according to different errors on GitHub Actions * feat(redis): add prefix * fix an error Co-authored-by: Daniel Pacak <[email protected]> * fix an error Co-authored-by: Daniel Pacak <[email protected]> * fix(main): defer close * test(redis): fix error messages * test(redis): count current connections Co-authored-by: Daniel Pacak <[email protected]> * test(redis): use structs instead of string literals Co-authored-by: Daniel Pacak <[email protected]>
1 parent da40228 commit b75c6c4

File tree

5 files changed

+742
-14
lines changed

5 files changed

+742
-14
lines changed

cache/redis.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package cache
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
8+
"github.com/go-redis/redis/v8"
9+
"github.com/google/go-containerregistry/pkg/v1"
10+
"golang.org/x/xerrors"
11+
12+
"github.com/aquasecurity/fanal/types"
13+
)
14+
15+
var _ Cache = &RedisCache{}
16+
17+
const (
18+
redisPrefix = "fanal"
19+
)
20+
21+
type RedisCache struct {
22+
client *redis.Client
23+
}
24+
25+
func NewRedisCache(options *redis.Options) RedisCache {
26+
return RedisCache{
27+
client: redis.NewClient(options),
28+
}
29+
}
30+
31+
func (c RedisCache) PutArtifact(artifactID string, artifactConfig types.ArtifactInfo) error {
32+
key := fmt.Sprintf("%s::%s::%s", redisPrefix, artifactBucket, artifactID)
33+
b, err := json.Marshal(artifactConfig)
34+
if err != nil {
35+
return xerrors.Errorf("failed to marshal artifact JSON: %w", err)
36+
}
37+
if err := c.client.Set(context.TODO(), key, string(b), 0).Err(); err != nil {
38+
return xerrors.Errorf("unable to store artifact information in Redis cache (%s): %w", artifactID, err)
39+
}
40+
return nil
41+
}
42+
43+
func (c RedisCache) PutBlob(blobID string, blobInfo types.BlobInfo) error {
44+
if _, err := v1.NewHash(blobID); err != nil {
45+
return xerrors.Errorf("invalid diffID (%s): %w", blobID, err)
46+
}
47+
b, err := json.Marshal(blobInfo)
48+
if err != nil {
49+
return xerrors.Errorf("failed to marshal blob JSON: %w", err)
50+
}
51+
key := fmt.Sprintf("%s::%s::%s", redisPrefix, blobBucket, blobID)
52+
if err := c.client.Set(context.TODO(), key, string(b), 0).Err(); err != nil {
53+
return xerrors.Errorf("unable to store blob information in Redis cache (%s): %w", blobID, err)
54+
}
55+
return nil
56+
}
57+
58+
func (c RedisCache) GetArtifact(artifactID string) (types.ArtifactInfo, error) {
59+
key := fmt.Sprintf("%s::%s::%s", redisPrefix, artifactBucket, artifactID)
60+
val, err := c.client.Get(context.TODO(), key).Bytes()
61+
if err == redis.Nil {
62+
return types.ArtifactInfo{}, xerrors.Errorf("artifact (%s) is missing in Redis cache", artifactID)
63+
} else if err != nil {
64+
return types.ArtifactInfo{}, xerrors.Errorf("failed to get artifact from the Redis cache: %w", err)
65+
}
66+
67+
var info types.ArtifactInfo
68+
err = json.Unmarshal(val, &info)
69+
if err != nil {
70+
return types.ArtifactInfo{}, xerrors.Errorf("failed to unmarshal artifact (%s) from Redis value: %w", artifactID, err)
71+
}
72+
return info, nil
73+
}
74+
75+
func (c RedisCache) GetBlob(blobID string) (types.BlobInfo, error) {
76+
key := fmt.Sprintf("%s::%s::%s", redisPrefix, blobBucket, blobID)
77+
val, err := c.client.Get(context.TODO(), key).Bytes()
78+
if err == redis.Nil {
79+
return types.BlobInfo{}, xerrors.Errorf("blob (%s) is missing in Redis cache", blobID)
80+
} else if err != nil {
81+
return types.BlobInfo{}, xerrors.Errorf("failed to get blob from the Redis cache: %w", err)
82+
}
83+
84+
var blobInfo types.BlobInfo
85+
if err = json.Unmarshal(val, &blobInfo); err != nil {
86+
return types.BlobInfo{}, xerrors.Errorf("failed to unmarshal blob (%s) from Redis value: %w", blobID, err)
87+
}
88+
return blobInfo, nil
89+
}
90+
91+
func (c RedisCache) MissingBlobs(artifactID string, blobIDs []string) (bool, []string, error) {
92+
var missingArtifact bool
93+
var missingBlobIDs []string
94+
for _, blobID := range blobIDs {
95+
blobInfo, err := c.GetBlob(blobID)
96+
if err != nil {
97+
// error means cache missed blob info
98+
missingBlobIDs = append(missingBlobIDs, blobID)
99+
continue
100+
}
101+
if blobInfo.SchemaVersion != types.BlobJSONSchemaVersion {
102+
missingBlobIDs = append(missingBlobIDs, blobID)
103+
}
104+
}
105+
// get artifact info
106+
artifactInfo, err := c.GetArtifact(artifactID)
107+
// error means cache missed artifact info
108+
if err != nil {
109+
return true, missingBlobIDs, nil
110+
}
111+
if artifactInfo.SchemaVersion != types.ArtifactJSONSchemaVersion {
112+
missingArtifact = true
113+
}
114+
return missingArtifact, missingBlobIDs, nil
115+
}
116+
117+
func (c RedisCache) Close() error {
118+
return c.client.Close()
119+
}
120+
121+
func (c RedisCache) Clear() error {
122+
ctx := context.Background()
123+
124+
var cursor uint64
125+
for {
126+
var keys []string
127+
var err error
128+
keys, cursor, err = c.client.Scan(ctx, cursor, redisPrefix+"::*", 100).Result()
129+
if err != nil {
130+
return xerrors.Errorf("failed to perform prefix scanning: %w", err)
131+
}
132+
if err = c.client.Unlink(ctx, keys...).Err(); err != nil {
133+
return xerrors.Errorf("failed to unlink redis keys: %w", err)
134+
}
135+
if cursor == 0 {
136+
break
137+
}
138+
}
139+
return nil
140+
}

0 commit comments

Comments
 (0)