From 576ec376d51b46f7debb643be6e19a5f28cf8715 Mon Sep 17 00:00:00 2001 From: Elad Leev Date: Thu, 18 Jul 2024 21:30:18 +0100 Subject: [PATCH] feat(kubectl): add cache to kubectl --- src/segments/kubectl.go | 54 +++++++++++++++++++++++++++ src/segments/kubectl_test.go | 7 ++++ website/docs/segments/cli/kubectl.mdx | 5 +++ 3 files changed, 66 insertions(+) diff --git a/src/segments/kubectl.go b/src/segments/kubectl.go index 6be01c93fe76..fac9eaec5575 100644 --- a/src/segments/kubectl.go +++ b/src/segments/kubectl.go @@ -1,6 +1,8 @@ package segments import ( + "encoding/json" + "fmt" "path/filepath" "github.com/jandedobbeleer/oh-my-posh/src/properties" @@ -13,12 +15,15 @@ import ( const ( ParseKubeConfig properties.Property = "parse_kubeconfig" ContextAliases properties.Property = "context_aliases" + kubectlCacheKey = "kubectl" ) type Kubectl struct { props properties.Properties env runtime.Environment + dirty bool + Context string KubeContext @@ -47,11 +52,50 @@ func (k *Kubectl) Init(props properties.Properties, env runtime.Environment) { k.env = env } +func (k *Kubectl) setCacheValue(timeout int) { + if !k.dirty { + return + } + + cachedData, _ := json.Marshal(k) + k.env.Cache().Set(kubectlCacheKey, string(cachedData), timeout) +} + +func (k *Kubectl) restoreCacheValue() error { + if val, found := k.env.Cache().Get(kubectlCacheKey); found { + err := json.Unmarshal([]byte(val), k) + if err != nil { + k.env.Error(err) + return err + } + + return nil + } + + return fmt.Errorf("no data in cache") +} + func (k *Kubectl) Enabled() bool { + cacheTimeout := k.props.GetInt(properties.CacheTimeout, 1) + + if cacheTimeout > 0 { + if err := k.restoreCacheValue(); err == nil { + return true + } + } + + defer func() { + if cacheTimeout > 0 { + k.setCacheValue(cacheTimeout) + } + }() + parseKubeConfig := k.props.GetBool(ParseKubeConfig, false) + if parseKubeConfig { return k.doParseKubeConfig() } + return k.doCallKubectl() } @@ -62,8 +106,10 @@ func (k *Kubectl) doParseKubeConfig() bool { if len(kubeconfigs) == 0 { kubeconfigs = []string{filepath.Join(k.env.Home(), ".kube/config")} } + contexts := make(map[string]*KubeContext) k.Context = "" + for _, kubeconfig := range kubeconfigs { if len(kubeconfig) == 0 { continue @@ -97,6 +143,7 @@ func (k *Kubectl) doParseKubeConfig() bool { } k.SetContextAlias() + k.dirty = true return true } @@ -114,12 +161,14 @@ func (k *Kubectl) doCallKubectl() bool { if !k.env.HasCommand(cmd) { return false } + result, err := k.env.RunCommand(cmd, "config", "view", "--output", "yaml", "--minify") displayError := k.props.GetBool(properties.DisplayError, false) if err != nil && displayError { k.setError("KUBECTL ERR") return true } + if err != nil { return false } @@ -129,11 +178,15 @@ func (k *Kubectl) doCallKubectl() bool { if err != nil { return false } + k.Context = config.CurrentContext k.SetContextAlias() + k.dirty = true + if len(config.Contexts) > 0 { k.KubeContext = *config.Contexts[0].Context } + return true } @@ -141,6 +194,7 @@ func (k *Kubectl) setError(message string) { if len(k.Context) == 0 { k.Context = message } + k.Namespace = message k.User = message k.Cluster = message diff --git a/src/segments/kubectl_test.go b/src/segments/kubectl_test.go index 74c16f35d0c0..31435326396f 100644 --- a/src/segments/kubectl_test.go +++ b/src/segments/kubectl_test.go @@ -134,11 +134,13 @@ func TestKubectlSegment(t *testing.T) { for _, tc := range cases { env := new(mock.Environment) env.On("HasCommand", "kubectl").Return(tc.KubectlExists) + var kubeconfig string content, err := os.ReadFile("../test/kubectl.yml") if err == nil { kubeconfig = fmt.Sprintf(string(content), tc.Cluster, tc.UserName, tc.Namespace, tc.Context) } + var kubectlErr error if tc.KubectlErr { kubectlErr = &runtime.CommandError{ @@ -146,11 +148,14 @@ func TestKubectlSegment(t *testing.T) { ExitCode: 1, } } + env.On("RunCommand", "kubectl", []string{"config", "view", "--output", "yaml", "--minify"}).Return(kubeconfig, kubectlErr) env.On("Getenv", "KUBECONFIG").Return(tc.Kubeconfig) + for path, content := range tc.Files { env.On("FileContent", path).Return(content) } + env.On("Home").Return("testhome") k := &Kubectl{ @@ -159,8 +164,10 @@ func TestKubectlSegment(t *testing.T) { properties.DisplayError: tc.DisplayError, ParseKubeConfig: tc.ParseKubeConfig, ContextAliases: tc.ContextAliases, + properties.CacheTimeout: 0, }, } + assert.Equal(t, tc.ExpectedEnabled, k.Enabled(), tc.Case) if tc.ExpectedEnabled { assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, k), tc.Case) diff --git a/website/docs/segments/cli/kubectl.mdx b/website/docs/segments/cli/kubectl.mdx index 7f2485f3f225..1380f9ce7c39 100644 --- a/website/docs/segments/cli/kubectl.mdx +++ b/website/docs/segments/cli/kubectl.mdx @@ -8,6 +8,10 @@ sidebar_label: Kubectl Display the currently active Kubernetes context name and namespace name. +:::caution +The Kubernetes context is cached for 1 minute by default. To avoid caching, set `cache_timeout` to 0. +::: + ## Sample Configuration import Config from "@site/src/components/Config.js"; @@ -35,6 +39,7 @@ import Config from "@site/src/components/Config.js"; | `display_error` | `boolean` | `false` | show the error context when failing to retrieve the kubectl information | | `parse_kubeconfig` | `boolean` | `false` | parse kubeconfig files instead of calling out to kubectl to improve performance | | `context_aliases` | `object` | | custom context namespace | +| `cache_timeout` | `int` | `1` | in minutes - how long is the context cached | ## Template ([info][templates])