diff --git a/src/engine/migrate_test.go b/src/engine/migrate_test.go index 428ee8a4..b8322c03 100644 --- a/src/engine/migrate_test.go +++ b/src/engine/migrate_test.go @@ -6,6 +6,7 @@ import ( "github.com/jandedobbeleer/oh-my-posh/src/mock" "github.com/jandedobbeleer/oh-my-posh/src/platform" "github.com/jandedobbeleer/oh-my-posh/src/properties" + "github.com/jandedobbeleer/oh-my-posh/src/segments" "github.com/stretchr/testify/assert" ) @@ -385,7 +386,7 @@ func TestMigratePreAndPostfix(t *testing.T) { }, { Case: "Prefix", - Expected: " {{ .Name }} ", + Expected: segments.NameTemplate, Props: properties.Map{ "prefix": " ", "template": "{{ .Name }}", @@ -393,7 +394,7 @@ func TestMigratePreAndPostfix(t *testing.T) { }, { Case: "Postfix", - Expected: " {{ .Name }} ", + Expected: segments.NameTemplate, Props: properties.Map{ "postfix": " ", "template": "{{ .Name }} ", diff --git a/src/engine/segment.go b/src/engine/segment.go index e3dfba42..4329f62d 100644 --- a/src/engine/segment.go +++ b/src/engine/segment.go @@ -98,6 +98,8 @@ const ( // ANGULAR writes which angular cli version us currently active ANGULAR SegmentType = "angular" + // ARGOCD writes the current argocd context + ARGOCD SegmentType = "argocd" // AWS writes the active aws context AWS SegmentType = "aws" // AZ writes the Azure subscription info we're currently in @@ -246,6 +248,7 @@ const ( // Consumers of the library can also add their own segment writer. var Segments = map[SegmentType]func() SegmentWriter{ ANGULAR: func() SegmentWriter { return &segments.Angular{} }, + ARGOCD: func() SegmentWriter { return &segments.Argocd{} }, AWS: func() SegmentWriter { return &segments.Aws{} }, AZ: func() SegmentWriter { return &segments.Az{} }, AZFUNC: func() SegmentWriter { return &segments.AzFunc{} }, diff --git a/src/go.mod b/src/go.mod index 21cbb200..f781f348 100644 --- a/src/go.mod +++ b/src/go.mod @@ -38,6 +38,7 @@ require ( github.com/hashicorp/hcl/v2 v2.16.2 github.com/mattn/go-runewidth v0.0.14 github.com/spf13/cobra v1.6.1 + github.com/spf13/pflag v1.0.5 golang.org/x/mod v0.9.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -84,7 +85,6 @@ require ( github.com/rivo/uniseg v0.4.4 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/zclconf/go-cty v1.13.1 // indirect golang.org/x/sync v0.1.0 // indirect diff --git a/src/segments/argocd.go b/src/segments/argocd.go new file mode 100644 index 00000000..4f761248 --- /dev/null +++ b/src/segments/argocd.go @@ -0,0 +1,110 @@ +package segments + +import ( + "errors" + "os" + "path" + "strings" + + "github.com/jandedobbeleer/oh-my-posh/src/platform" + "github.com/jandedobbeleer/oh-my-posh/src/properties" + "github.com/spf13/pflag" + "gopkg.in/yaml.v3" +) + +const ( + argocdOptsEnv = "ARGOCD_OPTS" + argocdInvalidFlag = "invalid flag" + argocdInvalidYaml = "invalid yaml" + argocdNoCurrent = "no current context" + + NameTemplate = " {{ .Name }} " +) + +type ArgocdContext struct { + Name string `yaml:"name"` + Server string `yaml:"server"` + User string `yaml:"user"` +} + +type ArgocdConfig struct { + Contexts []*ArgocdContext `yaml:"contexts"` + CurrentContext string `yaml:"current-context"` +} + +type Argocd struct { + props properties.Properties + env platform.Environment + + ArgocdContext +} + +func (a *Argocd) Template() string { + return NameTemplate +} + +func (a *Argocd) Init(props properties.Properties, env platform.Environment) { + a.props = props + a.env = env +} + +func (a *Argocd) Enabled() bool { + // always parse config instead of using cli to save time + configPath := a.getConfigPath() + succeeded, err := a.parseConfig(configPath) + if err != nil { + a.env.Error(err) + return false + } + return succeeded +} + +func (a *Argocd) getConfigPath() string { + cp := path.Join(a.env.Home(), ".config", "argocd", "config") + cpo := a.getConfigFromOpts() + if len(cpo) > 0 { + cp = cpo + } + return cp +} + +func (a *Argocd) getConfigFromOpts() string { + // don't exit/panic when encountering invalid flags + flags := pflag.NewFlagSet(os.Args[0], pflag.ContinueOnError) + // ignore other valid and invalid flags + flags.ParseErrorsWhitelist.UnknownFlags = true + // only care about config + flags.String("config", "", "get config from opts") + + opts := a.env.Getenv(argocdOptsEnv) + _ = flags.Parse(strings.Split(opts, " ")) + return flags.Lookup("config").Value.String() +} + +func (a *Argocd) parseConfig(file string) (bool, error) { + config := a.env.FileContent(file) + // missing or empty file content + if len(config) == 0 { + return false, errors.New(argocdInvalidYaml) + } + + var data ArgocdConfig + err := yaml.Unmarshal([]byte(config), &data) + if err != nil { + a.env.Error(err) + return false, errors.New(argocdInvalidYaml) + } + a.Name = data.CurrentContext + for _, context := range data.Contexts { + if context.Name == a.Name { + // mandatory fields in yaml + if len(context.Server) == 0 || len(context.User) == 0 { + return false, errors.New(argocdInvalidYaml) + } + a.Server = context.Server + a.User = context.User + return true, nil + } + } + return false, errors.New(argocdNoCurrent) +} diff --git a/src/segments/argocd_test.go b/src/segments/argocd_test.go new file mode 100644 index 00000000..398d0ace --- /dev/null +++ b/src/segments/argocd_test.go @@ -0,0 +1,273 @@ +package segments + +import ( + "fmt" + "path" + "testing" + + "github.com/jandedobbeleer/oh-my-posh/src/mock" + "github.com/jandedobbeleer/oh-my-posh/src/properties" + "github.com/stretchr/testify/assert" + mock2 "github.com/stretchr/testify/mock" +) + +const ( + poshHome = "/Users/posh" +) + +func TestArgocdGetConfigFromOpts(t *testing.T) { + configFile := "/Users/posh/.config/argocd/config" + cases := []struct { + Case string + Opts string + Expected string + }{ + {Case: "invalid flag in opts", Opts: "--invalid", Expected: ""}, + {Case: "no config in opts", Opts: "--grpc-web", Expected: ""}, + { + Case: "config in opts", + Opts: fmt.Sprintf("--grpc-web --config %s --plaintext", configFile), + Expected: configFile, + }, + } + + for _, tc := range cases { + env := new(mock.MockedEnvironment) + env.On("Getenv", argocdOptsEnv).Return(tc.Opts) + + argocd := &Argocd{ + env: env, + props: properties.Map{}, + } + config := argocd.getConfigFromOpts() + assert.Equal(t, tc.Expected, config, tc.Case) + } +} + +func TestArgocdGetConfigPath(t *testing.T) { + configFile := path.Join(poshHome, ".config", "argocd", "config") + cases := []struct { + Case string + Opts string + Expected string + ExpectedError string + }{ + {Case: "without opts", Expected: configFile}, + {Case: "with opts", Opts: "--config /etc/argocd/config", Expected: "/etc/argocd/config"}, + } + + for _, tc := range cases { + env := new(mock.MockedEnvironment) + env.On("Home").Return(poshHome) + env.On("Getenv", argocdOptsEnv).Return(tc.Opts) + + argocd := &Argocd{ + env: env, + props: properties.Map{}, + } + assert.Equal(t, tc.Expected, argocd.getConfigPath()) + } +} + +func TestArgocdParseConfig(t *testing.T) { + configFile := "/Users/posh/.config/argocd/config" + cases := []struct { + Case string + Config string + Expected bool + ExpectedError string + ExpectedContext ArgocdContext + }{ + {Case: "missing or empty yaml", Config: "", ExpectedError: argocdInvalidYaml}, + { + Case: "invalid yaml", + ExpectedError: argocdInvalidYaml, + Config: ` +[context] +context +`, + }, + { + Case: "invalid config", + ExpectedError: argocdInvalidYaml, + Config: ` +contexts: + - name: context1 + server: server1 + user: user1 + - name: context2 + server: server2 + userr: user2 +current-context: context2 +servers: + - grpc-web: true + server: server1 + - grpc-web: false + server: serve2 +`, + }, + { + Case: "no current context found", + ExpectedError: argocdNoCurrent, + Config: ` +contexts: + - name: context1 + server: server1 + user: user1 + - name: context2 + server: server2 + user: user2 +`, + }, + { + Case: "current context found", + Expected: true, + Config: ` +contexts: + - name: context1 + server: server1 + user: user1 + - name: context2 + server: server2 + user: user2 +current-context: context2 +servers: + - grpc-web: true + server: server1 + - grpc-web: false + server: serve2 +users: + - auth-token: authtoken1 + name: user1 + refresh-token: refreshtoken1 + - auth-token: authtoken2 + name: user2 + refresh-token: refreshtoken2 +`, + ExpectedContext: ArgocdContext{ + Name: "context2", + Server: "server2", + User: "user2", + }, + }, + } + + for _, tc := range cases { + env := new(mock.MockedEnvironment) + env.On("FileContent", configFile).Return(tc.Config) + env.On("Error", mock2.Anything).Return() + + argocd := &Argocd{ + env: env, + props: properties.Map{}, + } + if len(tc.ExpectedError) > 0 { + _, err := argocd.parseConfig(configFile) + assert.EqualError(t, err, tc.ExpectedError, tc.Case) + continue + } + config, err := argocd.parseConfig(configFile) + assert.NoErrorf(t, err, tc.Case) + assert.Equal(t, tc.Expected, config, tc.Case) + assert.Equal(t, tc.ExpectedContext, argocd.ArgocdContext, tc.Case) + } +} + +func TestArgocdSegment(t *testing.T) { + configFile := path.Join(poshHome, ".config", "argocd", "config") + cases := []struct { + Case string + Opts string + Config string + Template string + ExpectedString string + ExpectedEnabled bool + ExpectedError string + ExpectedContext ArgocdContext + }{ + { + Case: "default template", + Opts: "", + Config: ` +contexts: + - name: context1 + server: server1 + user: user1 + - name: context2 + server: server2 + user: user2 +current-context: context2 +servers: + - grpc-web: true + server: server1 + - grpc-web: false + server: serve2 +`, + ExpectedString: "context2", + ExpectedEnabled: true, + ExpectedContext: ArgocdContext{ + Name: "context2", + Server: "server2", + User: "user2", + }, + }, + { + Case: "full template", + Opts: "", + Config: ` +contexts: + - name: context1 + server: server1 + user: user1 + - name: context2 + server: server2 + user: user2 +current-context: context2 +servers: + - grpc-web: true + server: server1 + - grpc-web: false + server: serve2 +`, + Template: "{{ .Name }}:{{ .User}}@{{ .Server }}", + ExpectedString: "context2:user2@server2", + ExpectedEnabled: true, + ExpectedContext: ArgocdContext{ + Name: "context2", + Server: "server2", + User: "user2", + }, + }, + { + Case: "broken config", + Config: `}`, + ExpectedEnabled: false, + }, + } + + for _, tc := range cases { + env := new(mock.MockedEnvironment) + env.On("Home").Return(poshHome) + env.On("Getenv", argocdOptsEnv).Return(tc.Opts) + env.On("FileContent", configFile).Return(tc.Config) + env.On("Error", mock2.Anything).Return() + + argocd := &Argocd{ + env: env, + props: properties.Map{}, + } + + assert.Equal(t, tc.ExpectedEnabled, argocd.Enabled(), tc.Case) + + if !tc.ExpectedEnabled { + continue + } + + assert.Equal(t, tc.ExpectedContext, argocd.ArgocdContext, tc.Case) + if len(tc.Template) > 0 { + assert.Equal(t, tc.ExpectedString, renderTemplate(env, tc.Template, argocd), tc.Case) + } else { + assert.Equal(t, tc.ExpectedString, renderTemplate(env, argocd.Template(), argocd), tc.Case) + } + } +} diff --git a/src/segments/az.go b/src/segments/az.go index 3d9aaaad..e1e05e51 100644 --- a/src/segments/az.go +++ b/src/segments/az.go @@ -71,7 +71,7 @@ type AzurePowerShellSubscription struct { } func (a *Az) Template() string { - return " {{ .Name }} " + return NameTemplate } func (a *Az) Init(props properties.Properties, env platform.Environment) { diff --git a/src/segments/az_test.go b/src/segments/az_test.go index 7cfb7c9d..17557a59 100644 --- a/src/segments/az_test.go +++ b/src/segments/az_test.go @@ -109,8 +109,7 @@ func TestAzSegment(t *testing.T) { for _, tc := range cases { env := new(mock.MockedEnvironment) - home := "/Users/posh" - env.On("Home").Return(home) + env.On("Home").Return(poshHome) var azureProfile, azureRmContext string if tc.HasCLI { @@ -123,7 +122,7 @@ func TestAzSegment(t *testing.T) { } env.On("GOOS").Return(platform.LINUX) - env.On("FileContent", filepath.Join(home, ".azure", "azureProfile.json")).Return(azureProfile) + env.On("FileContent", filepath.Join(poshHome, ".azure", "azureProfile.json")).Return(azureProfile) env.On("Getenv", "POSH_AZURE_SUBSCRIPTION").Return(azureRmContext) env.On("Getenv", "AZURE_CONFIG_DIR").Return("") diff --git a/src/segments/git_test.go b/src/segments/git_test.go index 7ec2f1fc..23327e95 100644 --- a/src/segments/git_test.go +++ b/src/segments/git_test.go @@ -51,7 +51,7 @@ func TestEnabledInWorkingDirectory(t *testing.T) { env.On("IsWsl").Return(false) env.On("HasParentFilePath", ".git").Return(fileInfo, nil) env.On("PathSeparator").Return("/") - env.On("Home").Return("/Users/posh") + env.On("Home").Return(poshHome) env.On("Getenv", poshGitEnv).Return("") env.On("DirMatchesOneOf", mock2.Anything, mock2.Anything).Return(false) g := &Git{ diff --git a/src/segments/mercurial_test.go b/src/segments/mercurial_test.go index 4fffe781..f3d39214 100644 --- a/src/segments/mercurial_test.go +++ b/src/segments/mercurial_test.go @@ -39,7 +39,7 @@ func TestMercurialEnabledInWorkingDirectory(t *testing.T) { env.On("IsWsl").Return(false) env.On("HasParentFilePath", ".hg").Return(fileInfo, nil) env.On("PathSeparator").Return("/") - env.On("Home").Return("/Users/posh") + env.On("Home").Return(poshHome) env.On("Getenv", poshGitEnv).Return("") hg := &Mercurial{ @@ -153,7 +153,7 @@ A Added.File env.On("IsWsl").Return(false) env.On("HasParentFilePath", ".hg").Return(fileInfo, nil) env.On("PathSeparator").Return("/") - env.On("Home").Return("/Users/posh") + env.On("Home").Return(poshHome) env.On("Getenv", poshGitEnv).Return("") env.MockHgCommand(fileInfo.Path, tc.LogOutput, "log", "-r", ".", "--template", hgLogTemplate) env.MockHgCommand(fileInfo.Path, tc.StatusOutput, "status") diff --git a/src/segments/shell.go b/src/segments/shell.go index e8295e33..4f36f3a3 100644 --- a/src/segments/shell.go +++ b/src/segments/shell.go @@ -21,7 +21,7 @@ const ( ) func (s *Shell) Template() string { - return " {{ .Name }} " + return NameTemplate } func (s *Shell) Enabled() bool { diff --git a/website/docs/segments/argocd.mdx b/website/docs/segments/argocd.mdx new file mode 100644 index 00000000..2dffb62b --- /dev/null +++ b/website/docs/segments/argocd.mdx @@ -0,0 +1,42 @@ +--- +id: argocd +title: ArgoCD Context +sidebar_label: ArgoCD +--- + +## What + +Display the current ArgoCD context name, user and/or server. + +## Sample Configuration + +import Config from "@site/src/components/Config.js"; + + + +## Template ([info][templates]) + +:::note default template + +```template +{{ .Name }} +``` + +### Properties + +| Name | Type | Description | +| --------- | -------- | --------------------------------- | +| `.Name` | `string` | the current context name | +| `.Server` | `string` | the server of the current context | +| `.User` | `string` | the user of the current context | + +[templates]: /docs/configuration/templates diff --git a/website/sidebars.js b/website/sidebars.js index 92014c4c..cd7d16d0 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -52,6 +52,7 @@ module.exports = { collapsed: true, items: [ "segments/angular", + "segments/argocd", "segments/aws", "segments/az", "segments/azfunc",