From ce4f44ec256fe0a9ff08384ec1d86cbf1420a67f Mon Sep 17 00:00:00 2001 From: Egor Kovetskiy Date: Tue, 4 Jul 2023 06:49:25 +0000 Subject: [PATCH] add cache for namespaces for one hour Signed-off-by: Egor Kovetskiy --- cmd/tubectl/cache.go | 102 ++++++++++++++++++++++++++++++++++++++++ cmd/tubectl/complete.go | 2 +- cmd/tubectl/kube.go | 14 ++++++ 3 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 cmd/tubectl/cache.go diff --git a/cmd/tubectl/cache.go b/cmd/tubectl/cache.go new file mode 100644 index 0000000..fa4f7b5 --- /dev/null +++ b/cmd/tubectl/cache.go @@ -0,0 +1,102 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "time" + + "github.com/reconquest/karma-go" +) + +// useCache executes do() function and caches its result in path. +// if cache is older than ttl, do() function is executed again. +// if do() function returns error, cache is not updated. +func useCache[T any]( + do func() (T, error), + path string, + ttl time.Duration, +) (T, error) { + var result T + + userCacheDir, err := os.UserCacheDir() + if err != nil { + return result, karma.Format( + err, + "unable to get user cache dir", + ) + } + + cacheDir := filepath.Join(userCacheDir, "tubekit") + + cachePath := filepath.Join(cacheDir, path) + + cacheFile, err := os.OpenFile(cachePath, os.O_RDWR, 0644) + switch { + case err == nil: + defer cacheFile.Close() + + stat, err := cacheFile.Stat() + if err != nil { + return result, karma.Format( + err, + "unable to stat cache file: %s", cachePath, + ) + } + + if time.Since(stat.ModTime()) < ttl { + var result T + err := json.NewDecoder(cacheFile).Decode(&result) + if err != nil { + return result, karma.Format( + err, + "unable to decode cache file: %s", cachePath, + ) + } + + return result, nil + } + + case os.IsNotExist(err): + err = os.MkdirAll(filepath.Dir(cachePath), 0755) + if err != nil { + return result, karma.Format( + err, + "unable to create cache dir for: %s", cachePath, + ) + } + + cacheFile, err = os.Create(cachePath) + if err != nil { + return result, karma.Format( + err, + "unable to create cache file: %s", cachePath, + ) + } + + defer cacheFile.Close() + + default: + return result, karma.Format( + err, + "unable to open cache file: %s", cachePath, + ) + } + + result, err = do() + if err != nil { + return result, err + } + + encoder := json.NewEncoder(cacheFile) + + err = encoder.Encode(result) + if err != nil { + return result, karma.Format( + err, + "unable to encode cache file: %s", path, + ) + } + + return result, nil +} diff --git a/cmd/tubectl/complete.go b/cmd/tubectl/complete.go index a0f1373..68f122b 100644 --- a/cmd/tubectl/complete.go +++ b/cmd/tubectl/complete.go @@ -47,7 +47,7 @@ func completeParams(client string, params *Params) (*Params, error) { } if params.CompleteNamespace { - namespaces, err := requestNamespaces(client, params) + namespaces, err := requestNamespacesWithCache(client, params) if err != nil { return params, karma.Format( err, diff --git a/cmd/tubectl/kube.go b/cmd/tubectl/kube.go index 6f5b26c..8db3ce5 100644 --- a/cmd/tubectl/kube.go +++ b/cmd/tubectl/kube.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "sort" + "time" "github.com/reconquest/karma-go" clientcmdapi "k8s.io/client-go/tools/clientcmd" @@ -44,6 +45,19 @@ func parseKubernetesContexts(kubeconfig string) ([]string, error) { return contexts, nil } +// requestNamespacesWithCache returns list of namespaces available in current context. +// it uses requestNamespaces() to retrieve list of namespaces from kubernetes +// but caches the result in XDG_CACHE_HOME/tubekit/{current-context}/namespaces.json +func requestNamespacesWithCache(client string, params *Params) ([]string, error) { + return useCache( + func() ([]string, error) { + return requestNamespaces(client, params) + }, + fmt.Sprintf("%s/namespaces.json", params.Context), + time.Hour, + ) +} + func requestNamespaces(client string, params *Params) ([]string, error) { // omit namespace argument because requesting list of them cmd, args := getCommand(