mirror of
https://github.com/JanDeDobbeleer/oh-my-posh.git
synced 2025-02-02 05:41:10 -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
|
- 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}}`
|
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`
|
- 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
|
## Template Properties
|
||||||
|
|
||||||
- `.Context`: `string` - the current kubectl context
|
- `.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
|
## Tips
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,8 @@ require (
|
||||||
howett.net/plist v0.0.0-20201203080718-1454fab16a06 // indirect
|
howett.net/plist v0.0.0-20201203080718-1454fab16a06 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||||
github.com/Masterminds/semver/v3 v3.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/tklauser/numcpus v0.3.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // 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
|
replace github.com/distatus/battery v0.10.0 => github.com/JanDeDobbeleer/battery v0.10.0-2
|
||||||
|
|
|
@ -1,14 +1,34 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Whether to use kubectl or read kubeconfig ourselves
|
||||||
|
const ParseKubeConfig Property = "parse_kubeconfig"
|
||||||
|
|
||||||
type kubectl struct {
|
type kubectl struct {
|
||||||
props properties
|
props properties
|
||||||
env environmentInfo
|
env environmentInfo
|
||||||
Context string
|
Context string
|
||||||
Namespace 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 {
|
func (k *kubectl) string() string {
|
||||||
|
@ -31,15 +51,69 @@ func (k *kubectl) init(props properties, env environmentInfo) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *kubectl) enabled() bool {
|
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"
|
cmd := "kubectl"
|
||||||
if !k.env.hasCommand(cmd) {
|
if !k.env.hasCommand(cmd) {
|
||||||
return false
|
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)
|
displayError := k.props.getBool(DisplayError, false)
|
||||||
if err != nil && displayError {
|
if err != nil && displayError {
|
||||||
k.Context = "KUBECTL ERR"
|
k.setError("KUBECTL ERR")
|
||||||
k.Namespace = k.Context
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -49,5 +123,16 @@ func (k *kubectl) enabled() bool {
|
||||||
values := strings.Split(result, ",")
|
values := strings.Split(result, ",")
|
||||||
k.Context = values[0]
|
k.Context = values[0]
|
||||||
k.Namespace = values[1]
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
type kubectlArgs struct {
|
type kubectlArgs struct {
|
||||||
kubectlExists bool
|
kubectlExists bool
|
||||||
kubectlErr bool
|
kubectlErr bool
|
||||||
template string
|
kubeconfig string
|
||||||
displayError bool
|
parseKubeConfig bool
|
||||||
context string
|
template string
|
||||||
namespace 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 {
|
func bootStrapKubectlTest(args *kubectlArgs) *kubectl {
|
||||||
env := new(MockedEnvironment)
|
env := new(MockedEnvironment)
|
||||||
env.On("hasCommand", "kubectl").Return(args.kubectlExists)
|
env.On("hasCommand", "kubectl").Return(args.kubectlExists)
|
||||||
kubectlOut := args.context + "," + args.namespace
|
kubectlOut := args.kubectlOutContext + "," + args.kubectlOutNamespace + "," + args.kubectlOutUser + "," + args.kubectlOutCluster
|
||||||
var kubectlErr error
|
var kubectlErr error
|
||||||
if args.kubectlErr {
|
if args.kubectlErr {
|
||||||
kubectlErr = &commandError{
|
kubectlErr = &commandError{
|
||||||
|
@ -26,12 +34,21 @@ func bootStrapKubectlTest(args *kubectlArgs) *kubectl {
|
||||||
exitCode: 1,
|
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{
|
k := &kubectl{
|
||||||
env: env,
|
env: env,
|
||||||
props: map[Property]interface{}{
|
props: map[Property]interface{}{
|
||||||
SegmentTemplate: args.template,
|
SegmentTemplate: args.template,
|
||||||
DisplayError: args.displayError,
|
DisplayError: args.displayError,
|
||||||
|
ParseKubeConfig: args.parseKubeConfig,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return k
|
return k
|
||||||
|
@ -39,37 +56,108 @@ func bootStrapKubectlTest(args *kubectlArgs) *kubectl {
|
||||||
|
|
||||||
func TestKubectlSegment(t *testing.T) {
|
func TestKubectlSegment(t *testing.T) {
|
||||||
standardTemplate := "{{.Context}}{{if .Namespace}} :: {{.Namespace}}{{end}}"
|
standardTemplate := "{{.Context}}{{if .Namespace}} :: {{.Namespace}}{{end}}"
|
||||||
|
lsep := string(filepath.ListSeparator)
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
Case string
|
Case string
|
||||||
Template string
|
Template string
|
||||||
DisplayError bool
|
DisplayError bool
|
||||||
KubectlExists bool
|
KubectlExists bool
|
||||||
|
Kubeconfig string
|
||||||
|
ParseKubeConfig bool
|
||||||
Context string
|
Context string
|
||||||
Namespace string
|
Namespace string
|
||||||
|
User string
|
||||||
|
Cluster string
|
||||||
KubectlErr bool
|
KubectlErr bool
|
||||||
ExpectedEnabled bool
|
ExpectedEnabled bool
|
||||||
ExpectedString string
|
ExpectedString string
|
||||||
|
Files map[string]string
|
||||||
}{
|
}{
|
||||||
{Case: "disabled", Template: standardTemplate, KubectlExists: false, Context: "aaa", Namespace: "bbb", ExpectedString: "", ExpectedEnabled: false},
|
{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: "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: "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,
|
{Case: "kubectl error", Template: standardTemplate, DisplayError: true, KubectlExists: true, Context: "aaa", Namespace: "bbb", KubectlErr: true,
|
||||||
ExpectedString: "KUBECTL ERR :: KUBECTL ERR", ExpectedEnabled: true},
|
ExpectedString: "KUBECTL ERR :: KUBECTL ERR", ExpectedEnabled: true},
|
||||||
{Case: "kubectl error hidden", Template: standardTemplate, DisplayError: false, KubectlExists: true, Context: "aaa", Namespace: "bbb", KubectlErr: true,
|
{Case: "kubectl error hidden", Template: standardTemplate, DisplayError: false, KubectlExists: true, Context: "aaa", Namespace: "bbb", KubectlErr: true, ExpectedEnabled: false},
|
||||||
ExpectedString: "", 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 {
|
for _, tc := range cases {
|
||||||
args := &kubectlArgs{
|
args := &kubectlArgs{
|
||||||
kubectlExists: tc.KubectlExists,
|
kubectlExists: tc.KubectlExists,
|
||||||
template: tc.Template,
|
template: tc.Template,
|
||||||
displayError: tc.DisplayError,
|
displayError: tc.DisplayError,
|
||||||
context: tc.Context,
|
kubectlOutContext: tc.Context,
|
||||||
namespace: tc.Namespace,
|
kubectlOutNamespace: tc.Namespace,
|
||||||
kubectlErr: tc.KubectlErr,
|
kubectlOutUser: tc.User,
|
||||||
|
kubectlOutCluster: tc.Cluster,
|
||||||
|
kubectlErr: tc.KubectlErr,
|
||||||
|
parseKubeConfig: tc.ParseKubeConfig,
|
||||||
|
files: tc.Files,
|
||||||
|
kubeconfig: tc.Kubeconfig,
|
||||||
}
|
}
|
||||||
kubectl := bootStrapKubectlTest(args)
|
kubectl := bootStrapKubectlTest(args)
|
||||||
assert.Equal(t, tc.ExpectedEnabled, kubectl.enabled(), tc.Case)
|
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",
|
"title": "Display Error",
|
||||||
"description": "Show the error context when failing to retrieve the kubectl information",
|
"description": "Show the error context when failing to retrieve the kubectl information",
|
||||||
"default": false
|
"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