mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2024-11-09 20:44:03 -08:00
feat: enable kubectl segment to read kubeconfig
This extends the kubectl segment to optionally not rely on the kubectl command and instead to directly parse kubeconfig files like kubectl does. This is meant as a performance optimization similar to how the git segment can determine the current branch itself without calling to git. Especially on Windows and in the presence other factors slowing process creation like like AntiVirus this can make shells using the segment considerably more responsive. The functionality is enabled using the new parse_kubeconfig prop. It defaults to false to prevent breaking existing users in case there are any unanticipated behavioral changes. Additionally the new template properties Cluster and User were added as they are easily available and helpful in kubectl setups with more elaborate configuration.
This commit is contained in:
parent
4d925b69ba
commit
7a73bcff0b
|
@ -29,11 +29,15 @@ Display the currently active Kubernetes context name and namespace name.
|
|||
- template: `string` - A go [text/template][go-text-template] template extended with [sprig][sprig] utilizing the
|
||||
properties below - defaults to `{{.Context}}{{if .Namespace}} :: {{.Namespace}}{{end}}`
|
||||
- display_error: `boolean` - show the error context when failing to retrieve the kubectl information - defaults to `false`
|
||||
- parse_kubeconfig: `boolean` - parse kubeconfig files instead of calling out to kubectl to improve
|
||||
performance - defaults to `false`
|
||||
|
||||
## Template Properties
|
||||
|
||||
- `.Context`: `string` - the current kubectl context
|
||||
- `.Namespace`: `string` - the current kubectl namespace
|
||||
- `.Namespace`: `string` - the current kubectl context namespace
|
||||
- `.User`: `string` - the current kubectl context user
|
||||
- `.Cluster`: `string` - the current kubectl context cluster
|
||||
|
||||
## Tips
|
||||
|
||||
|
|
|
@ -35,6 +35,8 @@ require (
|
|||
howett.net/plist v0.0.0-20201203080718-1454fab16a06 // indirect
|
||||
)
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
|
||||
require (
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
|
@ -54,7 +56,6 @@ require (
|
|||
github.com/tklauser/numcpus v0.3.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
)
|
||||
|
||||
replace github.com/distatus/battery v0.10.0 => github.com/JanDeDobbeleer/battery v0.10.0-2
|
||||
|
|
|
@ -1,14 +1,34 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Whether to use kubectl or read kubeconfig ourselves
|
||||
const ParseKubeConfig Property = "parse_kubeconfig"
|
||||
|
||||
type kubectl struct {
|
||||
props properties
|
||||
env environmentInfo
|
||||
Context string
|
||||
Namespace string
|
||||
props properties
|
||||
env environmentInfo
|
||||
Context string
|
||||
KubeConfigContext
|
||||
}
|
||||
|
||||
type KubeConfigContext struct {
|
||||
Cluster string `yaml:"cluster"`
|
||||
User string `yaml:"user"`
|
||||
Namespace string `yaml:"namespace"`
|
||||
}
|
||||
|
||||
type KubeConfig struct {
|
||||
CurrentContext string `yaml:"current-context"`
|
||||
Contexts []struct {
|
||||
Context KubeConfigContext `yaml:"context"`
|
||||
Name string `yaml:"name"`
|
||||
} `yaml:"contexts"`
|
||||
}
|
||||
|
||||
func (k *kubectl) string() string {
|
||||
|
@ -31,15 +51,69 @@ func (k *kubectl) init(props properties, env environmentInfo) {
|
|||
}
|
||||
|
||||
func (k *kubectl) enabled() bool {
|
||||
parseKubeConfig := k.props.getBool(ParseKubeConfig, false)
|
||||
if parseKubeConfig {
|
||||
return k.doParseKubeConfig()
|
||||
}
|
||||
return k.doCallKubectl()
|
||||
}
|
||||
|
||||
func (k *kubectl) doParseKubeConfig() bool {
|
||||
// Follow kubectl search rules (see https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/#the-kubeconfig-environment-variable)
|
||||
// TL;DR: KUBECONFIG can contain a list of files. If it's empty ~/.kube/config is used. First file in list wins when merging keys.
|
||||
kubeconfigs := filepath.SplitList(k.env.getenv("KUBECONFIG"))
|
||||
if len(kubeconfigs) == 0 {
|
||||
kubeconfigs = []string{filepath.Join(k.env.homeDir(), ".kube/config")}
|
||||
}
|
||||
contexts := make(map[string]KubeConfigContext)
|
||||
k.Context = ""
|
||||
for _, kubeconfig := range kubeconfigs {
|
||||
if len(kubeconfig) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
content := k.env.getFileContent(kubeconfig)
|
||||
|
||||
var config KubeConfig
|
||||
err := yaml.Unmarshal([]byte(content), &config)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, context := range config.Contexts {
|
||||
if _, exists := contexts[context.Name]; !exists {
|
||||
contexts[context.Name] = context.Context
|
||||
}
|
||||
}
|
||||
|
||||
if len(k.Context) == 0 {
|
||||
k.Context = config.CurrentContext
|
||||
}
|
||||
|
||||
context, exists := contexts[k.Context]
|
||||
if exists {
|
||||
k.KubeConfigContext = context
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
displayError := k.props.getBool(DisplayError, false)
|
||||
if !displayError {
|
||||
return false
|
||||
}
|
||||
k.setError("KUBECONFIG ERR")
|
||||
return true
|
||||
}
|
||||
|
||||
func (k *kubectl) doCallKubectl() bool {
|
||||
cmd := "kubectl"
|
||||
if !k.env.hasCommand(cmd) {
|
||||
return false
|
||||
}
|
||||
result, err := k.env.runCommand(cmd, "config", "view", "--minify", "--output", "jsonpath={..current-context},{..namespace}")
|
||||
result, err := k.env.runCommand(cmd, "config", "view", "--minify", "--output", "jsonpath={..current-context},{..namespace},{..context.user},{..context.cluster}")
|
||||
displayError := k.props.getBool(DisplayError, false)
|
||||
if err != nil && displayError {
|
||||
k.Context = "KUBECTL ERR"
|
||||
k.Namespace = k.Context
|
||||
k.setError("KUBECTL ERR")
|
||||
return true
|
||||
}
|
||||
if err != nil {
|
||||
|
@ -49,5 +123,16 @@ func (k *kubectl) enabled() bool {
|
|||
values := strings.Split(result, ",")
|
||||
k.Context = values[0]
|
||||
k.Namespace = values[1]
|
||||
return k.Context != ""
|
||||
k.User = values[2]
|
||||
k.Cluster = values[3]
|
||||
return len(k.Context) > 0
|
||||
}
|
||||
|
||||
func (k *kubectl) setError(message string) {
|
||||
if len(k.Context) == 0 {
|
||||
k.Context = message
|
||||
}
|
||||
k.Namespace = message
|
||||
k.User = message
|
||||
k.Cluster = message
|
||||
}
|
||||
|
|
|
@ -1,24 +1,32 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type kubectlArgs struct {
|
||||
kubectlExists bool
|
||||
kubectlErr bool
|
||||
template string
|
||||
displayError bool
|
||||
context string
|
||||
namespace string
|
||||
kubectlExists bool
|
||||
kubectlErr bool
|
||||
kubeconfig string
|
||||
parseKubeConfig bool
|
||||
template string
|
||||
displayError bool
|
||||
kubectlOutContext string
|
||||
kubectlOutNamespace string
|
||||
kubectlOutUser string
|
||||
kubectlOutCluster string
|
||||
files map[string]string
|
||||
}
|
||||
|
||||
const testKubectlAllInfoTemplate = "{{.Context}} :: {{.Namespace}} :: {{.User}} :: {{.Cluster}}"
|
||||
|
||||
func bootStrapKubectlTest(args *kubectlArgs) *kubectl {
|
||||
env := new(MockedEnvironment)
|
||||
env.On("hasCommand", "kubectl").Return(args.kubectlExists)
|
||||
kubectlOut := args.context + "," + args.namespace
|
||||
kubectlOut := args.kubectlOutContext + "," + args.kubectlOutNamespace + "," + args.kubectlOutUser + "," + args.kubectlOutCluster
|
||||
var kubectlErr error
|
||||
if args.kubectlErr {
|
||||
kubectlErr = &commandError{
|
||||
|
@ -26,12 +34,21 @@ func bootStrapKubectlTest(args *kubectlArgs) *kubectl {
|
|||
exitCode: 1,
|
||||
}
|
||||
}
|
||||
env.On("runCommand", "kubectl", []string{"config", "view", "--minify", "--output", "jsonpath={..current-context},{..namespace}"}).Return(kubectlOut, kubectlErr)
|
||||
env.On("runCommand", "kubectl",
|
||||
[]string{"config", "view", "--minify", "--output", "jsonpath={..current-context},{..namespace},{..context.user},{..context.cluster}"}).Return(kubectlOut, kubectlErr)
|
||||
|
||||
env.On("getenv", "KUBECONFIG").Return(args.kubeconfig)
|
||||
for path, content := range args.files {
|
||||
env.On("getFileContent", path).Return(content)
|
||||
}
|
||||
env.On("homeDir", nil).Return("testhome")
|
||||
|
||||
k := &kubectl{
|
||||
env: env,
|
||||
props: map[Property]interface{}{
|
||||
SegmentTemplate: args.template,
|
||||
DisplayError: args.displayError,
|
||||
ParseKubeConfig: args.parseKubeConfig,
|
||||
},
|
||||
}
|
||||
return k
|
||||
|
@ -39,37 +56,108 @@ func bootStrapKubectlTest(args *kubectlArgs) *kubectl {
|
|||
|
||||
func TestKubectlSegment(t *testing.T) {
|
||||
standardTemplate := "{{.Context}}{{if .Namespace}} :: {{.Namespace}}{{end}}"
|
||||
lsep := string(filepath.ListSeparator)
|
||||
|
||||
cases := []struct {
|
||||
Case string
|
||||
Template string
|
||||
DisplayError bool
|
||||
KubectlExists bool
|
||||
Kubeconfig string
|
||||
ParseKubeConfig bool
|
||||
Context string
|
||||
Namespace string
|
||||
User string
|
||||
Cluster string
|
||||
KubectlErr bool
|
||||
ExpectedEnabled bool
|
||||
ExpectedString string
|
||||
Files map[string]string
|
||||
}{
|
||||
{Case: "disabled", Template: standardTemplate, KubectlExists: false, Context: "aaa", Namespace: "bbb", ExpectedString: "", ExpectedEnabled: false},
|
||||
{Case: "normal", Template: standardTemplate, KubectlExists: true, Context: "aaa", Namespace: "bbb", ExpectedString: "aaa :: bbb", ExpectedEnabled: true},
|
||||
{Case: "all information", Template: testKubectlAllInfoTemplate, KubectlExists: true, Context: "aaa", Namespace: "bbb", User: "ccc", Cluster: "ddd",
|
||||
ExpectedString: "aaa :: bbb :: ccc :: ddd", ExpectedEnabled: true},
|
||||
{Case: "no namespace", Template: standardTemplate, KubectlExists: true, Context: "aaa", Namespace: "", ExpectedString: "aaa", ExpectedEnabled: true},
|
||||
{Case: "kubectl error", Template: standardTemplate, DisplayError: true, KubectlExists: true, Context: "aaa", Namespace: "bbb", KubectlErr: true,
|
||||
ExpectedString: "KUBECTL ERR :: KUBECTL ERR", ExpectedEnabled: true},
|
||||
{Case: "kubectl error hidden", Template: standardTemplate, DisplayError: false, KubectlExists: true, Context: "aaa", Namespace: "bbb", KubectlErr: true,
|
||||
ExpectedString: "", ExpectedEnabled: false},
|
||||
{Case: "kubectl error hidden", Template: standardTemplate, DisplayError: false, KubectlExists: true, Context: "aaa", Namespace: "bbb", KubectlErr: true, ExpectedEnabled: false},
|
||||
{Case: "kubeconfig home", Template: testKubectlAllInfoTemplate, ParseKubeConfig: true, Files: testKubeConfigFiles, ExpectedString: "aaa :: bbb :: ccc :: ddd",
|
||||
ExpectedEnabled: true},
|
||||
{Case: "kubeconfig multiple current marker first", Template: testKubectlAllInfoTemplate, ParseKubeConfig: true,
|
||||
Kubeconfig: "" + lsep + "currentcontextmarker" + lsep + "contextdefinition" + lsep + "contextredefinition",
|
||||
Files: testKubeConfigFiles, ExpectedString: "ctx :: ns :: usr :: cl", ExpectedEnabled: true},
|
||||
{Case: "kubeconfig multiple context first", Template: testKubectlAllInfoTemplate, ParseKubeConfig: true,
|
||||
Kubeconfig: "contextdefinition" + lsep + "contextredefinition" + lsep + "currentcontextmarker" + lsep,
|
||||
Files: testKubeConfigFiles, ExpectedString: "ctx :: ns :: usr :: cl", ExpectedEnabled: true},
|
||||
{Case: "kubeconfig error hidden", Template: testKubectlAllInfoTemplate, ParseKubeConfig: true, Kubeconfig: "invalid", Files: testKubeConfigFiles, ExpectedEnabled: false},
|
||||
{Case: "kubeconfig error", Template: testKubectlAllInfoTemplate, ParseKubeConfig: true,
|
||||
Kubeconfig: "invalid", Files: testKubeConfigFiles, DisplayError: true,
|
||||
ExpectedString: "KUBECONFIG ERR :: KUBECONFIG ERR :: KUBECONFIG ERR :: KUBECONFIG ERR", ExpectedEnabled: true},
|
||||
{Case: "kubeconfig incomplete", Template: testKubectlAllInfoTemplate, ParseKubeConfig: true,
|
||||
Kubeconfig: "currentcontextmarker" + lsep + "contextdefinitionincomplete",
|
||||
Files: testKubeConfigFiles, ExpectedString: "ctx :: :: :: ", ExpectedEnabled: true},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
args := &kubectlArgs{
|
||||
kubectlExists: tc.KubectlExists,
|
||||
template: tc.Template,
|
||||
displayError: tc.DisplayError,
|
||||
context: tc.Context,
|
||||
namespace: tc.Namespace,
|
||||
kubectlErr: tc.KubectlErr,
|
||||
kubectlExists: tc.KubectlExists,
|
||||
template: tc.Template,
|
||||
displayError: tc.DisplayError,
|
||||
kubectlOutContext: tc.Context,
|
||||
kubectlOutNamespace: tc.Namespace,
|
||||
kubectlOutUser: tc.User,
|
||||
kubectlOutCluster: tc.Cluster,
|
||||
kubectlErr: tc.KubectlErr,
|
||||
parseKubeConfig: tc.ParseKubeConfig,
|
||||
files: tc.Files,
|
||||
kubeconfig: tc.Kubeconfig,
|
||||
}
|
||||
kubectl := bootStrapKubectlTest(args)
|
||||
assert.Equal(t, tc.ExpectedEnabled, kubectl.enabled(), tc.Case)
|
||||
assert.Equal(t, tc.ExpectedString, kubectl.string(), tc.Case)
|
||||
if tc.ExpectedEnabled {
|
||||
assert.Equal(t, tc.ExpectedString, kubectl.string(), tc.Case)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var testKubeConfigFiles = map[string]string{
|
||||
filepath.Join("testhome", ".kube/config"): `
|
||||
apiVersion: v1
|
||||
contexts:
|
||||
- context:
|
||||
cluster: ddd
|
||||
user: ccc
|
||||
namespace: bbb
|
||||
name: aaa
|
||||
current-context: aaa
|
||||
`,
|
||||
"contextdefinition": `
|
||||
apiVersion: v1
|
||||
contexts:
|
||||
- context:
|
||||
cluster: cl
|
||||
user: usr
|
||||
namespace: ns
|
||||
name: ctx
|
||||
`,
|
||||
"currentcontextmarker": `
|
||||
apiVersion: v1
|
||||
current-context: ctx
|
||||
`,
|
||||
"invalid": "this is not yaml",
|
||||
"contextdefinitionincomplete": `
|
||||
apiVersion: v1
|
||||
contexts:
|
||||
- name: ctx
|
||||
`,
|
||||
"contextredefinition": `
|
||||
apiVersion: v1
|
||||
contexts:
|
||||
- context:
|
||||
cluster: wrongcl
|
||||
user: wrongu
|
||||
namespace: wrongns
|
||||
name: ctx
|
||||
`,
|
||||
}
|
||||
|
|
|
@ -866,6 +866,12 @@
|
|||
"title": "Display Error",
|
||||
"description": "Show the error context when failing to retrieve the kubectl information",
|
||||
"default": false
|
||||
},
|
||||
"parse_kubeconfig": {
|
||||
"type": "boolean",
|
||||
"title": "Parse kubeconfig",
|
||||
"description": "Parse kubeconfig files instead of calling out to kubectl to improve performance.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue