feat(argocd): add context segment

This commit is contained in:
Jason Zhang 2023-04-03 18:10:51 +08:00 committed by Jan De Dobbeleer
parent 4f56a96dca
commit ddec1197df
12 changed files with 440 additions and 11 deletions

View file

@ -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 }} ",

View file

@ -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{} },

View file

@ -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

110
src/segments/argocd.go Normal file
View file

@ -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)
}

273
src/segments/argocd_test.go Normal file
View file

@ -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)
}
}
}

View file

@ -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) {

View file

@ -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("")

View file

@ -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{

View file

@ -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")

View file

@ -21,7 +21,7 @@ const (
)
func (s *Shell) Template() string {
return " {{ .Name }} "
return NameTemplate
}
func (s *Shell) Enabled() bool {

View file

@ -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";
<Config
data={{
type: "argocd",
style: "powerline",
powerline_symbol: "\uE0B0",
foreground: "#ffffff",
background: "#FFA400",
template: " {{ .Name }}:{{ .User }}@{{ .Server }} ",
}}
/>
## 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

View file

@ -52,6 +52,7 @@ module.exports = {
collapsed: true,
items: [
"segments/angular",
"segments/argocd",
"segments/aws",
"segments/az",
"segments/azfunc",