feat: use OS conform path for storing cached results

Instead of storing cached values in the config yaml, they are now stored
under these OS specific locations:
* Linux: `~/.cache/k8sgpt`
* MacOS: `~/Library/Caches`
* Windows: `%LocalAppData%\cache`

Additionally a `Cache` package and interface has been introduced.
Currently there are two implementations:
* Noop - Doesn't do anything
* FileBased - Stores data in files under the locations listed above

fixes #323

Signed-off-by: Patrick Pichler <git@patrickpichler.dev>
This commit is contained in:
Patrick Pichler
2023-04-24 18:41:54 +02:00
parent c3cc413e7f
commit 7eddb8f4a6
7 changed files with 133 additions and 35 deletions

View File

@@ -15,12 +15,14 @@ package ai
import ( import (
"context" "context"
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
) )
type IAI interface { type IAI interface {
Configure(config IAIConfig, language string) error Configure(config IAIConfig, language string) error
GetCompletion(ctx context.Context, prompt string) (string, error) GetCompletion(ctx context.Context, prompt string) (string, error)
Parse(ctx context.Context, prompt []string, nocache bool) (string, error) Parse(ctx context.Context, prompt []string, cache cache.ICache) (string, error)
GetName() string GetName() string
} }

View File

@@ -20,8 +20,8 @@ import (
"strings" "strings"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
"github.com/k8sgpt-ai/k8sgpt/pkg/util" "github.com/k8sgpt-ai/k8sgpt/pkg/util"
"github.com/spf13/viper"
) )
type NoOpAIClient struct { type NoOpAIClient struct {
@@ -44,7 +44,7 @@ func (c *NoOpAIClient) GetCompletion(ctx context.Context, prompt string) (string
return response, nil return response, nil
} }
func (a *NoOpAIClient) Parse(ctx context.Context, prompt []string, nocache bool) (string, error) { func (a *NoOpAIClient) Parse(ctx context.Context, prompt []string, cache cache.ICache) (string, error) {
// parse the text with the AI backend // parse the text with the AI backend
inputKey := strings.Join(prompt, " ") inputKey := strings.Join(prompt, " ")
// Check for cached data // Check for cached data
@@ -57,13 +57,13 @@ func (a *NoOpAIClient) Parse(ctx context.Context, prompt []string, nocache bool)
return "", err return "", err
} }
if !viper.IsSet(cacheKey) { err = cache.Store(cacheKey, base64.StdEncoding.EncodeToString([]byte(response)))
viper.Set(cacheKey, base64.StdEncoding.EncodeToString([]byte(response)))
if err := viper.WriteConfig(); err != nil { if err != nil {
color.Red("error writing config: %v", err) color.Red("error storing value to cache: %v", err)
return "", nil return "", nil
} }
}
return response, nil return response, nil
} }

View File

@@ -20,12 +20,12 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
"github.com/k8sgpt-ai/k8sgpt/pkg/util" "github.com/k8sgpt-ai/k8sgpt/pkg/util"
"github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai"
"github.com/fatih/color" "github.com/fatih/color"
"github.com/spf13/viper"
) )
type OpenAIClient struct { type OpenAIClient struct {
@@ -70,15 +70,20 @@ func (c *OpenAIClient) GetCompletion(ctx context.Context, prompt string) (string
return resp.Choices[0].Message.Content, nil return resp.Choices[0].Message.Content, nil
} }
func (a *OpenAIClient) Parse(ctx context.Context, prompt []string, nocache bool) (string, error) { func (a *OpenAIClient) Parse(ctx context.Context, prompt []string, cache cache.ICache) (string, error) {
inputKey := strings.Join(prompt, " ") inputKey := strings.Join(prompt, " ")
// Check for cached data // Check for cached data
sEnc := base64.StdEncoding.EncodeToString([]byte(inputKey)) sEnc := base64.StdEncoding.EncodeToString([]byte(inputKey))
cacheKey := util.GetCacheKey(a.GetName(), a.language, sEnc) cacheKey := util.GetCacheKey(a.GetName(), a.language, sEnc)
// find in viper cache // find in viper cache
if viper.IsSet(cacheKey) && !nocache { if cache.Exists(cacheKey) {
// retrieve data from cache // retrieve data from cache
response := viper.GetString(cacheKey) response, err := cache.Load(cacheKey)
if err != nil {
return "", err
}
if response == "" { if response == "" {
color.Red("error retrieving cached data") color.Red("error retrieving cached data")
return "", nil return "", nil
@@ -96,13 +101,13 @@ func (a *OpenAIClient) Parse(ctx context.Context, prompt []string, nocache bool)
return "", err return "", err
} }
if !viper.IsSet(cacheKey) || nocache { err = cache.Store(cacheKey, base64.StdEncoding.EncodeToString([]byte(response)))
viper.Set(cacheKey, base64.StdEncoding.EncodeToString([]byte(response)))
if err := viper.WriteConfig(); err != nil { if err != nil {
color.Red("error writing config: %v", err) color.Red("error storing value to cache: %v", err)
return "", nil return "", nil
} }
}
return response, nil return response, nil
} }

View File

@@ -25,6 +25,7 @@ import (
"github.com/fatih/color" "github.com/fatih/color"
"github.com/k8sgpt-ai/k8sgpt/pkg/ai" "github.com/k8sgpt-ai/k8sgpt/pkg/ai"
"github.com/k8sgpt-ai/k8sgpt/pkg/analyzer" "github.com/k8sgpt-ai/k8sgpt/pkg/analyzer"
"github.com/k8sgpt-ai/k8sgpt/pkg/cache"
"github.com/k8sgpt-ai/k8sgpt/pkg/common" "github.com/k8sgpt-ai/k8sgpt/pkg/common"
"github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes" "github.com/k8sgpt-ai/k8sgpt/pkg/kubernetes"
"github.com/k8sgpt-ai/k8sgpt/pkg/util" "github.com/k8sgpt-ai/k8sgpt/pkg/util"
@@ -39,7 +40,7 @@ type Analysis struct {
AIClient ai.IAI AIClient ai.IAI
Results []common.Result Results []common.Result
Namespace string Namespace string
NoCache bool Cache cache.ICache
Explain bool Explain bool
MaxConcurrency int MaxConcurrency int
} }
@@ -106,7 +107,7 @@ func NewAnalysis(backend string, language string, filters []string, namespace st
Client: client, Client: client,
AIClient: aiClient, AIClient: aiClient,
Namespace: namespace, Namespace: namespace,
NoCache: noCache, Cache: cache.New(noCache),
Explain: explain, Explain: explain,
MaxConcurrency: maxConcurrency, MaxConcurrency: maxConcurrency,
}, nil }, nil
@@ -229,7 +230,7 @@ func (a *Analysis) GetAIResults(output string, anonymize bool) error {
} }
texts = append(texts, failure.Text) texts = append(texts, failure.Text)
} }
parsedText, err := a.AIClient.Parse(a.Context, texts, a.NoCache) parsedText, err := a.AIClient.Parse(a.Context, texts, a.Cache)
if err != nil { if err != nil {
// FIXME: can we avoid checking if output is json multiple times? // FIXME: can we avoid checking if output is json multiple times?
// maybe implement the progress bar better? // maybe implement the progress bar better?

15
pkg/cache/cache.go vendored Normal file
View File

@@ -0,0 +1,15 @@
package cache
type ICache interface {
Store(key string, data string) error
Load(key string) (string, error)
Exists(key string) bool
}
func New(noCache bool) ICache {
if noCache {
return &NoopCache{}
}
return &FileBasedCache{}
}

58
pkg/cache/file_based.go vendored Normal file
View File

@@ -0,0 +1,58 @@
package cache
import (
"fmt"
"os"
"path/filepath"
"github.com/adrg/xdg"
"github.com/k8sgpt-ai/k8sgpt/pkg/util"
)
var _ (ICache) = (*FileBasedCache)(nil)
type FileBasedCache struct{}
func (*FileBasedCache) Exists(key string) bool {
path, err := xdg.CacheFile(filepath.Join("k8sgpt", key))
if err != nil {
fmt.Fprintln(os.Stderr, "warning: error while testing if cache key exists:", err)
return false
}
exists, err := util.FileExists(path)
if err != nil {
fmt.Fprintln(os.Stderr, "warning: error while testing if cache key exists:", err)
return false
}
return exists
}
func (*FileBasedCache) Load(key string) (string, error) {
path, err := xdg.CacheFile(filepath.Join("k8sgpt", key))
if err != nil {
return "", err
}
data, err := os.ReadFile(path)
if err != nil {
return "", err
}
return string(data), nil
}
func (*FileBasedCache) Store(key string, data string) error {
path, err := xdg.CacheFile(filepath.Join("k8sgpt", key))
if err != nil {
return err
}
return os.WriteFile(path, []byte(data), 0600)
}

17
pkg/cache/noop.go vendored Normal file
View File

@@ -0,0 +1,17 @@
package cache
var _ (ICache) = (*NoopCache)(nil)
type NoopCache struct{}
func (c *NoopCache) Store(key string, data string) error {
return nil
}
func (c *NoopCache) Load(key string) (string, error) {
return "", nil
}
func (c *NoopCache) Exists(key string) bool {
return false
}